Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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,10 +27,47 @@ 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 `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
24 changes: 24 additions & 0 deletions clients/go/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,23 @@ 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`.
### Added
- `status` and `error` fields on `ChangesetOperation` and the
`changeset/operationStatusChanged` action, tracking the
`idle → running → error` lifecycle of a changeset operation.
Expand All @@ -31,6 +48,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 @@ -586,6 +590,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 @@ -769,7 +775,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 @@ -781,7 +787,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 @@ -829,7 +835,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 @@ -846,7 +852,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 @@ -886,7 +892,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 @@ -904,7 +910,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 @@ -931,7 +937,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 @@ -949,7 +955,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 @@ -959,6 +965,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 @@ -1110,6 +1156,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