Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/instructions/general-instructions.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ applyTo: 'types/**/*.ts'

- `number` types are assumed to be 64-bit integers. If a floating point values are reasonable for a field, you MUST annotate its jsdoc with `@format float`
- For actions or commands that could be implemented by returning an array `T[]` directly, still prefer to wrap it in `{ items: T[] }` for forward compatibility. This allows adding additional fields later without breaking the shape.
- Naming discriminants for discriminated unions:
- Lifecycle / state-machine unions: name the union `Foo*State` and its discriminant enum `Foo*Status`. Variant interfaces are `Foo*State` (e.g. `ToolCallState` + `ToolCallStatus` + `ToolCallStreamingState`; `McpServerState` + `McpServerStatus` + `McpServerStartingState`; `CustomizationLoadState` + `CustomizationLoadStatus`).
- General/typological unions (not a lifecycle): name the discriminant `Foo*Kind` (e.g. `MessageAttachment` + `MessageAttachmentKind`, `ResponsePart` + `ResponsePartKind`, `ToolCallContributor` + `ToolCallContributorKind`).
- Generator note: variant interface names must differ from the union wrapper names emitted by the per-language generators (e.g. Kotlin emits `value class FooStateStarting(val value: FooStartingState)`), so name variants `Foo*State` rather than `FooStatus*`.
- After making your changes, check to make sure the documentation in `docs` is up to date. For significant new flows or features, consider adding new documentation for it. Note that Mermaid diagrams are allowed.
- Whenever you change or add an action, you must review the reducers in `types/reducers.ts` to see if that needs to be propagated into the state. If it does, add the appropriate logic and unit tests for it.
- Never update the protocol version unless you were instructed to do so.
Expand Down
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,50 @@ changes accumulate. Track in-flight protocol changes via PRs touching

Spec version: `0.3.0`

### Added

- `McpServerCustomization` now models MCP servers as first-class session
customizations: `enabled`, `state` (a discriminated
`McpServerState` union covering `starting`, `ready`, `authRequired`,
`error`, `stopped`), an optional `channel` URI for an `mcp://`
side-channel into the upstream server, and an optional `mcpApp` block
carrying `AhpMcpUiHostCapabilities` so clients can render
[MCP Apps](https://github.com/modelcontextprotocol/ext-apps).
- `McpServerAuthRequiredState` carries `ProtectedResourceMetadata` plus
`reason` / `requiredScopes` / `description`, letting clients drive the
existing `authenticate` command for per-MCP-server auth challenges.
- `Customization` now includes `McpServerCustomization` at the top level
(hosts MAY surface globally-configured MCP servers directly rather
than only inside a plugin or directory). MCP servers remain valid as
children of a container.
- New `session/mcpServerStateChanged` action — narrow upsert of
`state` + `channel` on an existing `McpServerCustomization`
by id, intended for the high-frequency
`starting`/`ready`/`authRequired` transitions. Other customization
fields stay in `session/customizationUpdated` territory.
- `InitializeParams.capabilities` — optional client-capability bag
declared during the handshake. First entry is `mcpApps?: {}`; hosts
SHOULD only populate `McpServerCustomization.mcpApp` / `channel` for
clients that declared it.
- New guide page `docs/guide/mcp.md` (with an MCP Apps subsection) and
new spec page `docs/specification/mcp-channel.md`.
- Added `changeKind` to `Changeset` (well-known values: `'session'`,
`'branch'`, `'uncommitted'`, `'turn'`, `'compare-turns'`) so clients can
group, sort, or pick an icon without parsing `uriTemplate`.
- Added `status` and `error` to `ChangesetOperation` and a new
`changeset/operationStatusChanged` action so servers can reflect an
operation's execution lifecycle (`idle → running → error`) back into
changeset state.

### Changed

- Replaced `ToolCallBase.toolClientId?: string` with a discriminated
`ToolCallBase.contributor?: ToolCallContributor` union
(`ToolCallClientContributor` / `ToolCallMcpContributor`) so MCP-served
tool calls can be attributed back to their originating
`McpServerCustomization`. `session/toolCallStart` carries the new
`contributor?` field in place of `toolClientId?`.

- Added optional `_meta` provider metadata to `AgentCustomization`.
- Added optional `changes` field of type `ChangesSummary` to `SessionSummary`,
carrying optional `additions`, `deletions`, and `files` counts so servers
Expand Down
23 changes: 23 additions & 0 deletions clients/go/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,22 @@ tag whose matching `## [X.Y.Z]` heading is missing from this file.

### Added

- `McpServerCustomization` now exposes the full MCP lifecycle: `Enabled`,
the discriminated `McpServerState` union
(`Starting`/`Ready`/`AuthRequired`/`Error`/`Stopped`), optional
`Channel` URI for the `mcp://` side-channel, and optional `McpApp`
block carrying `AhpMcpUiHostCapabilities` for MCP Apps.
- `McpServerAuthRequiredState` variant carries `ProtectedResourceMetadata`
plus `Reason` / `RequiredScopes` / `Description` so the existing
`authenticate` command can drive per-server auth.
- `Customization` top-level union now includes `McpServer` — hosts MAY
surface bare MCP servers directly rather than only inside a plugin or
directory.
- `SessionMcpServerStateChangedAction` and matching reducer case —
narrow upsert of `State` + `Channel` on an existing MCP
server customization by id.
- `ClientCapabilities` struct on `InitializeParams.Capabilities` with
first entry `McpApps`.
- `changeKind` field on `Changeset` (well-known values: `'session'`,
`'branch'`, `'uncommitted'`, `'turn'`, `'compare-turns'`).
- `status` and `error` fields on `ChangesetOperation` and the
Expand All @@ -34,6 +50,13 @@ tag whose matching `## [X.Y.Z]` heading is missing from this file.

- Removed the `additions`, `deletions`, and `files` fields from `ChangesetSummary`. Aggregate counts now live on `SessionSummary.changes`; per-changeset views derive their own totals from `ChangesetState.files`.

### Changed

- `ToolCallBase.ToolClientId *string` replaced by
`ToolCallBase.Contributor *ToolCallContributor` (union with
`Client { ClientId }` and `Mcp { CustomizationId }` variants).
`SessionToolCallStartAction` carries the new `Contributor` field as
well. The reducer follows the rename.
## [0.1.0] — 2026-05-28

Implements AHP `0.2.0`.
Expand Down
99 changes: 73 additions & 26 deletions clients/go/ahp/reducers.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,27 +70,27 @@ func withStatusFlag(status, flag ahptypes.SessionStatus, set bool) ahptypes.Sess
// toolCallCommon carries the fields shared by every concrete
// [ahptypes.ToolCallState] variant.
type toolCallCommon struct {
id string
name string
displayName string
toolClientID *string
meta ahptypes.JSONObject
id string
name string
displayName string
contributor *ahptypes.ToolCallContributor
meta ahptypes.JSONObject
}

func toolCallMeta(tc ahptypes.ToolCallState) toolCallCommon {
switch v := tc.Value.(type) {
case *ahptypes.ToolCallStreamingState:
return toolCallCommon{v.ToolCallId, v.ToolName, v.DisplayName, v.ToolClientId, v.Meta}
return toolCallCommon{v.ToolCallId, v.ToolName, v.DisplayName, v.Contributor, v.Meta}
case *ahptypes.ToolCallPendingConfirmationState:
return toolCallCommon{v.ToolCallId, v.ToolName, v.DisplayName, v.ToolClientId, v.Meta}
return toolCallCommon{v.ToolCallId, v.ToolName, v.DisplayName, v.Contributor, v.Meta}
case *ahptypes.ToolCallRunningState:
return toolCallCommon{v.ToolCallId, v.ToolName, v.DisplayName, v.ToolClientId, v.Meta}
return toolCallCommon{v.ToolCallId, v.ToolName, v.DisplayName, v.Contributor, v.Meta}
case *ahptypes.ToolCallPendingResultConfirmationState:
return toolCallCommon{v.ToolCallId, v.ToolName, v.DisplayName, v.ToolClientId, v.Meta}
return toolCallCommon{v.ToolCallId, v.ToolName, v.DisplayName, v.Contributor, v.Meta}
case *ahptypes.ToolCallCompletedState:
return toolCallCommon{v.ToolCallId, v.ToolName, v.DisplayName, v.ToolClientId, v.Meta}
return toolCallCommon{v.ToolCallId, v.ToolName, v.DisplayName, v.Contributor, v.Meta}
case *ahptypes.ToolCallCancelledState:
return toolCallCommon{v.ToolCallId, v.ToolName, v.DisplayName, v.ToolClientId, v.Meta}
return toolCallCommon{v.ToolCallId, v.ToolName, v.DisplayName, v.Contributor, v.Meta}
}
return toolCallCommon{}
}
Expand Down Expand Up @@ -189,7 +189,7 @@ func endTurn(state *ahptypes.SessionState, turnID string, turnState ahptypes.Tur
ToolCallId: common.id,
ToolName: common.name,
DisplayName: common.displayName,
ToolClientId: common.toolClientID,
Contributor: common.contributor,
Meta: common.meta,
InvocationMessage: invocation,
ToolInput: toolInput,
Expand Down Expand Up @@ -248,6 +248,8 @@ func customizationID(c ahptypes.Customization) (string, bool) {
return v.Id, true
case *ahptypes.DirectoryCustomization:
return v.Id, true
case *ahptypes.McpServerCustomization:
return v.Id, true
}
return "", false
}
Expand Down Expand Up @@ -286,6 +288,8 @@ func setContainerEnabled(c *ahptypes.Customization, enabled bool) {
v.Enabled = enabled
case *ahptypes.DirectoryCustomization:
v.Enabled = enabled
case *ahptypes.McpServerCustomization:
v.Enabled = enabled
}
}

Expand Down Expand Up @@ -421,12 +425,12 @@ func ApplyActionToSession(state *ahptypes.SessionState, action ahptypes.StateAct
state.ActiveTurn.ResponseParts = append(state.ActiveTurn.ResponseParts, ahptypes.ResponsePart{Value: &ahptypes.ToolCallResponsePart{
Kind: ahptypes.ResponsePartKindToolCall,
ToolCall: ahptypes.ToolCallState{Value: &ahptypes.ToolCallStreamingState{
Status: ahptypes.ToolCallStatusStreaming,
ToolCallId: a.ToolCallId,
ToolName: a.ToolName,
DisplayName: a.DisplayName,
ToolClientId: a.ToolClientId,
Meta: a.Meta,
Status: ahptypes.ToolCallStatusStreaming,
ToolCallId: a.ToolCallId,
ToolName: a.ToolName,
DisplayName: a.DisplayName,
Contributor: a.Contributor,
Meta: a.Meta,
}},
}})
return ReduceOutcomeApplied
Expand Down Expand Up @@ -590,6 +594,8 @@ func ApplyActionToSession(state *ahptypes.SessionState, action ahptypes.StateAct
}
}
return ReduceOutcomeNoOp
case *ahptypes.SessionMcpServerStateChangedAction:
return applyMcpServerStatusChanged(state, a)
case *ahptypes.SessionTruncatedAction:
return applyTruncated(state, a.TurnId)
case *ahptypes.SessionInputRequestedAction:
Expand Down Expand Up @@ -773,7 +779,7 @@ func applyToolCallReady(state *ahptypes.SessionState, a *ahptypes.SessionToolCal
ToolCallId: common.id,
ToolName: common.name,
DisplayName: common.displayName,
ToolClientId: common.toolClientID,
Contributor: common.contributor,
Meta: common.meta,
InvocationMessage: a.InvocationMessage,
ToolInput: a.ToolInput,
Expand All @@ -785,7 +791,7 @@ func applyToolCallReady(state *ahptypes.SessionState, a *ahptypes.SessionToolCal
ToolCallId: common.id,
ToolName: common.name,
DisplayName: common.displayName,
ToolClientId: common.toolClientID,
Contributor: common.contributor,
Meta: common.meta,
InvocationMessage: a.InvocationMessage,
ToolInput: a.ToolInput,
Expand Down Expand Up @@ -833,7 +839,7 @@ func applyToolCallConfirmed(state *ahptypes.SessionState, a *ahptypes.SessionToo
ToolCallId: s.ToolCallId,
ToolName: s.ToolName,
DisplayName: s.DisplayName,
ToolClientId: s.ToolClientId,
Contributor: s.Contributor,
Meta: s.Meta,
InvocationMessage: s.InvocationMessage,
ToolInput: toolInput,
Expand All @@ -850,7 +856,7 @@ func applyToolCallConfirmed(state *ahptypes.SessionState, a *ahptypes.SessionToo
ToolCallId: s.ToolCallId,
ToolName: s.ToolName,
DisplayName: s.DisplayName,
ToolClientId: s.ToolClientId,
Contributor: s.Contributor,
Meta: s.Meta,
InvocationMessage: s.InvocationMessage,
ToolInput: s.ToolInput,
Expand Down Expand Up @@ -890,7 +896,7 @@ func applyToolCallComplete(state *ahptypes.SessionState, a *ahptypes.SessionTool
ToolCallId: common.id,
ToolName: common.name,
DisplayName: common.displayName,
ToolClientId: common.toolClientID,
Contributor: common.contributor,
Meta: common.meta,
InvocationMessage: invocation,
ToolInput: toolInput,
Expand All @@ -908,7 +914,7 @@ func applyToolCallComplete(state *ahptypes.SessionState, a *ahptypes.SessionTool
ToolCallId: common.id,
ToolName: common.name,
DisplayName: common.displayName,
ToolClientId: common.toolClientID,
Contributor: common.contributor,
Meta: common.meta,
InvocationMessage: invocation,
ToolInput: toolInput,
Expand All @@ -935,7 +941,7 @@ func applyToolCallResultConfirmed(state *ahptypes.SessionState, a *ahptypes.Sess
ToolCallId: s.ToolCallId,
ToolName: s.ToolName,
DisplayName: s.DisplayName,
ToolClientId: s.ToolClientId,
Contributor: s.Contributor,
Meta: s.Meta,
InvocationMessage: s.InvocationMessage,
ToolInput: s.ToolInput,
Expand All @@ -953,7 +959,7 @@ func applyToolCallResultConfirmed(state *ahptypes.SessionState, a *ahptypes.Sess
ToolCallId: s.ToolCallId,
ToolName: s.ToolName,
DisplayName: s.DisplayName,
ToolClientId: s.ToolClientId,
Contributor: s.Contributor,
Meta: s.Meta,
InvocationMessage: s.InvocationMessage,
ToolInput: s.ToolInput,
Expand All @@ -963,6 +969,46 @@ func applyToolCallResultConfirmed(state *ahptypes.SessionState, a *ahptypes.Sess
})
}

func applyMcpServerStatusChanged(state *ahptypes.SessionState, a *ahptypes.SessionMcpServerStateChangedAction) ReduceOutcome {
list := state.Customizations
if list == nil {
return ReduceOutcomeNoOp
}
for i := range list {
got, ok := customizationID(list[i])
if !ok || got != a.Id {
continue
}
mcp, ok := list[i].Value.(*ahptypes.McpServerCustomization)
if !ok {
return ReduceOutcomeNoOp
}
mcp.State = a.State
mcp.Channel = a.Channel
return ReduceOutcomeApplied
}
for i := range list {
children := containerChildren(&list[i])
if children == nil {
continue
}
for j := range *children {
got, ok := childCustomizationID((*children)[j])
if !ok || got != a.Id {
continue
}
mcp, ok := (*children)[j].Value.(*ahptypes.McpServerCustomization)
if !ok {
return ReduceOutcomeNoOp
}
mcp.State = a.State
mcp.Channel = a.Channel
return ReduceOutcomeApplied
}
}
return ReduceOutcomeNoOp
}

func applyTruncated(state *ahptypes.SessionState, turnID *string) ReduceOutcome {
if turnID == nil {
state.Turns = []ahptypes.Turn{}
Expand Down Expand Up @@ -1114,6 +1160,7 @@ func ApplyActionToChangeset(state *ahptypes.ChangesetState, action ahptypes.Stat
*ahptypes.ChangesetFileSetAction,
*ahptypes.ChangesetFileRemovedAction,
*ahptypes.ChangesetOperationsChangedAction,
*ahptypes.ChangesetOperationStatusChangedAction,
*ahptypes.ChangesetClearedAction:
return ReduceOutcomeNoOp
}
Expand Down
Loading
Loading