From d7ee2672d25c7a5c5510e41c857a647b7f85f667 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 1 Jun 2026 21:09:43 -0700 Subject: [PATCH 1/4] agentHost: MCP support - Represents MCP servers in customizations and allow their state/ authentication requirements to be reflected. - Breaking: swap `ToolCallBase.clientId` -> `ToolCallBase.contributor` to allow signalling tool calls were triggered by particular MCP servers. - Add additional ToolCallBase._meta note for MCP servers and introduce the `mcp://` protocol channel. This should support all the base features of MCP + MCP Apps, excluding sampling, which is deprecated in the upcoming 2026-07-28 MCP spec and therefore not something I bothered including. --- CHANGELOG.md | 36 ++ docs/.vitepress/config.mts | 2 + docs/guide/customizations.md | 14 +- docs/guide/mcp.md | 249 ++++++++++++ docs/specification/mcp-channel.md | 52 +++ schema/actions.schema.json | 325 +++++++++++++-- schema/commands.schema.json | 285 ++++++++++++-- schema/errors.schema.json | 251 ++++++++++-- schema/notifications.schema.json | 251 ++++++++++-- schema/state.schema.json | 288 ++++++++++++-- types/action-origin.generated.ts | 4 + types/channels-root/state.ts | 12 +- types/channels-session/actions.ts | 60 ++- types/channels-session/reducer.ts | 62 ++- types/channels-session/state.ts | 372 ++++++++++++++++-- types/common/actions.ts | 3 + types/common/commands.ts | 34 ++ types/index.ts | 1 + ...-force-cancels-in-progress-tool-calls.json | 2 +- ...-start-delta-ready-confirmed-complete.json | 2 +- ...h-auto-confirm-transitions-to-running.json | 2 +- ...-call-denied-transitions-to-cancelled.json | 2 +- ...-result-confirmation-pending-approved.json | 2 +- ...d-cancelled-with-result-denied-reason.json | 2 +- ...nding-confirmation-defaults-confirmed.json | 2 +- ...ing-tool-back-to-pending-confirmation.json | 2 +- ...-approved-transitions-back-to-running.json | 2 +- ...ation-denied-transitions-to-cancelled.json | 2 +- ...-non-streaming-non-running-tool-calls.json | 4 +- ...w-with-tool-calls-and-re-confirmation.json | 4 +- ...dturn-force-cancels-running-tool-call.json | 2 +- ...confirmation-sets-input-needed-status.json | 2 +- ...confirmation-sets-input-needed-status.json | 2 +- ...olcallready-with-confirmation-options.json | 2 +- ...onfirmed-approved-with-selectedoption.json | 2 +- ...lconfirmed-denied-with-selectedoption.json | 2 +- ...edoption-carries-through-to-completed.json | 2 +- ...n-carries-through-result-confirmation.json | 2 +- ...doption-carries-through-result-denied.json | 2 +- types/version/registry.ts | 1 + 40 files changed, 2103 insertions(+), 245 deletions(-) create mode 100644 docs/guide/mcp.md create mode 100644 docs/specification/mcp-channel.md diff --git a/CHANGELOG.md b/CHANGELOG.md index f159a85d..53fb91df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,42 @@ 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`, `runtimeStatus` (a discriminated + `McpServerStatus` 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). +- `McpServerStatusAuthRequired` 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/mcpServerStatusChanged` action — narrow upsert of + `runtimeStatus` + `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`. + +### 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?`. - Renamed the `UserMessage` type to `Message` and surfaced it consistently across turn state (`Turn.message`, `ActiveTurn.message`, `PendingMessage.message`) and the actions that carry it (`session/turnStarted`, diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 9dc8e18b..d1097a0a 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -36,6 +36,7 @@ export default withMermaid(defineConfig({ { text: 'Elicitation', link: '/guide/elicitation' }, { text: 'Terminals', link: '/guide/terminals' }, { text: 'Customizations', link: '/guide/customizations' }, + { text: 'MCP Servers', link: '/guide/mcp' }, { text: 'Write-Ahead Reconciliation', link: '/guide/reconciliation' }, ], }, @@ -57,6 +58,7 @@ export default withMermaid(defineConfig({ { text: 'Lifecycle', link: '/specification/lifecycle' }, { text: 'Channels & Subscriptions', link: '/specification/subscriptions' }, { text: 'Authentication', link: '/specification/authentication' }, + { text: 'MCP Channel', link: '/specification/mcp-channel' }, { text: 'Versioning', link: '/specification/versioning' }, ], }, diff --git a/docs/guide/customizations.md b/docs/guide/customizations.md index 226e1f4f..22b37d02 100644 --- a/docs/guide/customizations.md +++ b/docs/guide/customizations.md @@ -1,11 +1,13 @@ # Customizations -Customizations extend agent sessions with additional capabilities — agents, skills, prompts, rules, hooks, and MCP servers. AHP organises them as a discriminated union with a fixed set of types and a strict two-level tree: +Customizations extend agent sessions with additional capabilities — agents, skills, prompts, rules, hooks, and MCP servers. AHP organises them as a discriminated union with a fixed set of types and a shallow tree: -- **Top-level entries are always containers**: a `PluginCustomization` (an [Open Plugins](https://open-plugins.com/) package) or a `DirectoryCustomization` (a directory the host watches on disk). -- **Everything else is a child** of a container: `AgentCustomization`, `SkillCustomization`, `PromptCustomization`, `RuleCustomization`, `HookCustomization`, `McpServerCustomization`. +- **Top-level entries are typically containers**: a `PluginCustomization` (an [Open Plugins](https://open-plugins.com/) package) or a `DirectoryCustomization` (a directory the host watches on disk). The host MAY also surface a bare `McpServerCustomization` at the top level (for example, a globally-configured MCP server that isn't bundled in a plugin). +- **Other children live inside a container**: `AgentCustomization`, `SkillCustomization`, `PromptCustomization`, `RuleCustomization`, `HookCustomization`, `McpServerCustomization`. MCP servers can therefore appear in either position. -The agent host is authoritative on the effective tree. Clients publish plugins, the host expands them into children, and the host owns disk-backed directories. +The agent host is authoritative on the effective tree. Clients publish plugins, the host expands them into children, and the host owns disk-backed directories and bare top-level MCP servers. + +For MCP-specific behaviour (server lifecycle, authentication, App support), see [MCP Servers](/guide/mcp). ## Sources @@ -112,10 +114,10 @@ SkillCustomization { type: 'skill'; description?, disableModelInvoc PromptCustomization { type: 'prompt'; description? } RuleCustomization { type: 'rule'; description?, alwaysApply?, globs? } // covers "instruction" formats too HookCustomization { type: 'hook'; event?, matcher? } -McpServerCustomization { type: 'mcpServer'; description? } +McpServerCustomization { type: 'mcpServer'; enabled, runtimeStatus, channel?, mcpApp? } // see /guide/mcp ``` -The protocol intentionally omits host-internal execution details (a hook's command/script, an MCP server's `command`/`args`/`env`, etc.). Those stay on the agent host; clients see only what's needed for display, search, and selection. MCP tools and their descriptions surface through the standard tool channels once the server is running. +The protocol intentionally omits host-internal execution details (a hook's command/script, an MCP server's `command`/`args`/`env`, etc.). Those stay on the agent host; clients see only what's needed for display, search, and selection. MCP tools and their descriptions surface through the standard tool channels once the server is running. The MCP-specific runtime fields (`runtimeStatus`, `channel`, `mcpApp`) are covered in [MCP Servers](/guide/mcp). Consumers filter by `type` to find the children they care about — for example, the agent picker reads every `AgentCustomization` under any container: diff --git a/docs/guide/mcp.md b/docs/guide/mcp.md new file mode 100644 index 00000000..ca740d97 --- /dev/null +++ b/docs/guide/mcp.md @@ -0,0 +1,249 @@ +# MCP Servers + +[Model Context Protocol](https://modelcontextprotocol.io/) servers are surfaced in AHP as a [`McpServerCustomization`](/reference/session#mcpservercustomization) — a customization that represents one running (or registered) MCP server within a session. AHP intentionally does **not** re-spec MCP. It exposes: + +- Enough state for clients to render the server (name, icon, enabled flag, runtime status). +- Enough state for clients to drive authentication when the server demands it. +- An optional [`mcp://` side-channel](/specification/mcp-channel) the client can use to talk to the upstream server when it needs to render an [MCP App](#mcp-apps). + +Everything else — connection management, transport, the server's `command`/`args`/`env`, tool discovery, request fan-out — lives in the agent harness the host wraps. The agent host's job is to normalize whatever the harness exposes into AHP state. + +## Where MCP servers appear + +MCP servers may appear in two positions in [`SessionState.customizations`](/reference/session#sessionstate): + +1. **As a child** of a container customization — for example, an MCP server declared inside a `plugins.json` manifest or discovered in a directory. The container's `uri` points at the manifest file; the child's `range` narrows it to the declaration's span. +2. **As a bare top-level entry** — the host MAY surface MCP servers directly when they are globally configured rather than bundled in a plugin or directory. + +Clients only ever publish customizations through `ClientPluginCustomization`, so client-contributed MCP servers always arrive as children of a client plugin. Top-level `McpServerCustomization` entries are always host-originated. + +```typescript +state.customizations + ?.flatMap(c => c.type === 'mcpServer' ? [c] : (c.children ?? [])) + .filter(c => c.type === 'mcpServer') +``` + +## Shape + +```typescript +McpServerCustomization { + type: 'mcpServer' + id: string // session-unique handle + uri: URI // declaration source (file or marketplace URL) + name: string + icons?: Icon[] + range?: TextRange // span inside `uri` for inline declarations + enabled: boolean // user-toggleable (see Customizations guide) + runtimeStatus: McpServerStatus // discriminated union — see below + channel?: URI // optional mcp:// side-channel + mcpApp?: McpServerCustomizationApps +} +``` + +`enabled` follows the same model as any other container — it's toggled with `session/customizationToggled`. Disabling a server signals the host to stop it; the host then transitions the runtime through `stopped` and removes it from the session (or leaves it as `stopped` until removal, host's choice). + +## Runtime status + +`runtimeStatus` is a [discriminated union on `kind`](/reference/session#mcpserverstatus). It is the host's view of the server's lifecycle, separate from `enabled` (which is the user's intent). + +```mermaid +stateDiagram-v2 + [*] --> starting : server registered + + starting --> ready : connected, initialize completed + starting --> authRequired : 401/403 during connect + starting --> error : startup failed + + ready --> authRequired : token expired / step-up + ready --> error : crashed + ready --> stopped : disabled or removed + + authRequired --> ready : authenticate succeeded + authRequired --> error : auth abandoned / fatal + authRequired --> stopped : disabled + + error --> starting : retry + error --> stopped : removed + + stopped --> [*] +``` + +| Kind | Meaning | +|---|---| +| `starting` | Registered but not yet running. Tools/resources are not available. | +| `ready` | Running and serving requests. Tools/resources surface through the usual channels. | +| `authRequired` | Reachable but blocked on authentication. Carries `ProtectedResourceMetadata` for the client to act on. | +| `error` | Unrecoverable failure. Carries an `ErrorInfo`. Use `authRequired` for auth-specific failures. | +| `stopped` | Shut down. The host MAY remove the entry shortly after. | + +High-frequency lifecycle transitions go through the narrow [`session/mcpServerStatusChanged`](/reference/session#sessionmcpserverstatuschangedaction) action, which upserts just `runtimeStatus` (and optionally `channel`) on an existing entry. Use `session/customizationUpdated` for anything else (name, icons, `mcpApp`). + +## Authentication + +AHP reuses the existing [`authenticate`](/reference/common#authenticate) command for MCP server authentication. The flow is **driven entirely by state** — there is no MCP-specific notification. + +```mermaid +sequenceDiagram + participant Client + participant Host as Agent Host + participant AS as Authorization Server + + Note over Host: MCP server returns 401 with PRM + Host->>Client: customizationUpdated (runtimeStatus: authRequired, resource: PRM) + + Client->>AS: OAuth flow against PRM.authorization_servers + AS-->>Client: Bearer token + + Client->>Host: authenticate({ channel: 'ahp-root://', resource, token }) + Host-->>Client: {} + + Host->>Client: customizationUpdated (runtimeStatus: ready) +``` + +`McpServerStatusAuthRequired` carries: + +- **`reason`** — `required`, `expired`, or `insufficientScope`. Mirrors the [MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization.md) failure modes. +- **`resource`** — [`ProtectedResourceMetadata`](/reference/common#protectedresourcemetadata) per [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728). The `resource` field inside is the canonical MCP server URI (per RFC 8707) and what the client passes back as `authenticate({ resource })`. +- **`requiredScopes`** — Scopes parsed from `WWW-Authenticate: Bearer scope="…"`. Authoritative for the next authorization request; clients MUST NOT assume any relationship to `resource.scopes_supported`. +- **`description`** — Human-readable hint, typically the OAuth `error_description`. + +### Mid-tool-call step-up auth + +`reason: 'insufficientScope'` almost always surfaces *during* a tool call — the model invokes an MCP tool, the upstream server responds 403, and a turn that was happily streaming suddenly needs the user to grant more access. AHP couples this case through two signals so it can't be missed: + +1. **The host SHOULD raise [`SessionStatus.InputNeeded`](/reference/session#sessionstatus) on the session** when it transitions an MCP server to `authRequired` because of an in-flight request. This makes the block visible at the session-summary level, exactly like a tool confirmation or input request. +2. **Clients SHOULD watch the `runtimeStatus.kind` of any MCP server backing a running tool call** (via [`ToolCallContributor`](/reference/session#toolcallcontributor) — `{ kind: 'mcp', customizationId }`). When that server flips to `authRequired`, the client SHOULD present an explicit affordance tied to *that tool call* (e.g. an inline "grant additional access" button), rather than relying on the user to spot a status badge on the server's customization entry. + +The same monitoring pattern also covers `reason: 'expired'` mid-turn — the difference is purely whether the user needs to re-authenticate or grant additional scopes. + +Per-agent protected resources in [`AgentInfo.protectedResources`](/reference/root#agentinfo) cover agents themselves. MCP server resources are advertised here, on the customization, so a single agent can carry an arbitrary number of MCP servers each with their own authorization servers without bloating the root state. + +::: tip +The existing `authenticate` command requires `resource` to match one declared by an agent. Hosts that surface MCP server auth via `McpServerStatusAuthRequired` either need to widen that rule or mirror MCP server resources into `AgentInfo.protectedResources` until the dedicated MCP actions land. This is a known gap and will be tightened when the MCP-specific action surface is specified. +::: + +## Where MCP tools live + +MCP tools follow the normal AHP tool-call flow: + +- The agent harness inside the host discovers tools from each `ready` MCP server, the host normalizes them into the agent's tool catalogue, and exposes invocations through `session/toolCallStart` / `session/toolCallReady` / `session/toolCallComplete`. +- The originating MCP server is identified by [`ToolCallContributor`](/reference/session#toolcallcontributor) on the tool call: `{ kind: 'mcp', customizationId: }`. Clients can use this to render the originating server's name/icon next to the tool call. + +There is no separate "MCP tool" state. From the client's perspective an MCP tool call is just a tool call with an MCP contributor. + +## MCP Apps + +[MCP Apps](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx) (SEP-1865) let an MCP server ship a UI resource — typically an HTML page — that a host renders for a specific tool call. The View runs inside a sandbox controlled by the AHP client and talks back via JSON-RPC over `postMessage`. AHP's role is to give the client everything it needs to render that View on behalf of the agent host — and nothing more. + +This section describes the AHP-level plumbing only. For the View ↔ host protocol itself (the `ui/*` methods, `hostCapabilities`, `hostContext`, sandboxing rules), see the upstream MCP Apps spec. + +### Division of labour + +```mermaid +flowchart LR + Server["MCP Server"] + Harness["Agent Harness"] + Host["Agent Host"] + Client["AHP Client"] + View["MCP App View (iframe)"] + + Server <-- "MCP (full)" --> Harness + Harness <-- "harness-internal" --> Host + Host <-- "AHP" --> Client + Client <-- "ui/* postMessage" --> View + Client <-- "mcp:// (subset of MCP)" --> Host + Host <-- "harness-internal" --> Harness +``` + +- **Agent harness**: holds the underlying MCP connection to the server. How the harness exposes that to the agent host (proxy, callback, message bus, etc.) is harness-defined and outside AHP. +- **Agent host**: normalizes whatever the harness gives it into AHP state. Decides which tool calls instantiate an App. Forwards the constrained subset of MCP traffic the client sends over the [`mcp://` channel](/specification/mcp-channel) into the harness. +- **AHP client**: declares `capabilities.mcpApps` on `initialize`. Renders the View. Runs the `ui/initialize` handshake. Provides `hostContext` (theme, locale, dimensions, etc.) and the locally-decided parts of `hostCapabilities` (`openLinks`, `downloadFile`, `sandbox`, `experimental`). Delivers tool input/result notifications to the View. Routes `tools/*`, `resources/*`, `logging/*`, and `sampling/*` over the `mcp://` channel. +- **View**: an opaque HTML document the client treats as untrusted. AHP says nothing about it directly. + +The client is the *only* party that talks to the View. AHP carries no `ui/*` traffic — that protocol lives between the client and the iframe. + +### Declaring support + +A client opts in by setting [`InitializeParams.capabilities.mcpApps`](/reference/common#initializeparams) to `{}`. Hosts SHOULD only populate `mcpApp` (and expose the corresponding `mcp://` channel) on customizations served to clients that declared the capability. Clients that omit it MUST treat App-bearing tool calls as ordinary MCP tool calls. + +### Discovering App support + +Each [`McpServerCustomization`](/reference/session#mcpservercustomization) MAY advertise App support via `mcpApp`: + +```typescript +McpServerCustomization { + // ... + channel?: URI + mcpApp?: { + capabilities: AhpMcpUiHostCapabilities + } +} +``` + +`mcpApp` SHOULD be present whenever the server can host Apps. Its presence tells the client "if a tool call from this server points at a UI resource, you can render it as an App, and here is the slice of `hostCapabilities` I can satisfy on your behalf". + +[`AhpMcpUiHostCapabilities`](/reference/session#ahpmcpuihostcapabilities) is deliberately a **subset** of the upstream `HostCapabilities`. It only covers capabilities that depend on the host's relationship with the upstream MCP server: + +| AHP capability | What the host promises | What the client passes to `ui/initialize` | +|---|---|---| +| `serverTools` | `tools/list` and `tools/call` will be proxied via the `mcp://` channel. `listChanged` controls whether `notifications/tools/list_changed` is forwarded. | `hostCapabilities.serverTools` (mirroring `listChanged`) | +| `serverResources` | `resources/list`, `resources/templates/list`, and `resources/read` will be proxied. `listChanged` controls notification forwarding. | `hostCapabilities.serverResources` | +| `logging` | `notifications/message` from the App will be forwarded to the server; `logging/setLevel` from the server will reach the App. | `hostCapabilities.logging` (`{}`) | +| `sampling` | `sampling/createMessage` from the App will be handled inside the agent host (typically by the same harness that runs the agent's turns). `sampling.tools` controls SEP-1577 content acceptance. | `hostCapabilities.sampling` | + +The other `hostCapabilities` fields — `openLinks`, `downloadFile`, `sandbox`, `experimental` — depend on the client's renderer (web vs. desktop, what permissions it can grant, what CSP it enforces) and are **not** part of `AhpMcpUiHostCapabilities`. The client fills them in itself before responding to `ui/initialize`. + +### Identifying an App tool call + +A tool call that should render as an App carries two AHP-level signals: + +1. **`contributor`** — `{ kind: 'mcp', customizationId }` points at the originating [`McpServerCustomization`](/reference/session#mcpservercustomization). Look up `mcpApp` and `channel` on that customization. +2. **`_meta.ui`** — the AHP tool call's `_meta` MAY carry a `ui` field mirroring MCP Apps' `McpUiToolMeta` verbatim (typically `{ resourceUri?: string, visibility?: ('model' | 'app')[] }`). The client SHOULD read `resourceUri` to know which UI resource backs the call. AHP does not retype this shape — clients consume it through `_meta`. + +If `_meta.ui.resourceUri` is absent, the tool call is an ordinary MCP tool call and the client renders it normally. + +### End-to-end flow + +```mermaid +sequenceDiagram + participant Server as MCP Server + participant Host as Agent Host + participant Client as AHP Client + participant View + + Note over Host,Client: Session subscription; mcpApp.capabilities visible + + Host->>Client: toolCallStart (contributor.mcp, _meta.ui.resourceUri) + Host->>Client: toolCallReady + + Note over Client: Resolve resource via mcp:// (resources/read) + Client->>Host: mcp:// resources/read (resourceUri) + Host->>Server: resources/read + Server-->>Host: HTML / metadata + Host-->>Client: HTML / metadata + + Note over Client: Mount iframe, await View + View->>Client: ui/initialize (postMessage) + Client-->>View: ui/initialize result
(hostCapabilities = local + mcpApp.capabilities,
hostContext = theme/locale/dimensions) + View->>Client: ui/notifications/initialized + + Note over Client: Pump tool input + result into the View + Client->>View: ui/notifications/tool-input + Host->>Client: toolCallComplete (result) + Client->>View: ui/notifications/tool-result + + Note over View,Server: View now serves a UI; calls into MCP via the client + View->>Client: tools/call (server tool) + Client->>Host: mcp:// tools/call + Host->>Server: tools/call + Server-->>Host: result + Host-->>Client: result + Client-->>View: result +``` + +The client is responsible for translating any locally-supported `ui/*` request into the right local affordance — e.g. `ui/open-link` opens a system link, `ui/request-display-mode` toggles the renderer's layout, `ui/message` becomes a steering message or queued message on the AHP session, `ui/update-model-context` becomes an attachment or `Message._meta` field on the next user message. + +## Next steps + +- [`mcp://` channel](/specification/mcp-channel) — the side-channel clients use to talk to the upstream server. +- [Session Channel Reference](/reference/session) — full type definitions for `McpServerCustomization`, `McpServerStatus`, and friends. diff --git a/docs/specification/mcp-channel.md b/docs/specification/mcp-channel.md new file mode 100644 index 00000000..b623f399 --- /dev/null +++ b/docs/specification/mcp-channel.md @@ -0,0 +1,52 @@ +# The `mcp://` Channel + +The `mcp://` channel is an optional side-channel that lets an AHP client originate a constrained subset of [MCP](https://modelcontextprotocol.io/) traffic against an MCP server the agent host is already running. It is the wire format AHP uses whenever a client needs to talk MCP — but only as much MCP as the host has explicitly opted into exposing. + +The channel itself is generic. The set of methods and notifications it actually serves is determined entirely by capability advertisements on the customization it hangs off. Today the only such advertisement is [`AhpMcpUiHostCapabilities`](/reference/session#ahpmcpuihostcapabilities) (used by [MCP Apps](/guide/mcp#mcp-apps)), but additional domain-specific capability sets MAY be added in the future without changing the channel itself. + +## Wire format + +The channel speaks [MCP](https://modelcontextprotocol.io/specification) verbatim — JSON-RPC 2.0 requests, responses, and notifications exactly as defined by the upstream MCP specification. AHP does not redefine the request/response shapes or notification payloads; consult MCP for those. + +The only AHP-level addition is the routing envelope shared by every AHP message: each request, response, and notification carries a top-level `channel: URI` whose value is the [channel URI](#channel-uri) exposed on the owning customization. The receiver routes the message by `channel` exactly the same way it routes any other AHP traffic — no per-method dispatch logic is needed. + +Because the channel piggybacks on the existing AHP transport rather than opening a fresh MCP connection to the server, by the time a client opens the channel the upstream server is already past MCP `initialize` (or it isn't `ready` and the channel is unavailable); the client is joining an in-flight session. As a consequence: + +- The MCP `initialize` / `initialized` handshake is **not** carried over the channel. +- Methods that are state-bearing in MCP and already mirrored by AHP (e.g. tool execution lifecycle, session state) are **not** served over the channel — clients use the corresponding AHP actions and state. +- Only the methods explicitly enabled by a capability advertisement on the channel's owning customization are served. Everything else MUST be rejected by the host. + +The host serves the channel; the client originates traffic on it. + +## Negotiating the served surface + +The set of methods a client may send (and the set of notifications the host promises to forward) is the **union** of every capability advertisement attached to the channel's owning customization. Each capability set covers a specific feature area; a server that needs more surface area than one capability set provides advertises additional ones. + +Currently the only defined capability set is [`AhpMcpUiHostCapabilities`](/reference/session#ahpmcpuihostcapabilities), which covers what MCP Apps need: + +| Capability flag | Methods served (Client → Host → Server) | Notifications forwarded (Server → Host → Client) | +|---|---|---| +| `serverTools` | `tools/list`, `tools/call` | `notifications/tools/list_changed` *(when `listChanged: true`)* | +| `serverResources` | `resources/list`, `resources/templates/list`, `resources/read` | `notifications/resources/list_changed` *(when `listChanged: true`)* | +| `logging` | `logging/setLevel`, `notifications/message` | — | +| `sampling` | `sampling/createMessage` | — | + +A method outside every advertised capability set MUST be rejected by the host with JSON-RPC `-32601` *Method not found*. Clients SHOULD NOT speculate beyond the advertisement — capability sets are the only source of truth for what's served. + +How the host satisfies a served method (proxying it upstream to the MCP server, handling it inside the agent harness, or some mixture) is an implementation detail and not specified here. The advertisement guarantees the method is **served**, not how it's served. + +## Channel URI + +The channel URI is exposed on the customization that owns it. Today that means [`McpServerCustomization.channel`](/reference/session#mcpservercustomization); future customizations that warrant a side-channel MAY follow the same pattern. + +The URI itself is opaque to the client. Its scheme is `mcp://`; its path and authority are host-defined. + +- There is at most one channel per customization within a session. +- The host MAY only expose `channel` while the owning customization is in a usable runtime state (for `McpServerCustomization` that's `runtimeStatus.kind === 'ready'`). When that condition no longer holds, the host MAY clear `channel` via the customization's update action. Clients SHOULD treat the channel as unavailable while it is absent. +- The URI SHOULD be stable across the customization's lifetime, but the host MAY change it (for example after a restart). Clients MUST re-read `channel` whenever the customization is updated. +- The channel is only present when the owning customization advertises at least one capability set requiring it. Customizations without such an advertisement do not need a side-channel — their state is already covered by AHP's normal flows. + +## Next steps + +- [MCP Servers](/guide/mcp) — how the customization the channel hangs off works. +- [Session Channel Reference](/reference/session#ahpmcpuihostcapabilities) — type definition for `AhpMcpUiHostCapabilities` (the first capability set served on this channel). diff --git a/schema/actions.schema.json b/schema/actions.schema.json index 68d7719d..7eb4c8f3 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -257,7 +257,7 @@ }, "SessionToolCallStartAction": { "type": "object", - "description": "A tool call begins — parameters are streaming from the LM.\n\nFor client-provided tools, the server sets `toolClientId` to identify the\nowning client. That client is responsible for executing the tool once it\nreaches the `running` state and dispatching `session/toolCallComplete`.", + "description": "A tool call begins — parameters are streaming from the LM.\n\nThe server sets {@link ToolCallContributor | `contributor`} to identify\nthe origin of the tool. For client-provided tools, the named client is\nresponsible for executing the tool once it reaches the `running` state\nand dispatching `session/toolCallComplete`. For MCP-served tools, the\nserver executes the call against the named `McpServerCustomization`.", "properties": { "turnId": { "type": "string", @@ -283,9 +283,9 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called. Absent for\nserver-side tools that are not contributed by a client or MCP server." } }, "required": [ @@ -965,6 +965,32 @@ "id" ] }, + "SessionMcpServerStatusChangedAction": { + "type": "object", + "description": "Updates the runtime fields of an existing\n{@link McpServerCustomization} — narrow alternative to\n{@link SessionCustomizationUpdatedAction} for the high-frequency\n`starting` ↔ `ready` ↔ `authRequired` transitions.\n\nLocates the target entry by `id`, searching both the top-level\ncustomization list and the `children` array of every container.\nReplaces the entry's {@link McpServerCustomization.runtimeStatus | `runtimeStatus`}\nand {@link McpServerCustomization.channel | `channel`}\n(full-replacement semantics: omit `channel` to clear an existing\nchannel URI). Other fields of the customization are preserved.\n\nIs a no-op when no matching `McpServerCustomization` is found. To\nupdate any other field (name, icons, `mcpApp` capabilities, etc.) use\n{@link SessionCustomizationUpdatedAction} instead.\n\nWhen the transition is to {@link McpServerStatusKind.AuthRequired}\nbecause of a request issued mid-turn, the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session — see\n{@link McpServerStatusAuthRequired} for the rationale.", + "properties": { + "type": { + "$ref": "#/$defs/ActionType.SessionMcpServerStatusChanged" + }, + "id": { + "type": "string", + "description": "The id of the {@link McpServerCustomization} to update." + }, + "runtimeStatus": { + "$ref": "#/$defs/McpServerStatus", + "description": "The new runtime status." + }, + "channel": { + "$ref": "#/$defs/URI", + "description": "Updated `mcp://` side-channel URI. Full-replacement: omit to clear\nan existing channel (typical when leaving\n{@link McpServerStatusKind.Ready | `Ready`})." + } + }, + "required": [ + "type", + "id", + "runtimeStatus" + ] + }, "SessionConfigChangedAction": { "type": "object", "description": "Client changed a mutable config value mid-session.\n\nOnly properties with `sessionMutable: true` in the config schema may be\nchanged. The server validates and broadcasts the action; the reducer merges\nthe new values into `state.config.values`.", @@ -1970,7 +1996,7 @@ "items": { "$ref": "#/$defs/Customization" }, - "description": "Customizations associated with this agent.\n\nAlways container customizations —\n{@link PluginCustomization | `PluginCustomization`} entries the agent\nbundles, plus {@link DirectoryCustomization | `DirectoryCustomization`}\nentries it watches in any workspace it's used with. When a session is\ncreated with this agent, these entries are augmented (e.g. directory\nURIs are resolved against the workspace, children are parsed) and\npropagated into the session's `customizations` list." + "description": "Customizations associated with this agent.\n\nEither container customizations —\n{@link PluginCustomization | `PluginCustomization`} entries the agent\nbundles, plus {@link DirectoryCustomization | `DirectoryCustomization`}\nentries it watches in any workspace it's used with — or top-level\n{@link McpServerCustomization | `McpServerCustomization`} entries\nthe agent host declares directly. When a session is created with\nthis agent, these entries are augmented (e.g. directory URIs are\nresolved against the workspace, children are parsed) and propagated\ninto the session's `customizations` list." } }, "required": [ @@ -2145,7 +2171,7 @@ "items": { "$ref": "#/$defs/Customization" }, - "description": "Top-level customizations active in this session.\n\nAlways container customizations — {@link PluginCustomization} or\n{@link DirectoryCustomization}. Children (agents, skills, prompts,\nrules, hooks, MCP servers) live in each container's\n{@link ContainerCustomizationBase.children | `children`} array.\n\nClient-published plugins arrive via\n{@link SessionActiveClient.customizations | `activeClient.customizations`}\nand the host propagates them into this list (typically with the\ncontainer's `clientId` set and `children` populated)." + "description": "Top-level customizations active in this session.\n\nAlways one of the {@link Customization} variants:\n\n- Container customizations ({@link PluginCustomization},\n {@link DirectoryCustomization}) whose children — agents, skills,\n prompts, rules, hooks, MCP servers — live in each container's\n {@link ContainerCustomizationBase.children | `children`} array.\n- Top-level {@link McpServerCustomization} entries the host\n surfaces directly (for example a globally-configured MCP server\n that isn't bundled in a plugin or directory). MCP servers may\n also appear as children of a container.\n\nClient-published plugins arrive via\n{@link SessionActiveClient.customizations | `activeClient.customizations`}\nand the host propagates them into this list (typically with the\ncontainer's `clientId` set and `children` populated). Clients\npublish in container shape only; bare MCP servers at the top level\nare server-originated." }, "_meta": { "type": "object", @@ -3259,6 +3285,38 @@ "kind" ] }, + "ToolCallClientContributor": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallContributorKind.Client" + }, + "clientId": { + "type": "string", + "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + } + }, + "required": [ + "kind", + "clientId" + ] + }, + "ToolCallMcpContributor": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallContributorKind.MCP" + }, + "customizationId": { + "type": "string", + "description": "Customization ID of the corresponding MCP server in {@link SessionState.customizations}." + } + }, + "required": [ + "kind", + "customizationId" + ] + }, "ToolCallBase": { "type": "object", "description": "Metadata common to all tool call states.", @@ -3275,14 +3333,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." } }, "required": [ @@ -3369,14 +3427,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, "status": { "$ref": "#/$defs/ToolCallStatus.Streaming" @@ -3413,14 +3471,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", @@ -3485,14 +3543,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", @@ -3546,14 +3604,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", @@ -3637,14 +3695,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", @@ -3728,14 +3786,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", @@ -4595,7 +4653,7 @@ }, "McpServerCustomization": { "type": "object", - "description": "An MCP manifest contributed by a plugin or directory.\n\nWhen the server is declared inline in the containing plugin manifest,\n`uri` points at the manifest file and\n{@link CustomizationBase.range | `range`} narrows it to the\ndeclaration's span.", + "description": "An MCP server contributed by a plugin or directory.\n\nWhen the server is declared inline in the containing plugin manifest,\n`uri` points at the manifest file and\n{@link CustomizationBase.range | `range`} narrows it to the\ndeclaration's span.\n\nThe MCP server customization also reflects its current status.", "properties": { "id": { "type": "string", @@ -4622,13 +4680,168 @@ }, "type": { "$ref": "#/$defs/CustomizationType.McpServer" + }, + "enabled": { + "type": "boolean", + "description": "Whether this MCP server is currently enabled." + }, + "runtimeStatus": { + "$ref": "#/$defs/McpServerStatus", + "description": "Current status of the MCP server." + }, + "channel": { + "$ref": "#/$defs/URI", + "description": "An `mcp://`-protocol channel the client uses to side-channel traffic\ninto the upstream MCP server itself. The channel is NOT a fresh raw MCP\nconnection: it piggybacks on the AHP transport\nand skips the MCP `initialize` sequence.\n\nThe agent host MAY only serve a subset of MCP on this\nchannel; the served subset is described by domain-specific\ncapabilities such as those in\n{@link McpServerCustomizationApps.capabilities}.\n\nThe channel URI SHOULD be stable across the server's lifetime, but\nthe agent host MAY change it (for example across a restart) and\nMAY only expose it while the server is in\n{@link McpServerStatusKind.Ready | `Ready`}. Absence means no\nside-channel is currently available." + }, + "mcpApp": { + "$ref": "#/$defs/McpServerCustomizationApps", + "description": "MCP App support. This property SHOULD be advertised for MCP servers\nwhich support apps." } }, "required": [ "id", "uri", "name", - "type" + "type", + "enabled", + "runtimeStatus" + ] + }, + "McpServerCustomizationApps": { + "type": "object", + "description": "Information from the agent host needed to render MCP Apps served\nby this MCP server.", + "properties": { + "capabilities": { + "$ref": "#/$defs/AhpMcpUiHostCapabilities", + "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nthe AHP host can satisfy for Views backed by this server. The\nclient feeds these straight through into the `hostCapabilities` of\nthe `ui/initialize` response delivered to the View." + } + }, + "required": [ + "capabilities" + ] + }, + "AhpMcpUiHostCapabilities": { + "type": "object", + "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nan AHP host can derive from the upstream MCP server (and from AHP's own\nforwarding plumbing). Advertised on\n{@link McpServerCustomizationApps.capabilities} so clients can pass it\nthrough into the `hostCapabilities` of the `ui/initialize` response\ndelivered to an MCP App View.\n\nField names mirror the MCP Apps spec exactly, so the AHP-side producer\ncan pass them straight through into the `hostCapabilities` of the\n`ui/initialize` response delivered to the View.\n\nCapabilities outside this set (`openLinks`, `downloadFile`, `sandbox`,\n`experimental`) are decided locally by whichever AHP client renders the\nView and are NOT part of this AHP-level advertisement — only the\nserver-derived subset is.\n\nAn agent host MUST only advertise a capability when it actually accepts the\ncorresponding methods/notifications on the `mcp://` channel:\n\n- {@link serverTools}: host proxies `tools/list` and `tools/call` to\n the MCP server. When `listChanged` is `true`, the host also forwards\n `notifications/tools/list_changed`.\n- {@link serverResources}: host proxies `resources/read`,\n `resources/list`, and `resources/templates/list` to the MCP server.\n When `listChanged` is `true`, the host also forwards\n `notifications/resources/list_changed`.\n- {@link logging}: host accepts `notifications/message` log entries\n from the App and forwards them via `mcpNotification` (and forwards\n `logging/setLevel` calls to the server).\n- {@link sampling}: host serves `sampling/createMessage` via\n `mcpMethodCall`. When `sampling.tools` is present, the host also\n accepts SEP-1577 `tools` / `toolChoice` / `tool_use` content blocks\n inside `CreateMessageRequest`.", + "properties": { + "serverTools": { + "type": "object", + "properties": { + "listChanged": { + "type": "boolean" + } + }, + "description": "Producer proxies the MCP `tools/*` methods to the upstream server." + }, + "serverResources": { + "type": "object", + "properties": { + "listChanged": { + "type": "boolean" + } + }, + "description": "Producer proxies the MCP `resources/*` methods to the upstream server." + }, + "logging": { + "type": "object", + "additionalProperties": {}, + "description": "Producer accepts `notifications/message` log entries from the App via `mcpNotification`." + }, + "sampling": { + "type": "object", + "properties": { + "tools": { + "type": "string" + } + }, + "description": "Producer serves `sampling/createMessage` via `mcpMethodCall`." + } + } + }, + "McpServerStatusStarting": { + "type": "object", + "description": "Server is registered with the host but has not yet started.", + "properties": { + "kind": { + "$ref": "#/$defs/McpServerStatusKind.Starting" + } + }, + "required": [ + "kind" + ] + }, + "McpServerStatusReady": { + "type": "object", + "description": "Server is running and serving requests.", + "properties": { + "kind": { + "$ref": "#/$defs/McpServerStatusKind.Ready" + } + }, + "required": [ + "kind" + ] + }, + "McpServerStatusAuthRequired": { + "type": "object", + "description": "Server is reachable but cannot serve requests until the client\nauthenticates. Mirrors the discovery flow defined by\n[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)\n(Protected Resource Metadata) and the OAuth 2.1 / RFC 6750 challenge\nsemantics required by the MCP authorization spec.\n\nClients react to this state by calling the existing `authenticate`\ncommand with the {@link ProtectedResourceMetadata.resource | resource}\ncarried here. There is **no** `notify/authRequired` notification for\nMCP servers — the action stream is the single source of truth.\n\nWhen the transition is triggered by a request issued during a turn\n— most commonly\n{@link McpAuthRequiredReason.InsufficientScope | `InsufficientScope`}\nsurfacing mid-tool-call — the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session so the block is\nvisible at the summary level. Clients SHOULD watch this status on\nany MCP server backing a running tool call and surface an explicit\naffordance (e.g. a \"grant additional access\" prompt) tied to that\ntool call, rather than relying on the user to notice the\ncustomization’s status badge.", + "properties": { + "kind": { + "$ref": "#/$defs/McpServerStatusKind.AuthRequired" + }, + "reason": { + "$ref": "#/$defs/McpAuthRequiredReason", + "description": "Why authentication is required." + }, + "resource": { + "$ref": "#/$defs/ProtectedResourceMetadata", + "description": "RFC 9728 Protected Resource Metadata. The `resource` field is the\ncanonical MCP server URI per RFC 8707, used as the OAuth `resource`\nindicator. `authorization_servers` is REQUIRED by the MCP\nauthorization spec." + }, + "requiredScopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Scopes required for the current challenge, parsed from the\n`WWW-Authenticate: Bearer scope=\"…\"` header (or `scopes_supported`\nfallback). Authoritative for the next authorization request — clients\nMUST NOT assume any subset/superset relationship to\n`resource.scopes_supported`." + }, + "description": { + "type": "string", + "description": "Human-readable hint, typically from the OAuth `error_description`." + } + }, + "required": [ + "kind", + "reason", + "resource" + ] + }, + "McpServerStatusError": { + "type": "object", + "description": "Server failed to start, crashed, or otherwise transitioned to a\nnon-recoverable error. Use {@link McpServerStatusKind.AuthRequired}\nfor authentication failures.", + "properties": { + "kind": { + "$ref": "#/$defs/McpServerStatusKind.Error" + }, + "error": { + "$ref": "#/$defs/ErrorInfo", + "description": "Error details." + } + }, + "required": [ + "kind", + "error" + ] + }, + "McpServerStatusStopped": { + "type": "object", + "description": "Server has been shut down. The host MAY remove the server from the\nsession entirely shortly after this state.", + "properties": { + "kind": { + "$ref": "#/$defs/McpServerStatusKind.Stopped" + } + }, + "required": [ + "kind" ] }, "TerminalInfo": { @@ -5125,6 +5338,16 @@ } ] }, + "ToolCallContributor": { + "oneOf": [ + { + "$ref": "#/$defs/ToolCallClientContributor" + }, + { + "$ref": "#/$defs/ToolCallMcpContributor" + } + ] + }, "ToolCallState": { "oneOf": [ {}, @@ -5241,14 +5464,39 @@ }, "Customization": { "oneOf": [ + {}, { "$ref": "#/$defs/PluginCustomization" }, { "$ref": "#/$defs/DirectoryCustomization" + }, + { + "$ref": "#/$defs/McpServerCustomization" + } + ], + "description": "A top-level customization active in a session. Either a container\n({@link PluginCustomization} or {@link DirectoryCustomization}) whose\nleaf customizations live in its\n{@link ContainerCustomizationBase.children | `children`} array, or a\nbare {@link McpServerCustomization} surfaced directly by the host." + }, + "McpServerStatus": { + "oneOf": [ + {}, + { + "$ref": "#/$defs/McpServerStatusStarting" + }, + { + "$ref": "#/$defs/McpServerStatusReady" + }, + { + "$ref": "#/$defs/McpServerStatusAuthRequired" + }, + { + "$ref": "#/$defs/McpServerStatusError" + }, + { + "$ref": "#/$defs/McpServerStatusStopped" } ], - "description": "A top-level customization active in a session. Always a container\n({@link PluginCustomization} or {@link DirectoryCustomization}); the\nremaining customization types appear inside the container's\n{@link ContainerCustomizationBase.children | `children`} array." + "description": "Discriminated union of all MCP server statuses. Discriminated by `kind`." }, "TerminalClaim": { "oneOf": [ @@ -5390,6 +5638,9 @@ { "$ref": "#/$defs/SessionCustomizationRemovedAction" }, + { + "$ref": "#/$defs/SessionMcpServerStatusChangedAction" + }, { "$ref": "#/$defs/SessionTruncatedAction" }, diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 8a916568..71f221b2 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -1647,7 +1647,7 @@ "items": { "$ref": "#/$defs/Customization" }, - "description": "Customizations associated with this agent.\n\nAlways container customizations —\n{@link PluginCustomization | `PluginCustomization`} entries the agent\nbundles, plus {@link DirectoryCustomization | `DirectoryCustomization`}\nentries it watches in any workspace it's used with. When a session is\ncreated with this agent, these entries are augmented (e.g. directory\nURIs are resolved against the workspace, children are parsed) and\npropagated into the session's `customizations` list." + "description": "Customizations associated with this agent.\n\nEither container customizations —\n{@link PluginCustomization | `PluginCustomization`} entries the agent\nbundles, plus {@link DirectoryCustomization | `DirectoryCustomization`}\nentries it watches in any workspace it's used with — or top-level\n{@link McpServerCustomization | `McpServerCustomization`} entries\nthe agent host declares directly. When a session is created with\nthis agent, these entries are augmented (e.g. directory URIs are\nresolved against the workspace, children are parsed) and propagated\ninto the session's `customizations` list." } }, "required": [ @@ -1822,7 +1822,7 @@ "items": { "$ref": "#/$defs/Customization" }, - "description": "Top-level customizations active in this session.\n\nAlways container customizations — {@link PluginCustomization} or\n{@link DirectoryCustomization}. Children (agents, skills, prompts,\nrules, hooks, MCP servers) live in each container's\n{@link ContainerCustomizationBase.children | `children`} array.\n\nClient-published plugins arrive via\n{@link SessionActiveClient.customizations | `activeClient.customizations`}\nand the host propagates them into this list (typically with the\ncontainer's `clientId` set and `children` populated)." + "description": "Top-level customizations active in this session.\n\nAlways one of the {@link Customization} variants:\n\n- Container customizations ({@link PluginCustomization},\n {@link DirectoryCustomization}) whose children — agents, skills,\n prompts, rules, hooks, MCP servers — live in each container's\n {@link ContainerCustomizationBase.children | `children`} array.\n- Top-level {@link McpServerCustomization} entries the host\n surfaces directly (for example a globally-configured MCP server\n that isn't bundled in a plugin or directory). MCP servers may\n also appear as children of a container.\n\nClient-published plugins arrive via\n{@link SessionActiveClient.customizations | `activeClient.customizations`}\nand the host propagates them into this list (typically with the\ncontainer's `clientId` set and `children` populated). Clients\npublish in container shape only; bare MCP servers at the top level\nare server-originated." }, "_meta": { "type": "object", @@ -2936,6 +2936,38 @@ "kind" ] }, + "ToolCallClientContributor": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallContributorKind.Client" + }, + "clientId": { + "type": "string", + "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + } + }, + "required": [ + "kind", + "clientId" + ] + }, + "ToolCallMcpContributor": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallContributorKind.MCP" + }, + "customizationId": { + "type": "string", + "description": "Customization ID of the corresponding MCP server in {@link SessionState.customizations}." + } + }, + "required": [ + "kind", + "customizationId" + ] + }, "ToolCallBase": { "type": "object", "description": "Metadata common to all tool call states.", @@ -2952,14 +2984,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." } }, "required": [ @@ -3046,14 +3078,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, "status": { "$ref": "#/$defs/ToolCallStatus.Streaming" @@ -3090,14 +3122,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", @@ -3162,14 +3194,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", @@ -3223,14 +3255,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", @@ -3314,14 +3346,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", @@ -3405,14 +3437,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", @@ -4272,7 +4304,7 @@ }, "McpServerCustomization": { "type": "object", - "description": "An MCP manifest contributed by a plugin or directory.\n\nWhen the server is declared inline in the containing plugin manifest,\n`uri` points at the manifest file and\n{@link CustomizationBase.range | `range`} narrows it to the\ndeclaration's span.", + "description": "An MCP server contributed by a plugin or directory.\n\nWhen the server is declared inline in the containing plugin manifest,\n`uri` points at the manifest file and\n{@link CustomizationBase.range | `range`} narrows it to the\ndeclaration's span.\n\nThe MCP server customization also reflects its current status.", "properties": { "id": { "type": "string", @@ -4299,13 +4331,168 @@ }, "type": { "$ref": "#/$defs/CustomizationType.McpServer" + }, + "enabled": { + "type": "boolean", + "description": "Whether this MCP server is currently enabled." + }, + "runtimeStatus": { + "$ref": "#/$defs/McpServerStatus", + "description": "Current status of the MCP server." + }, + "channel": { + "$ref": "#/$defs/URI", + "description": "An `mcp://`-protocol channel the client uses to side-channel traffic\ninto the upstream MCP server itself. The channel is NOT a fresh raw MCP\nconnection: it piggybacks on the AHP transport\nand skips the MCP `initialize` sequence.\n\nThe agent host MAY only serve a subset of MCP on this\nchannel; the served subset is described by domain-specific\ncapabilities such as those in\n{@link McpServerCustomizationApps.capabilities}.\n\nThe channel URI SHOULD be stable across the server's lifetime, but\nthe agent host MAY change it (for example across a restart) and\nMAY only expose it while the server is in\n{@link McpServerStatusKind.Ready | `Ready`}. Absence means no\nside-channel is currently available." + }, + "mcpApp": { + "$ref": "#/$defs/McpServerCustomizationApps", + "description": "MCP App support. This property SHOULD be advertised for MCP servers\nwhich support apps." } }, "required": [ "id", "uri", "name", - "type" + "type", + "enabled", + "runtimeStatus" + ] + }, + "McpServerCustomizationApps": { + "type": "object", + "description": "Information from the agent host needed to render MCP Apps served\nby this MCP server.", + "properties": { + "capabilities": { + "$ref": "#/$defs/AhpMcpUiHostCapabilities", + "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nthe AHP host can satisfy for Views backed by this server. The\nclient feeds these straight through into the `hostCapabilities` of\nthe `ui/initialize` response delivered to the View." + } + }, + "required": [ + "capabilities" + ] + }, + "AhpMcpUiHostCapabilities": { + "type": "object", + "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nan AHP host can derive from the upstream MCP server (and from AHP's own\nforwarding plumbing). Advertised on\n{@link McpServerCustomizationApps.capabilities} so clients can pass it\nthrough into the `hostCapabilities` of the `ui/initialize` response\ndelivered to an MCP App View.\n\nField names mirror the MCP Apps spec exactly, so the AHP-side producer\ncan pass them straight through into the `hostCapabilities` of the\n`ui/initialize` response delivered to the View.\n\nCapabilities outside this set (`openLinks`, `downloadFile`, `sandbox`,\n`experimental`) are decided locally by whichever AHP client renders the\nView and are NOT part of this AHP-level advertisement — only the\nserver-derived subset is.\n\nAn agent host MUST only advertise a capability when it actually accepts the\ncorresponding methods/notifications on the `mcp://` channel:\n\n- {@link serverTools}: host proxies `tools/list` and `tools/call` to\n the MCP server. When `listChanged` is `true`, the host also forwards\n `notifications/tools/list_changed`.\n- {@link serverResources}: host proxies `resources/read`,\n `resources/list`, and `resources/templates/list` to the MCP server.\n When `listChanged` is `true`, the host also forwards\n `notifications/resources/list_changed`.\n- {@link logging}: host accepts `notifications/message` log entries\n from the App and forwards them via `mcpNotification` (and forwards\n `logging/setLevel` calls to the server).\n- {@link sampling}: host serves `sampling/createMessage` via\n `mcpMethodCall`. When `sampling.tools` is present, the host also\n accepts SEP-1577 `tools` / `toolChoice` / `tool_use` content blocks\n inside `CreateMessageRequest`.", + "properties": { + "serverTools": { + "type": "object", + "properties": { + "listChanged": { + "type": "boolean" + } + }, + "description": "Producer proxies the MCP `tools/*` methods to the upstream server." + }, + "serverResources": { + "type": "object", + "properties": { + "listChanged": { + "type": "boolean" + } + }, + "description": "Producer proxies the MCP `resources/*` methods to the upstream server." + }, + "logging": { + "type": "object", + "additionalProperties": {}, + "description": "Producer accepts `notifications/message` log entries from the App via `mcpNotification`." + }, + "sampling": { + "type": "object", + "properties": { + "tools": { + "type": "string" + } + }, + "description": "Producer serves `sampling/createMessage` via `mcpMethodCall`." + } + } + }, + "McpServerStatusStarting": { + "type": "object", + "description": "Server is registered with the host but has not yet started.", + "properties": { + "kind": { + "$ref": "#/$defs/McpServerStatusKind.Starting" + } + }, + "required": [ + "kind" + ] + }, + "McpServerStatusReady": { + "type": "object", + "description": "Server is running and serving requests.", + "properties": { + "kind": { + "$ref": "#/$defs/McpServerStatusKind.Ready" + } + }, + "required": [ + "kind" + ] + }, + "McpServerStatusAuthRequired": { + "type": "object", + "description": "Server is reachable but cannot serve requests until the client\nauthenticates. Mirrors the discovery flow defined by\n[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)\n(Protected Resource Metadata) and the OAuth 2.1 / RFC 6750 challenge\nsemantics required by the MCP authorization spec.\n\nClients react to this state by calling the existing `authenticate`\ncommand with the {@link ProtectedResourceMetadata.resource | resource}\ncarried here. There is **no** `notify/authRequired` notification for\nMCP servers — the action stream is the single source of truth.\n\nWhen the transition is triggered by a request issued during a turn\n— most commonly\n{@link McpAuthRequiredReason.InsufficientScope | `InsufficientScope`}\nsurfacing mid-tool-call — the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session so the block is\nvisible at the summary level. Clients SHOULD watch this status on\nany MCP server backing a running tool call and surface an explicit\naffordance (e.g. a \"grant additional access\" prompt) tied to that\ntool call, rather than relying on the user to notice the\ncustomization’s status badge.", + "properties": { + "kind": { + "$ref": "#/$defs/McpServerStatusKind.AuthRequired" + }, + "reason": { + "$ref": "#/$defs/McpAuthRequiredReason", + "description": "Why authentication is required." + }, + "resource": { + "$ref": "#/$defs/ProtectedResourceMetadata", + "description": "RFC 9728 Protected Resource Metadata. The `resource` field is the\ncanonical MCP server URI per RFC 8707, used as the OAuth `resource`\nindicator. `authorization_servers` is REQUIRED by the MCP\nauthorization spec." + }, + "requiredScopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Scopes required for the current challenge, parsed from the\n`WWW-Authenticate: Bearer scope=\"…\"` header (or `scopes_supported`\nfallback). Authoritative for the next authorization request — clients\nMUST NOT assume any subset/superset relationship to\n`resource.scopes_supported`." + }, + "description": { + "type": "string", + "description": "Human-readable hint, typically from the OAuth `error_description`." + } + }, + "required": [ + "kind", + "reason", + "resource" + ] + }, + "McpServerStatusError": { + "type": "object", + "description": "Server failed to start, crashed, or otherwise transitioned to a\nnon-recoverable error. Use {@link McpServerStatusKind.AuthRequired}\nfor authentication failures.", + "properties": { + "kind": { + "$ref": "#/$defs/McpServerStatusKind.Error" + }, + "error": { + "$ref": "#/$defs/ErrorInfo", + "description": "Error details." + } + }, + "required": [ + "kind", + "error" + ] + }, + "McpServerStatusStopped": { + "type": "object", + "description": "Server has been shut down. The host MAY remove the server from the\nsession entirely shortly after this state.", + "properties": { + "kind": { + "$ref": "#/$defs/McpServerStatusKind.Stopped" + } + }, + "required": [ + "kind" ] }, "TerminalInfo": { @@ -4950,7 +5137,7 @@ }, "SessionToolCallStartAction": { "type": "object", - "description": "A tool call begins — parameters are streaming from the LM.\n\nFor client-provided tools, the server sets `toolClientId` to identify the\nowning client. That client is responsible for executing the tool once it\nreaches the `running` state and dispatching `session/toolCallComplete`.", + "description": "A tool call begins — parameters are streaming from the LM.\n\nThe server sets {@link ToolCallContributor | `contributor`} to identify\nthe origin of the tool. For client-provided tools, the named client is\nresponsible for executing the tool once it reaches the `running` state\nand dispatching `session/toolCallComplete`. For MCP-served tools, the\nserver executes the call against the named `McpServerCustomization`.", "properties": { "turnId": { "type": "string", @@ -4976,9 +5163,9 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called. Absent for\nserver-side tools that are not contributed by a client or MCP server." } }, "required": [ @@ -5658,6 +5845,32 @@ "id" ] }, + "SessionMcpServerStatusChangedAction": { + "type": "object", + "description": "Updates the runtime fields of an existing\n{@link McpServerCustomization} — narrow alternative to\n{@link SessionCustomizationUpdatedAction} for the high-frequency\n`starting` ↔ `ready` ↔ `authRequired` transitions.\n\nLocates the target entry by `id`, searching both the top-level\ncustomization list and the `children` array of every container.\nReplaces the entry's {@link McpServerCustomization.runtimeStatus | `runtimeStatus`}\nand {@link McpServerCustomization.channel | `channel`}\n(full-replacement semantics: omit `channel` to clear an existing\nchannel URI). Other fields of the customization are preserved.\n\nIs a no-op when no matching `McpServerCustomization` is found. To\nupdate any other field (name, icons, `mcpApp` capabilities, etc.) use\n{@link SessionCustomizationUpdatedAction} instead.\n\nWhen the transition is to {@link McpServerStatusKind.AuthRequired}\nbecause of a request issued mid-turn, the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session — see\n{@link McpServerStatusAuthRequired} for the rationale.", + "properties": { + "type": { + "$ref": "#/$defs/ActionType.SessionMcpServerStatusChanged" + }, + "id": { + "type": "string", + "description": "The id of the {@link McpServerCustomization} to update." + }, + "runtimeStatus": { + "$ref": "#/$defs/McpServerStatus", + "description": "The new runtime status." + }, + "channel": { + "$ref": "#/$defs/URI", + "description": "Updated `mcp://` side-channel URI. Full-replacement: omit to clear\nan existing channel (typical when leaving\n{@link McpServerStatusKind.Ready | `Ready`})." + } + }, + "required": [ + "type", + "id", + "runtimeStatus" + ] + }, "SessionConfigChangedAction": { "type": "object", "description": "Client changed a mutable config value mid-session.\n\nOnly properties with `sessionMutable: true` in the config schema may be\nchanged. The server validates and broadcasts the action; the reducer merges\nthe new values into `state.config.values`.", diff --git a/schema/errors.schema.json b/schema/errors.schema.json index 6a537bb3..cb959205 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -579,7 +579,7 @@ "items": { "$ref": "#/$defs/Customization" }, - "description": "Customizations associated with this agent.\n\nAlways container customizations —\n{@link PluginCustomization | `PluginCustomization`} entries the agent\nbundles, plus {@link DirectoryCustomization | `DirectoryCustomization`}\nentries it watches in any workspace it's used with. When a session is\ncreated with this agent, these entries are augmented (e.g. directory\nURIs are resolved against the workspace, children are parsed) and\npropagated into the session's `customizations` list." + "description": "Customizations associated with this agent.\n\nEither container customizations —\n{@link PluginCustomization | `PluginCustomization`} entries the agent\nbundles, plus {@link DirectoryCustomization | `DirectoryCustomization`}\nentries it watches in any workspace it's used with — or top-level\n{@link McpServerCustomization | `McpServerCustomization`} entries\nthe agent host declares directly. When a session is created with\nthis agent, these entries are augmented (e.g. directory URIs are\nresolved against the workspace, children are parsed) and propagated\ninto the session's `customizations` list." } }, "required": [ @@ -754,7 +754,7 @@ "items": { "$ref": "#/$defs/Customization" }, - "description": "Top-level customizations active in this session.\n\nAlways container customizations — {@link PluginCustomization} or\n{@link DirectoryCustomization}. Children (agents, skills, prompts,\nrules, hooks, MCP servers) live in each container's\n{@link ContainerCustomizationBase.children | `children`} array.\n\nClient-published plugins arrive via\n{@link SessionActiveClient.customizations | `activeClient.customizations`}\nand the host propagates them into this list (typically with the\ncontainer's `clientId` set and `children` populated)." + "description": "Top-level customizations active in this session.\n\nAlways one of the {@link Customization} variants:\n\n- Container customizations ({@link PluginCustomization},\n {@link DirectoryCustomization}) whose children — agents, skills,\n prompts, rules, hooks, MCP servers — live in each container's\n {@link ContainerCustomizationBase.children | `children`} array.\n- Top-level {@link McpServerCustomization} entries the host\n surfaces directly (for example a globally-configured MCP server\n that isn't bundled in a plugin or directory). MCP servers may\n also appear as children of a container.\n\nClient-published plugins arrive via\n{@link SessionActiveClient.customizations | `activeClient.customizations`}\nand the host propagates them into this list (typically with the\ncontainer's `clientId` set and `children` populated). Clients\npublish in container shape only; bare MCP servers at the top level\nare server-originated." }, "_meta": { "type": "object", @@ -1868,6 +1868,38 @@ "kind" ] }, + "ToolCallClientContributor": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallContributorKind.Client" + }, + "clientId": { + "type": "string", + "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + } + }, + "required": [ + "kind", + "clientId" + ] + }, + "ToolCallMcpContributor": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallContributorKind.MCP" + }, + "customizationId": { + "type": "string", + "description": "Customization ID of the corresponding MCP server in {@link SessionState.customizations}." + } + }, + "required": [ + "kind", + "customizationId" + ] + }, "ToolCallBase": { "type": "object", "description": "Metadata common to all tool call states.", @@ -1884,14 +1916,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." } }, "required": [ @@ -1978,14 +2010,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, "status": { "$ref": "#/$defs/ToolCallStatus.Streaming" @@ -2022,14 +2054,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", @@ -2094,14 +2126,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", @@ -2155,14 +2187,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", @@ -2246,14 +2278,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", @@ -2337,14 +2369,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", @@ -3204,7 +3236,7 @@ }, "McpServerCustomization": { "type": "object", - "description": "An MCP manifest contributed by a plugin or directory.\n\nWhen the server is declared inline in the containing plugin manifest,\n`uri` points at the manifest file and\n{@link CustomizationBase.range | `range`} narrows it to the\ndeclaration's span.", + "description": "An MCP server contributed by a plugin or directory.\n\nWhen the server is declared inline in the containing plugin manifest,\n`uri` points at the manifest file and\n{@link CustomizationBase.range | `range`} narrows it to the\ndeclaration's span.\n\nThe MCP server customization also reflects its current status.", "properties": { "id": { "type": "string", @@ -3231,13 +3263,168 @@ }, "type": { "$ref": "#/$defs/CustomizationType.McpServer" + }, + "enabled": { + "type": "boolean", + "description": "Whether this MCP server is currently enabled." + }, + "runtimeStatus": { + "$ref": "#/$defs/McpServerStatus", + "description": "Current status of the MCP server." + }, + "channel": { + "$ref": "#/$defs/URI", + "description": "An `mcp://`-protocol channel the client uses to side-channel traffic\ninto the upstream MCP server itself. The channel is NOT a fresh raw MCP\nconnection: it piggybacks on the AHP transport\nand skips the MCP `initialize` sequence.\n\nThe agent host MAY only serve a subset of MCP on this\nchannel; the served subset is described by domain-specific\ncapabilities such as those in\n{@link McpServerCustomizationApps.capabilities}.\n\nThe channel URI SHOULD be stable across the server's lifetime, but\nthe agent host MAY change it (for example across a restart) and\nMAY only expose it while the server is in\n{@link McpServerStatusKind.Ready | `Ready`}. Absence means no\nside-channel is currently available." + }, + "mcpApp": { + "$ref": "#/$defs/McpServerCustomizationApps", + "description": "MCP App support. This property SHOULD be advertised for MCP servers\nwhich support apps." } }, "required": [ "id", "uri", "name", - "type" + "type", + "enabled", + "runtimeStatus" + ] + }, + "McpServerCustomizationApps": { + "type": "object", + "description": "Information from the agent host needed to render MCP Apps served\nby this MCP server.", + "properties": { + "capabilities": { + "$ref": "#/$defs/AhpMcpUiHostCapabilities", + "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nthe AHP host can satisfy for Views backed by this server. The\nclient feeds these straight through into the `hostCapabilities` of\nthe `ui/initialize` response delivered to the View." + } + }, + "required": [ + "capabilities" + ] + }, + "AhpMcpUiHostCapabilities": { + "type": "object", + "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nan AHP host can derive from the upstream MCP server (and from AHP's own\nforwarding plumbing). Advertised on\n{@link McpServerCustomizationApps.capabilities} so clients can pass it\nthrough into the `hostCapabilities` of the `ui/initialize` response\ndelivered to an MCP App View.\n\nField names mirror the MCP Apps spec exactly, so the AHP-side producer\ncan pass them straight through into the `hostCapabilities` of the\n`ui/initialize` response delivered to the View.\n\nCapabilities outside this set (`openLinks`, `downloadFile`, `sandbox`,\n`experimental`) are decided locally by whichever AHP client renders the\nView and are NOT part of this AHP-level advertisement — only the\nserver-derived subset is.\n\nAn agent host MUST only advertise a capability when it actually accepts the\ncorresponding methods/notifications on the `mcp://` channel:\n\n- {@link serverTools}: host proxies `tools/list` and `tools/call` to\n the MCP server. When `listChanged` is `true`, the host also forwards\n `notifications/tools/list_changed`.\n- {@link serverResources}: host proxies `resources/read`,\n `resources/list`, and `resources/templates/list` to the MCP server.\n When `listChanged` is `true`, the host also forwards\n `notifications/resources/list_changed`.\n- {@link logging}: host accepts `notifications/message` log entries\n from the App and forwards them via `mcpNotification` (and forwards\n `logging/setLevel` calls to the server).\n- {@link sampling}: host serves `sampling/createMessage` via\n `mcpMethodCall`. When `sampling.tools` is present, the host also\n accepts SEP-1577 `tools` / `toolChoice` / `tool_use` content blocks\n inside `CreateMessageRequest`.", + "properties": { + "serverTools": { + "type": "object", + "properties": { + "listChanged": { + "type": "boolean" + } + }, + "description": "Producer proxies the MCP `tools/*` methods to the upstream server." + }, + "serverResources": { + "type": "object", + "properties": { + "listChanged": { + "type": "boolean" + } + }, + "description": "Producer proxies the MCP `resources/*` methods to the upstream server." + }, + "logging": { + "type": "object", + "additionalProperties": {}, + "description": "Producer accepts `notifications/message` log entries from the App via `mcpNotification`." + }, + "sampling": { + "type": "object", + "properties": { + "tools": { + "type": "string" + } + }, + "description": "Producer serves `sampling/createMessage` via `mcpMethodCall`." + } + } + }, + "McpServerStatusStarting": { + "type": "object", + "description": "Server is registered with the host but has not yet started.", + "properties": { + "kind": { + "$ref": "#/$defs/McpServerStatusKind.Starting" + } + }, + "required": [ + "kind" + ] + }, + "McpServerStatusReady": { + "type": "object", + "description": "Server is running and serving requests.", + "properties": { + "kind": { + "$ref": "#/$defs/McpServerStatusKind.Ready" + } + }, + "required": [ + "kind" + ] + }, + "McpServerStatusAuthRequired": { + "type": "object", + "description": "Server is reachable but cannot serve requests until the client\nauthenticates. Mirrors the discovery flow defined by\n[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)\n(Protected Resource Metadata) and the OAuth 2.1 / RFC 6750 challenge\nsemantics required by the MCP authorization spec.\n\nClients react to this state by calling the existing `authenticate`\ncommand with the {@link ProtectedResourceMetadata.resource | resource}\ncarried here. There is **no** `notify/authRequired` notification for\nMCP servers — the action stream is the single source of truth.\n\nWhen the transition is triggered by a request issued during a turn\n— most commonly\n{@link McpAuthRequiredReason.InsufficientScope | `InsufficientScope`}\nsurfacing mid-tool-call — the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session so the block is\nvisible at the summary level. Clients SHOULD watch this status on\nany MCP server backing a running tool call and surface an explicit\naffordance (e.g. a \"grant additional access\" prompt) tied to that\ntool call, rather than relying on the user to notice the\ncustomization’s status badge.", + "properties": { + "kind": { + "$ref": "#/$defs/McpServerStatusKind.AuthRequired" + }, + "reason": { + "$ref": "#/$defs/McpAuthRequiredReason", + "description": "Why authentication is required." + }, + "resource": { + "$ref": "#/$defs/ProtectedResourceMetadata", + "description": "RFC 9728 Protected Resource Metadata. The `resource` field is the\ncanonical MCP server URI per RFC 8707, used as the OAuth `resource`\nindicator. `authorization_servers` is REQUIRED by the MCP\nauthorization spec." + }, + "requiredScopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Scopes required for the current challenge, parsed from the\n`WWW-Authenticate: Bearer scope=\"…\"` header (or `scopes_supported`\nfallback). Authoritative for the next authorization request — clients\nMUST NOT assume any subset/superset relationship to\n`resource.scopes_supported`." + }, + "description": { + "type": "string", + "description": "Human-readable hint, typically from the OAuth `error_description`." + } + }, + "required": [ + "kind", + "reason", + "resource" + ] + }, + "McpServerStatusError": { + "type": "object", + "description": "Server failed to start, crashed, or otherwise transitioned to a\nnon-recoverable error. Use {@link McpServerStatusKind.AuthRequired}\nfor authentication failures.", + "properties": { + "kind": { + "$ref": "#/$defs/McpServerStatusKind.Error" + }, + "error": { + "$ref": "#/$defs/ErrorInfo", + "description": "Error details." + } + }, + "required": [ + "kind", + "error" + ] + }, + "McpServerStatusStopped": { + "type": "object", + "description": "Server has been shut down. The host MAY remove the server from the\nsession entirely shortly after this state.", + "properties": { + "kind": { + "$ref": "#/$defs/McpServerStatusKind.Stopped" + } + }, + "required": [ + "kind" ] }, "TerminalInfo": { diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 0fe621b4..51fa6473 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -707,7 +707,7 @@ "items": { "$ref": "#/$defs/Customization" }, - "description": "Customizations associated with this agent.\n\nAlways container customizations —\n{@link PluginCustomization | `PluginCustomization`} entries the agent\nbundles, plus {@link DirectoryCustomization | `DirectoryCustomization`}\nentries it watches in any workspace it's used with. When a session is\ncreated with this agent, these entries are augmented (e.g. directory\nURIs are resolved against the workspace, children are parsed) and\npropagated into the session's `customizations` list." + "description": "Customizations associated with this agent.\n\nEither container customizations —\n{@link PluginCustomization | `PluginCustomization`} entries the agent\nbundles, plus {@link DirectoryCustomization | `DirectoryCustomization`}\nentries it watches in any workspace it's used with — or top-level\n{@link McpServerCustomization | `McpServerCustomization`} entries\nthe agent host declares directly. When a session is created with\nthis agent, these entries are augmented (e.g. directory URIs are\nresolved against the workspace, children are parsed) and propagated\ninto the session's `customizations` list." } }, "required": [ @@ -882,7 +882,7 @@ "items": { "$ref": "#/$defs/Customization" }, - "description": "Top-level customizations active in this session.\n\nAlways container customizations — {@link PluginCustomization} or\n{@link DirectoryCustomization}. Children (agents, skills, prompts,\nrules, hooks, MCP servers) live in each container's\n{@link ContainerCustomizationBase.children | `children`} array.\n\nClient-published plugins arrive via\n{@link SessionActiveClient.customizations | `activeClient.customizations`}\nand the host propagates them into this list (typically with the\ncontainer's `clientId` set and `children` populated)." + "description": "Top-level customizations active in this session.\n\nAlways one of the {@link Customization} variants:\n\n- Container customizations ({@link PluginCustomization},\n {@link DirectoryCustomization}) whose children — agents, skills,\n prompts, rules, hooks, MCP servers — live in each container's\n {@link ContainerCustomizationBase.children | `children`} array.\n- Top-level {@link McpServerCustomization} entries the host\n surfaces directly (for example a globally-configured MCP server\n that isn't bundled in a plugin or directory). MCP servers may\n also appear as children of a container.\n\nClient-published plugins arrive via\n{@link SessionActiveClient.customizations | `activeClient.customizations`}\nand the host propagates them into this list (typically with the\ncontainer's `clientId` set and `children` populated). Clients\npublish in container shape only; bare MCP servers at the top level\nare server-originated." }, "_meta": { "type": "object", @@ -1996,6 +1996,38 @@ "kind" ] }, + "ToolCallClientContributor": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallContributorKind.Client" + }, + "clientId": { + "type": "string", + "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + } + }, + "required": [ + "kind", + "clientId" + ] + }, + "ToolCallMcpContributor": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallContributorKind.MCP" + }, + "customizationId": { + "type": "string", + "description": "Customization ID of the corresponding MCP server in {@link SessionState.customizations}." + } + }, + "required": [ + "kind", + "customizationId" + ] + }, "ToolCallBase": { "type": "object", "description": "Metadata common to all tool call states.", @@ -2012,14 +2044,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." } }, "required": [ @@ -2106,14 +2138,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, "status": { "$ref": "#/$defs/ToolCallStatus.Streaming" @@ -2150,14 +2182,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", @@ -2222,14 +2254,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", @@ -2283,14 +2315,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", @@ -2374,14 +2406,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", @@ -2465,14 +2497,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", @@ -3332,7 +3364,7 @@ }, "McpServerCustomization": { "type": "object", - "description": "An MCP manifest contributed by a plugin or directory.\n\nWhen the server is declared inline in the containing plugin manifest,\n`uri` points at the manifest file and\n{@link CustomizationBase.range | `range`} narrows it to the\ndeclaration's span.", + "description": "An MCP server contributed by a plugin or directory.\n\nWhen the server is declared inline in the containing plugin manifest,\n`uri` points at the manifest file and\n{@link CustomizationBase.range | `range`} narrows it to the\ndeclaration's span.\n\nThe MCP server customization also reflects its current status.", "properties": { "id": { "type": "string", @@ -3359,13 +3391,168 @@ }, "type": { "$ref": "#/$defs/CustomizationType.McpServer" + }, + "enabled": { + "type": "boolean", + "description": "Whether this MCP server is currently enabled." + }, + "runtimeStatus": { + "$ref": "#/$defs/McpServerStatus", + "description": "Current status of the MCP server." + }, + "channel": { + "$ref": "#/$defs/URI", + "description": "An `mcp://`-protocol channel the client uses to side-channel traffic\ninto the upstream MCP server itself. The channel is NOT a fresh raw MCP\nconnection: it piggybacks on the AHP transport\nand skips the MCP `initialize` sequence.\n\nThe agent host MAY only serve a subset of MCP on this\nchannel; the served subset is described by domain-specific\ncapabilities such as those in\n{@link McpServerCustomizationApps.capabilities}.\n\nThe channel URI SHOULD be stable across the server's lifetime, but\nthe agent host MAY change it (for example across a restart) and\nMAY only expose it while the server is in\n{@link McpServerStatusKind.Ready | `Ready`}. Absence means no\nside-channel is currently available." + }, + "mcpApp": { + "$ref": "#/$defs/McpServerCustomizationApps", + "description": "MCP App support. This property SHOULD be advertised for MCP servers\nwhich support apps." } }, "required": [ "id", "uri", "name", - "type" + "type", + "enabled", + "runtimeStatus" + ] + }, + "McpServerCustomizationApps": { + "type": "object", + "description": "Information from the agent host needed to render MCP Apps served\nby this MCP server.", + "properties": { + "capabilities": { + "$ref": "#/$defs/AhpMcpUiHostCapabilities", + "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nthe AHP host can satisfy for Views backed by this server. The\nclient feeds these straight through into the `hostCapabilities` of\nthe `ui/initialize` response delivered to the View." + } + }, + "required": [ + "capabilities" + ] + }, + "AhpMcpUiHostCapabilities": { + "type": "object", + "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nan AHP host can derive from the upstream MCP server (and from AHP's own\nforwarding plumbing). Advertised on\n{@link McpServerCustomizationApps.capabilities} so clients can pass it\nthrough into the `hostCapabilities` of the `ui/initialize` response\ndelivered to an MCP App View.\n\nField names mirror the MCP Apps spec exactly, so the AHP-side producer\ncan pass them straight through into the `hostCapabilities` of the\n`ui/initialize` response delivered to the View.\n\nCapabilities outside this set (`openLinks`, `downloadFile`, `sandbox`,\n`experimental`) are decided locally by whichever AHP client renders the\nView and are NOT part of this AHP-level advertisement — only the\nserver-derived subset is.\n\nAn agent host MUST only advertise a capability when it actually accepts the\ncorresponding methods/notifications on the `mcp://` channel:\n\n- {@link serverTools}: host proxies `tools/list` and `tools/call` to\n the MCP server. When `listChanged` is `true`, the host also forwards\n `notifications/tools/list_changed`.\n- {@link serverResources}: host proxies `resources/read`,\n `resources/list`, and `resources/templates/list` to the MCP server.\n When `listChanged` is `true`, the host also forwards\n `notifications/resources/list_changed`.\n- {@link logging}: host accepts `notifications/message` log entries\n from the App and forwards them via `mcpNotification` (and forwards\n `logging/setLevel` calls to the server).\n- {@link sampling}: host serves `sampling/createMessage` via\n `mcpMethodCall`. When `sampling.tools` is present, the host also\n accepts SEP-1577 `tools` / `toolChoice` / `tool_use` content blocks\n inside `CreateMessageRequest`.", + "properties": { + "serverTools": { + "type": "object", + "properties": { + "listChanged": { + "type": "boolean" + } + }, + "description": "Producer proxies the MCP `tools/*` methods to the upstream server." + }, + "serverResources": { + "type": "object", + "properties": { + "listChanged": { + "type": "boolean" + } + }, + "description": "Producer proxies the MCP `resources/*` methods to the upstream server." + }, + "logging": { + "type": "object", + "additionalProperties": {}, + "description": "Producer accepts `notifications/message` log entries from the App via `mcpNotification`." + }, + "sampling": { + "type": "object", + "properties": { + "tools": { + "type": "string" + } + }, + "description": "Producer serves `sampling/createMessage` via `mcpMethodCall`." + } + } + }, + "McpServerStatusStarting": { + "type": "object", + "description": "Server is registered with the host but has not yet started.", + "properties": { + "kind": { + "$ref": "#/$defs/McpServerStatusKind.Starting" + } + }, + "required": [ + "kind" + ] + }, + "McpServerStatusReady": { + "type": "object", + "description": "Server is running and serving requests.", + "properties": { + "kind": { + "$ref": "#/$defs/McpServerStatusKind.Ready" + } + }, + "required": [ + "kind" + ] + }, + "McpServerStatusAuthRequired": { + "type": "object", + "description": "Server is reachable but cannot serve requests until the client\nauthenticates. Mirrors the discovery flow defined by\n[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)\n(Protected Resource Metadata) and the OAuth 2.1 / RFC 6750 challenge\nsemantics required by the MCP authorization spec.\n\nClients react to this state by calling the existing `authenticate`\ncommand with the {@link ProtectedResourceMetadata.resource | resource}\ncarried here. There is **no** `notify/authRequired` notification for\nMCP servers — the action stream is the single source of truth.\n\nWhen the transition is triggered by a request issued during a turn\n— most commonly\n{@link McpAuthRequiredReason.InsufficientScope | `InsufficientScope`}\nsurfacing mid-tool-call — the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session so the block is\nvisible at the summary level. Clients SHOULD watch this status on\nany MCP server backing a running tool call and surface an explicit\naffordance (e.g. a \"grant additional access\" prompt) tied to that\ntool call, rather than relying on the user to notice the\ncustomization’s status badge.", + "properties": { + "kind": { + "$ref": "#/$defs/McpServerStatusKind.AuthRequired" + }, + "reason": { + "$ref": "#/$defs/McpAuthRequiredReason", + "description": "Why authentication is required." + }, + "resource": { + "$ref": "#/$defs/ProtectedResourceMetadata", + "description": "RFC 9728 Protected Resource Metadata. The `resource` field is the\ncanonical MCP server URI per RFC 8707, used as the OAuth `resource`\nindicator. `authorization_servers` is REQUIRED by the MCP\nauthorization spec." + }, + "requiredScopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Scopes required for the current challenge, parsed from the\n`WWW-Authenticate: Bearer scope=\"…\"` header (or `scopes_supported`\nfallback). Authoritative for the next authorization request — clients\nMUST NOT assume any subset/superset relationship to\n`resource.scopes_supported`." + }, + "description": { + "type": "string", + "description": "Human-readable hint, typically from the OAuth `error_description`." + } + }, + "required": [ + "kind", + "reason", + "resource" + ] + }, + "McpServerStatusError": { + "type": "object", + "description": "Server failed to start, crashed, or otherwise transitioned to a\nnon-recoverable error. Use {@link McpServerStatusKind.AuthRequired}\nfor authentication failures.", + "properties": { + "kind": { + "$ref": "#/$defs/McpServerStatusKind.Error" + }, + "error": { + "$ref": "#/$defs/ErrorInfo", + "description": "Error details." + } + }, + "required": [ + "kind", + "error" + ] + }, + "McpServerStatusStopped": { + "type": "object", + "description": "Server has been shut down. The host MAY remove the server from the\nsession entirely shortly after this state.", + "properties": { + "kind": { + "$ref": "#/$defs/McpServerStatusKind.Stopped" + } + }, + "required": [ + "kind" ] }, "TerminalInfo": { diff --git a/schema/state.schema.json b/schema/state.schema.json index 2637e559..e3190d39 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -490,7 +490,7 @@ "items": { "$ref": "#/$defs/Customization" }, - "description": "Customizations associated with this agent.\n\nAlways container customizations —\n{@link PluginCustomization | `PluginCustomization`} entries the agent\nbundles, plus {@link DirectoryCustomization | `DirectoryCustomization`}\nentries it watches in any workspace it's used with. When a session is\ncreated with this agent, these entries are augmented (e.g. directory\nURIs are resolved against the workspace, children are parsed) and\npropagated into the session's `customizations` list." + "description": "Customizations associated with this agent.\n\nEither container customizations —\n{@link PluginCustomization | `PluginCustomization`} entries the agent\nbundles, plus {@link DirectoryCustomization | `DirectoryCustomization`}\nentries it watches in any workspace it's used with — or top-level\n{@link McpServerCustomization | `McpServerCustomization`} entries\nthe agent host declares directly. When a session is created with\nthis agent, these entries are augmented (e.g. directory URIs are\nresolved against the workspace, children are parsed) and propagated\ninto the session's `customizations` list." } }, "required": [ @@ -665,7 +665,7 @@ "items": { "$ref": "#/$defs/Customization" }, - "description": "Top-level customizations active in this session.\n\nAlways container customizations — {@link PluginCustomization} or\n{@link DirectoryCustomization}. Children (agents, skills, prompts,\nrules, hooks, MCP servers) live in each container's\n{@link ContainerCustomizationBase.children | `children`} array.\n\nClient-published plugins arrive via\n{@link SessionActiveClient.customizations | `activeClient.customizations`}\nand the host propagates them into this list (typically with the\ncontainer's `clientId` set and `children` populated)." + "description": "Top-level customizations active in this session.\n\nAlways one of the {@link Customization} variants:\n\n- Container customizations ({@link PluginCustomization},\n {@link DirectoryCustomization}) whose children — agents, skills,\n prompts, rules, hooks, MCP servers — live in each container's\n {@link ContainerCustomizationBase.children | `children`} array.\n- Top-level {@link McpServerCustomization} entries the host\n surfaces directly (for example a globally-configured MCP server\n that isn't bundled in a plugin or directory). MCP servers may\n also appear as children of a container.\n\nClient-published plugins arrive via\n{@link SessionActiveClient.customizations | `activeClient.customizations`}\nand the host propagates them into this list (typically with the\ncontainer's `clientId` set and `children` populated). Clients\npublish in container shape only; bare MCP servers at the top level\nare server-originated." }, "_meta": { "type": "object", @@ -1779,6 +1779,38 @@ "kind" ] }, + "ToolCallClientContributor": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallContributorKind.Client" + }, + "clientId": { + "type": "string", + "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + } + }, + "required": [ + "kind", + "clientId" + ] + }, + "ToolCallMcpContributor": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallContributorKind.MCP" + }, + "customizationId": { + "type": "string", + "description": "Customization ID of the corresponding MCP server in {@link SessionState.customizations}." + } + }, + "required": [ + "kind", + "customizationId" + ] + }, "ToolCallBase": { "type": "object", "description": "Metadata common to all tool call states.", @@ -1795,14 +1827,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." } }, "required": [ @@ -1889,14 +1921,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, "status": { "$ref": "#/$defs/ToolCallStatus.Streaming" @@ -1933,14 +1965,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", @@ -2005,14 +2037,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", @@ -2066,14 +2098,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", @@ -2157,14 +2189,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", @@ -2248,14 +2280,14 @@ "type": "string", "description": "Human-readable tool name" }, - "toolClientId": { - "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", @@ -3115,7 +3147,7 @@ }, "McpServerCustomization": { "type": "object", - "description": "An MCP manifest contributed by a plugin or directory.\n\nWhen the server is declared inline in the containing plugin manifest,\n`uri` points at the manifest file and\n{@link CustomizationBase.range | `range`} narrows it to the\ndeclaration's span.", + "description": "An MCP server contributed by a plugin or directory.\n\nWhen the server is declared inline in the containing plugin manifest,\n`uri` points at the manifest file and\n{@link CustomizationBase.range | `range`} narrows it to the\ndeclaration's span.\n\nThe MCP server customization also reflects its current status.", "properties": { "id": { "type": "string", @@ -3142,13 +3174,168 @@ }, "type": { "$ref": "#/$defs/CustomizationType.McpServer" + }, + "enabled": { + "type": "boolean", + "description": "Whether this MCP server is currently enabled." + }, + "runtimeStatus": { + "$ref": "#/$defs/McpServerStatus", + "description": "Current status of the MCP server." + }, + "channel": { + "$ref": "#/$defs/URI", + "description": "An `mcp://`-protocol channel the client uses to side-channel traffic\ninto the upstream MCP server itself. The channel is NOT a fresh raw MCP\nconnection: it piggybacks on the AHP transport\nand skips the MCP `initialize` sequence.\n\nThe agent host MAY only serve a subset of MCP on this\nchannel; the served subset is described by domain-specific\ncapabilities such as those in\n{@link McpServerCustomizationApps.capabilities}.\n\nThe channel URI SHOULD be stable across the server's lifetime, but\nthe agent host MAY change it (for example across a restart) and\nMAY only expose it while the server is in\n{@link McpServerStatusKind.Ready | `Ready`}. Absence means no\nside-channel is currently available." + }, + "mcpApp": { + "$ref": "#/$defs/McpServerCustomizationApps", + "description": "MCP App support. This property SHOULD be advertised for MCP servers\nwhich support apps." } }, "required": [ "id", "uri", "name", - "type" + "type", + "enabled", + "runtimeStatus" + ] + }, + "McpServerCustomizationApps": { + "type": "object", + "description": "Information from the agent host needed to render MCP Apps served\nby this MCP server.", + "properties": { + "capabilities": { + "$ref": "#/$defs/AhpMcpUiHostCapabilities", + "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nthe AHP host can satisfy for Views backed by this server. The\nclient feeds these straight through into the `hostCapabilities` of\nthe `ui/initialize` response delivered to the View." + } + }, + "required": [ + "capabilities" + ] + }, + "AhpMcpUiHostCapabilities": { + "type": "object", + "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nan AHP host can derive from the upstream MCP server (and from AHP's own\nforwarding plumbing). Advertised on\n{@link McpServerCustomizationApps.capabilities} so clients can pass it\nthrough into the `hostCapabilities` of the `ui/initialize` response\ndelivered to an MCP App View.\n\nField names mirror the MCP Apps spec exactly, so the AHP-side producer\ncan pass them straight through into the `hostCapabilities` of the\n`ui/initialize` response delivered to the View.\n\nCapabilities outside this set (`openLinks`, `downloadFile`, `sandbox`,\n`experimental`) are decided locally by whichever AHP client renders the\nView and are NOT part of this AHP-level advertisement — only the\nserver-derived subset is.\n\nAn agent host MUST only advertise a capability when it actually accepts the\ncorresponding methods/notifications on the `mcp://` channel:\n\n- {@link serverTools}: host proxies `tools/list` and `tools/call` to\n the MCP server. When `listChanged` is `true`, the host also forwards\n `notifications/tools/list_changed`.\n- {@link serverResources}: host proxies `resources/read`,\n `resources/list`, and `resources/templates/list` to the MCP server.\n When `listChanged` is `true`, the host also forwards\n `notifications/resources/list_changed`.\n- {@link logging}: host accepts `notifications/message` log entries\n from the App and forwards them via `mcpNotification` (and forwards\n `logging/setLevel` calls to the server).\n- {@link sampling}: host serves `sampling/createMessage` via\n `mcpMethodCall`. When `sampling.tools` is present, the host also\n accepts SEP-1577 `tools` / `toolChoice` / `tool_use` content blocks\n inside `CreateMessageRequest`.", + "properties": { + "serverTools": { + "type": "object", + "properties": { + "listChanged": { + "type": "boolean" + } + }, + "description": "Producer proxies the MCP `tools/*` methods to the upstream server." + }, + "serverResources": { + "type": "object", + "properties": { + "listChanged": { + "type": "boolean" + } + }, + "description": "Producer proxies the MCP `resources/*` methods to the upstream server." + }, + "logging": { + "type": "object", + "additionalProperties": {}, + "description": "Producer accepts `notifications/message` log entries from the App via `mcpNotification`." + }, + "sampling": { + "type": "object", + "properties": { + "tools": { + "type": "string" + } + }, + "description": "Producer serves `sampling/createMessage` via `mcpMethodCall`." + } + } + }, + "McpServerStatusStarting": { + "type": "object", + "description": "Server is registered with the host but has not yet started.", + "properties": { + "kind": { + "$ref": "#/$defs/McpServerStatusKind.Starting" + } + }, + "required": [ + "kind" + ] + }, + "McpServerStatusReady": { + "type": "object", + "description": "Server is running and serving requests.", + "properties": { + "kind": { + "$ref": "#/$defs/McpServerStatusKind.Ready" + } + }, + "required": [ + "kind" + ] + }, + "McpServerStatusAuthRequired": { + "type": "object", + "description": "Server is reachable but cannot serve requests until the client\nauthenticates. Mirrors the discovery flow defined by\n[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)\n(Protected Resource Metadata) and the OAuth 2.1 / RFC 6750 challenge\nsemantics required by the MCP authorization spec.\n\nClients react to this state by calling the existing `authenticate`\ncommand with the {@link ProtectedResourceMetadata.resource | resource}\ncarried here. There is **no** `notify/authRequired` notification for\nMCP servers — the action stream is the single source of truth.\n\nWhen the transition is triggered by a request issued during a turn\n— most commonly\n{@link McpAuthRequiredReason.InsufficientScope | `InsufficientScope`}\nsurfacing mid-tool-call — the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session so the block is\nvisible at the summary level. Clients SHOULD watch this status on\nany MCP server backing a running tool call and surface an explicit\naffordance (e.g. a \"grant additional access\" prompt) tied to that\ntool call, rather than relying on the user to notice the\ncustomization’s status badge.", + "properties": { + "kind": { + "$ref": "#/$defs/McpServerStatusKind.AuthRequired" + }, + "reason": { + "$ref": "#/$defs/McpAuthRequiredReason", + "description": "Why authentication is required." + }, + "resource": { + "$ref": "#/$defs/ProtectedResourceMetadata", + "description": "RFC 9728 Protected Resource Metadata. The `resource` field is the\ncanonical MCP server URI per RFC 8707, used as the OAuth `resource`\nindicator. `authorization_servers` is REQUIRED by the MCP\nauthorization spec." + }, + "requiredScopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Scopes required for the current challenge, parsed from the\n`WWW-Authenticate: Bearer scope=\"…\"` header (or `scopes_supported`\nfallback). Authoritative for the next authorization request — clients\nMUST NOT assume any subset/superset relationship to\n`resource.scopes_supported`." + }, + "description": { + "type": "string", + "description": "Human-readable hint, typically from the OAuth `error_description`." + } + }, + "required": [ + "kind", + "reason", + "resource" + ] + }, + "McpServerStatusError": { + "type": "object", + "description": "Server failed to start, crashed, or otherwise transitioned to a\nnon-recoverable error. Use {@link McpServerStatusKind.AuthRequired}\nfor authentication failures.", + "properties": { + "kind": { + "$ref": "#/$defs/McpServerStatusKind.Error" + }, + "error": { + "$ref": "#/$defs/ErrorInfo", + "description": "Error details." + } + }, + "required": [ + "kind", + "error" + ] + }, + "McpServerStatusStopped": { + "type": "object", + "description": "Server has been shut down. The host MAY remove the server from the\nsession entirely shortly after this state.", + "properties": { + "kind": { + "$ref": "#/$defs/McpServerStatusKind.Stopped" + } + }, + "required": [ + "kind" ] }, "TerminalInfo": { @@ -3645,6 +3832,16 @@ } ] }, + "ToolCallContributor": { + "oneOf": [ + { + "$ref": "#/$defs/ToolCallClientContributor" + }, + { + "$ref": "#/$defs/ToolCallMcpContributor" + } + ] + }, "ToolCallState": { "oneOf": [ {}, @@ -3761,14 +3958,39 @@ }, "Customization": { "oneOf": [ + {}, { "$ref": "#/$defs/PluginCustomization" }, { "$ref": "#/$defs/DirectoryCustomization" + }, + { + "$ref": "#/$defs/McpServerCustomization" + } + ], + "description": "A top-level customization active in a session. Either a container\n({@link PluginCustomization} or {@link DirectoryCustomization}) whose\nleaf customizations live in its\n{@link ContainerCustomizationBase.children | `children`} array, or a\nbare {@link McpServerCustomization} surfaced directly by the host." + }, + "McpServerStatus": { + "oneOf": [ + {}, + { + "$ref": "#/$defs/McpServerStatusStarting" + }, + { + "$ref": "#/$defs/McpServerStatusReady" + }, + { + "$ref": "#/$defs/McpServerStatusAuthRequired" + }, + { + "$ref": "#/$defs/McpServerStatusError" + }, + { + "$ref": "#/$defs/McpServerStatusStopped" } ], - "description": "A top-level customization active in a session. Always a container\n({@link PluginCustomization} or {@link DirectoryCustomization}); the\nremaining customization types appear inside the container's\n{@link ContainerCustomizationBase.children | `children`} array." + "description": "Discriminated union of all MCP server statuses. Discriminated by `kind`." }, "TerminalClaim": { "oneOf": [ diff --git a/types/action-origin.generated.ts b/types/action-origin.generated.ts index e2ccbb93..e1766791 100644 --- a/types/action-origin.generated.ts +++ b/types/action-origin.generated.ts @@ -40,6 +40,7 @@ import type { SessionCustomizationToggledAction, SessionCustomizationUpdatedAction, SessionCustomizationRemovedAction, + SessionMcpServerStatusChangedAction, SessionTruncatedAction, SessionIsReadChangedAction, SessionIsArchivedChangedAction, @@ -125,6 +126,7 @@ export type SessionAction = | SessionCustomizationToggledAction | SessionCustomizationUpdatedAction | SessionCustomizationRemovedAction + | SessionMcpServerStatusChangedAction | SessionTruncatedAction | SessionIsReadChangedAction | SessionIsArchivedChangedAction @@ -177,6 +179,7 @@ export type ServerSessionAction = | SessionCustomizationsChangedAction | SessionCustomizationUpdatedAction | SessionCustomizationRemovedAction + | SessionMcpServerStatusChangedAction | SessionActivityChangedAction | SessionChangesetsChangedAction | SessionMetaChangedAction @@ -298,6 +301,7 @@ export const IS_CLIENT_DISPATCHABLE: { readonly [K in StateAction['type']]: bool [ActionType.SessionCustomizationToggled]: true, [ActionType.SessionCustomizationUpdated]: false, [ActionType.SessionCustomizationRemoved]: false, + [ActionType.SessionMcpServerStatusChanged]: false, [ActionType.SessionTruncated]: true, [ActionType.SessionIsReadChanged]: true, [ActionType.SessionIsArchivedChanged]: true, diff --git a/types/channels-root/state.ts b/types/channels-root/state.ts index 849cd986..33976486 100644 --- a/types/channels-root/state.ts +++ b/types/channels-root/state.ts @@ -67,13 +67,15 @@ export interface AgentInfo { /** * Customizations associated with this agent. * - * Always container customizations — + * Either container customizations — * {@link PluginCustomization | `PluginCustomization`} entries the agent * bundles, plus {@link DirectoryCustomization | `DirectoryCustomization`} - * entries it watches in any workspace it's used with. When a session is - * created with this agent, these entries are augmented (e.g. directory - * URIs are resolved against the workspace, children are parsed) and - * propagated into the session's `customizations` list. + * entries it watches in any workspace it's used with — or top-level + * {@link McpServerCustomization | `McpServerCustomization`} entries + * the agent host declares directly. When a session is created with + * this agent, these entries are augmented (e.g. directory URIs are + * resolved against the workspace, children are parsed) and propagated + * into the session's `customizations` list. */ customizations?: Customization[]; } diff --git a/types/channels-session/actions.ts b/types/channels-session/actions.ts index c5d11457..7f12126a 100644 --- a/types/channels-session/actions.ts +++ b/types/channels-session/actions.ts @@ -14,13 +14,16 @@ import type { ToolDefinition, SessionActiveClient, Customization, + McpServerStatus, SessionInputAnswer, SessionInputRequest, SessionInputResponseKind, ConfirmationOption, AgentSelection, + ToolCallContributor, } from './state.js'; import type { ModelSelection } from '../channels-root/state.js'; +import type { URI } from '../common/state.js'; import { ToolCallConfirmationReason, ToolCallCancellationReason, @@ -79,7 +82,7 @@ export interface SessionCreationFailedAction { /** * A new message has been sent to the agent, and a new turn starts. - * + * * A client is only allowed to send {@link MessageKind.User} messages. * * @category Session Actions @@ -132,9 +135,11 @@ export interface SessionResponsePartAction { /** * A tool call begins — parameters are streaming from the LM. * - * For client-provided tools, the server sets `toolClientId` to identify the - * owning client. That client is responsible for executing the tool once it - * reaches the `running` state and dispatching `session/toolCallComplete`. + * The server sets {@link ToolCallContributor | `contributor`} to identify + * the origin of the tool. For client-provided tools, the named client is + * responsible for executing the tool once it reaches the `running` state + * and dispatching `session/toolCallComplete`. For MCP-served tools, the + * server executes the call against the named `McpServerCustomization`. * * @category Session Actions * @version 1 @@ -146,10 +151,10 @@ export interface SessionToolCallStartAction extends ToolCallActionBase { /** Human-readable tool name */ displayName: string; /** - * If this tool is provided by a client, the `clientId` of the owning client. - * Absent for server-side tools. + * Reference to the contributor of the tool being called. Absent for + * server-side tools that are not contributed by a client or MCP server. */ - toolClientId?: string; + contributor?: ToolCallContributor; } /** @@ -633,6 +638,45 @@ export interface SessionCustomizationRemovedAction { id: string; } +/** + * Updates the runtime fields of an existing + * {@link McpServerCustomization} — narrow alternative to + * {@link SessionCustomizationUpdatedAction} for the high-frequency + * `starting` ↔ `ready` ↔ `authRequired` transitions. + * + * Locates the target entry by `id`, searching both the top-level + * customization list and the `children` array of every container. + * Replaces the entry's {@link McpServerCustomization.runtimeStatus | `runtimeStatus`} + * and {@link McpServerCustomization.channel | `channel`} + * (full-replacement semantics: omit `channel` to clear an existing + * channel URI). Other fields of the customization are preserved. + * + * Is a no-op when no matching `McpServerCustomization` is found. To + * update any other field (name, icons, `mcpApp` capabilities, etc.) use + * {@link SessionCustomizationUpdatedAction} instead. + * + * When the transition is to {@link McpServerStatusKind.AuthRequired} + * because of a request issued mid-turn, the host SHOULD also raise + * {@link SessionStatus.InputNeeded} on the session — see + * {@link McpServerStatusAuthRequired} for the rationale. + * + * @category Session Actions + * @version 1 + */ +export interface SessionMcpServerStatusChangedAction { + type: ActionType.SessionMcpServerStatusChanged; + /** The id of the {@link McpServerCustomization} to update. */ + id: string; + /** The new runtime status. */ + runtimeStatus: McpServerStatus; + /** + * Updated `mcp://` side-channel URI. Full-replacement: omit to clear + * an existing channel (typical when leaving + * {@link McpServerStatusKind.Ready | `Ready`}). + */ + channel?: URI; +} + // ─── Config Actions ────────────────────────────────────────────────────────── /** @@ -701,7 +745,7 @@ export interface SessionTruncatedAction { * updated in place; otherwise it is appended to the queue. If the session is * idle when a queued message is set, the server SHOULD immediately consume it * and start a new turn. - * + * * A client is only allowed to send {@link MessageKind.User} messages. * * @category Session Actions diff --git a/types/channels-session/reducer.ts b/types/channels-session/reducer.ts index ea162676..35c0ea2b 100644 --- a/types/channels-session/reducer.ts +++ b/types/channels-session/reducer.ts @@ -16,6 +16,7 @@ import type { Turn, PendingMessage, ConfirmationOption, + McpServerCustomization, } from './state.js'; import { SessionLifecycle, @@ -26,6 +27,7 @@ import { ToolCallCancellationReason, ResponsePartKind, PendingMessageKind, + CustomizationType, } from './state.js'; import type { SessionAction } from '../action-origin.generated.js'; import { softAssertNever } from '../common/reducer-helpers.js'; @@ -38,7 +40,7 @@ function tcBase(tc: ToolCallState) { toolCallId: tc.toolCallId, toolName: tc.toolName, displayName: tc.displayName, - toolClientId: tc.toolClientId, + contributor: tc.contributor, _meta: tc._meta, }; } @@ -359,7 +361,7 @@ export function sessionReducer(state: SessionState, action: SessionAction, log?: toolCallId: action.toolCallId, toolName: action.toolName, displayName: action.displayName, - toolClientId: action.toolClientId, + contributor: action.contributor, _meta: action._meta, status: ToolCallStatus.Streaming, }, @@ -655,6 +657,9 @@ export function sessionReducer(state: SessionState, action: SessionAction, log?: } let changed = false; const updated = list.map(container => { + if (container.type === CustomizationType.McpServer) { + return container; + } const children = container.children; if (!children) { return container; @@ -674,6 +679,59 @@ export function sessionReducer(state: SessionState, action: SessionAction, log?: return { ...state, customizations: updated }; } + case ActionType.SessionMcpServerStatusChanged: { + const list = state.customizations; + if (!list) { + return state; + } + const topIdx = list.findIndex(c => c.id === action.id); + if (topIdx >= 0) { + const entry = list[topIdx]; + if (entry.type !== CustomizationType.McpServer) { + return state; + } + const updatedEntry: McpServerCustomization = { + ...entry, + runtimeStatus: action.runtimeStatus, + channel: action.channel, + }; + const updated = list.slice(); + updated[topIdx] = updatedEntry; + return { ...state, customizations: updated }; + } + let changed = false; + const updated = list.map(container => { + if (container.type === CustomizationType.McpServer) { + return container; + } + const children = container.children; + if (!children) { + return container; + } + const childIdx = children.findIndex(c => c.id === action.id); + if (childIdx < 0) { + return container; + } + const child = children[childIdx]; + if (child.type !== CustomizationType.McpServer) { + return container; + } + changed = true; + const updatedChild: McpServerCustomization = { + ...child, + runtimeStatus: action.runtimeStatus, + channel: action.channel, + }; + const newChildren = children.slice(); + newChildren[childIdx] = updatedChild; + return { ...container, children: newChildren }; + }); + if (!changed) { + return state; + } + return { ...state, customizations: updated }; + } + // ── Truncation ──────────────────────────────────────────────────────── case ActionType.SessionTruncated: { diff --git a/types/channels-session/state.ts b/types/channels-session/state.ts index 71160e1b..54b25dc1 100644 --- a/types/channels-session/state.ts +++ b/types/channels-session/state.ts @@ -5,20 +5,21 @@ * @module channels-session/state */ +import type { ChangesetSummary } from '../channels-changeset/state.js'; +import type { ModelSelection } from '../channels-root/state.js'; import type { - URI, - StringOrMarkdown, - Icon, ConfigPropertySchema, ContentRef, ErrorInfo, FileEdit, + Icon, + ProtectedResourceMetadata, + StringOrMarkdown, TextRange, TextSelection, + URI, UsageInfo, } from '../common/state.js'; -import type { ChangesetSummary } from '../channels-changeset/state.js'; -import type { ModelSelection } from '../channels-root/state.js'; // ─── Pending Message Types ─────────────────────────────────────────────────── @@ -118,15 +119,23 @@ export interface SessionState { /** * Top-level customizations active in this session. * - * Always container customizations — {@link PluginCustomization} or - * {@link DirectoryCustomization}. Children (agents, skills, prompts, - * rules, hooks, MCP servers) live in each container's - * {@link ContainerCustomizationBase.children | `children`} array. + * Always one of the {@link Customization} variants: + * + * - Container customizations ({@link PluginCustomization}, + * {@link DirectoryCustomization}) whose children — agents, skills, + * prompts, rules, hooks, MCP servers — live in each container's + * {@link ContainerCustomizationBase.children | `children`} array. + * - Top-level {@link McpServerCustomization} entries the host + * surfaces directly (for example a globally-configured MCP server + * that isn't bundled in a plugin or directory). MCP servers may + * also appear as children of a container. * * Client-published plugins arrive via * {@link SessionActiveClient.customizations | `activeClient.customizations`} * and the host propagates them into this list (typically with the - * container's `clientId` set and `children` populated). + * container's `clientId` set and `children` populated). Clients + * publish in container shape only; bare MCP servers at the top level + * are server-originated. */ customizations?: Customization[]; /** @@ -601,7 +610,7 @@ export interface ActiveTurn { /** * Discriminant for Message types. - * + * * @category Turn Types */ export enum MessageKind { @@ -922,6 +931,33 @@ export interface ConfirmationOption { group?: number; } +export const enum ToolCallContributorKind { + Client = 'client', + MCP = 'mcp', +} + +export interface ToolCallClientContributor { + kind: ToolCallContributorKind.Client; + /** + * If this tool is provided by a client, the `clientId` of the owning client. + * Absent for server-side tools. + * + * When set, the identified client is responsible for executing the tool and + * dispatching `session/toolCallComplete` with the result. + */ + clientId: string; +} + +export interface ToolCallMcpContributor { + kind: ToolCallContributorKind.MCP; + /** + * Customization ID of the corresponding MCP server in {@link SessionState.customizations}. + */ + customizationId: string; +} + +export type ToolCallContributor = ToolCallClientContributor | ToolCallMcpContributor; + /** * Metadata common to all tool call states. * @@ -940,20 +976,15 @@ interface ToolCallBase { /** Human-readable tool name */ displayName: string; /** - * If this tool is provided by a client, the `clientId` of the owning client. - * Absent for server-side tools. - * - * When set, the identified client is responsible for executing the tool and - * dispatching `session/toolCallComplete` with the result. + * Reference to the contributor of the tool being called. */ - toolClientId?: string; + contributor?: ToolCallContributor; /** * Additional provider-specific metadata for this tool call. * - * Clients MAY look for well-known keys here to provide enhanced UI. - * For example, a `ptyTerminal` key with `{ input: string; output: string }` - * indicates the tool operated on a terminal (both `input` and `output` may - * contain escape sequences). + * This MAY include a `ui` field corresponding to the MCP Apps (SEP-1865) + * `McpUiToolMeta` found in MCP tool calls, which may be used in combination + * with the {@link contributor} to serve MCP Apps. */ _meta?: Record; } @@ -1302,10 +1333,12 @@ export type ToolResultContent = * Discriminant for the kind of customization. * * Top-level entries in {@link SessionState.customizations} and - * {@link AgentInfo.customizations} are always - * {@link CustomizationType.Plugin | `Plugin`} or - * {@link CustomizationType.Directory | `Directory`}; the remaining - * types appear only as children of those containers. + * {@link AgentInfo.customizations} are either container customizations + * ({@link CustomizationType.Plugin | `Plugin`} or + * {@link CustomizationType.Directory | `Directory`}) or + * {@link CustomizationType.McpServer | `McpServer`} entries surfaced + * directly by the host. The remaining types appear only as children of + * a container. * * @category Customization Types */ @@ -1605,17 +1638,129 @@ export interface HookCustomization extends CustomizationBase { } /** - * An MCP manifest contributed by a plugin or directory. + * An MCP server contributed by a plugin or directory. * * When the server is declared inline in the containing plugin manifest, * `uri` points at the manifest file and * {@link CustomizationBase.range | `range`} narrows it to the * declaration's span. * + * The MCP server customization also reflects its current status. + * * @category Customization Types */ export interface McpServerCustomization extends CustomizationBase { type: CustomizationType.McpServer; + /** + * Whether this MCP server is currently enabled. + */ + enabled: boolean; + /** + * Current status of the MCP server. + */ + runtimeStatus: McpServerStatus; + /** + * An `mcp://`-protocol channel the client uses to side-channel traffic + * into the upstream MCP server itself. The channel is NOT a fresh raw MCP + * connection: it piggybacks on the AHP transport + * and skips the MCP `initialize` sequence. + * + * The agent host MAY only serve a subset of MCP on this + * channel; the served subset is described by domain-specific + * capabilities such as those in + * {@link McpServerCustomizationApps.capabilities}. + * + * The channel URI SHOULD be stable across the server's lifetime, but + * the agent host MAY change it (for example across a restart) and + * MAY only expose it while the server is in + * {@link McpServerStatusKind.Ready | `Ready`}. Absence means no + * side-channel is currently available. + */ + channel?: URI; + /** + * MCP App support. This property SHOULD be advertised for MCP servers + * which support apps. + */ + mcpApp?: McpServerCustomizationApps; +} + +/** + * Information from the agent host needed to render MCP Apps served + * by this MCP server. + * + * @category MCP Server State + */ +export interface McpServerCustomizationApps { + /** + * The subset of MCP App + * [`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx) + * the AHP host can satisfy for Views backed by this server. The + * client feeds these straight through into the `hostCapabilities` of + * the `ui/initialize` response delivered to the View. + */ + capabilities: AhpMcpUiHostCapabilities; +} + +/** + * The subset of MCP App + * [`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx) + * an AHP host can derive from the upstream MCP server (and from AHP's own + * forwarding plumbing). Advertised on + * {@link McpServerCustomizationApps.capabilities} so clients can pass it + * through into the `hostCapabilities` of the `ui/initialize` response + * delivered to an MCP App View. + * + * Field names mirror the MCP Apps spec exactly, so the AHP-side producer + * can pass them straight through into the `hostCapabilities` of the + * `ui/initialize` response delivered to the View. + * + * Capabilities outside this set (`openLinks`, `downloadFile`, `sandbox`, + * `experimental`) are decided locally by whichever AHP client renders the + * View and are NOT part of this AHP-level advertisement — only the + * server-derived subset is. + * + * An agent host MUST only advertise a capability when it actually accepts the + * corresponding methods/notifications on the `mcp://` channel: + * + * - {@link serverTools}: host proxies `tools/list` and `tools/call` to + * the MCP server. When `listChanged` is `true`, the host also forwards + * `notifications/tools/list_changed`. + * - {@link serverResources}: host proxies `resources/read`, + * `resources/list`, and `resources/templates/list` to the MCP server. + * When `listChanged` is `true`, the host also forwards + * `notifications/resources/list_changed`. + * - {@link logging}: host accepts `notifications/message` log entries + * from the App and forwards them via `mcpNotification` (and forwards + * `logging/setLevel` calls to the server). + * - {@link sampling}: host serves `sampling/createMessage` via + * `mcpMethodCall`. When `sampling.tools` is present, the host also + * accepts SEP-1577 `tools` / `toolChoice` / `tool_use` content blocks + * inside `CreateMessageRequest`. + * + * @category MCP Server State + * @see {@link https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx | MCP Apps spec (SEP-1865)} + */ +export interface AhpMcpUiHostCapabilities { + /** Producer proxies the MCP `tools/*` methods to the upstream server. */ + serverTools?: { + /** Producer forwards `notifications/tools/list_changed` from the server. */ + listChanged?: boolean; + }; + /** Producer proxies the MCP `resources/*` methods to the upstream server. */ + serverResources?: { + /** Producer forwards `notifications/resources/list_changed` from the server. */ + listChanged?: boolean; + }; + /** Producer accepts `notifications/message` log entries from the App via `mcpNotification`. */ + logging?: Record; + /** Producer serves `sampling/createMessage` via `mcpMethodCall`. */ + sampling?: { + /** + * Producer accepts SEP-1577 `tools` / `toolChoice` / `tool_use` content + * blocks inside `CreateMessageRequest`. + */ + tools?: Record; + }; } /** @@ -1633,11 +1778,176 @@ export type ChildCustomization = | McpServerCustomization; /** - * A top-level customization active in a session. Always a container - * ({@link PluginCustomization} or {@link DirectoryCustomization}); the - * remaining customization types appear inside the container's - * {@link ContainerCustomizationBase.children | `children`} array. + * A top-level customization active in a session. Either a container + * ({@link PluginCustomization} or {@link DirectoryCustomization}) whose + * leaf customizations live in its + * {@link ContainerCustomizationBase.children | `children`} array, or a + * bare {@link McpServerCustomization} surfaced directly by the host. * * @category Customization Types */ -export type Customization = PluginCustomization | DirectoryCustomization; +export type Customization = + | PluginCustomization + | DirectoryCustomization + | McpServerCustomization; + + +// ─── MCP Server State ──────────────────────────────────────────────────────── + +/** + * Discriminant for the {@link McpServerStatus} union. + * + * @category MCP Server State + */ +export const enum McpServerStatusKind { + /** Server has been registered but is not yet running. */ + Starting = 'starting', + /** Server is running and serving requests. */ + Ready = 'ready', + /** + * Server is reachable but requires additional authentication before it + * can start, or before it can serve a particular request. Carries the + * RFC 9728 Protected Resource Metadata the client needs to obtain a + * token; the client then pushes the token via the existing + * `authenticate` command. + */ + AuthRequired = 'authRequired', + /** Server failed to start, crashed, or otherwise transitioned to a fatal error. */ + Error = 'error', + /** Server has been shut down. */ + Stopped = 'stopped', +} + +/** + * Why an MCP server is currently in the {@link McpServerStatusKind.AuthRequired} + * state. Mirrors the three failure modes defined by the + * [MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization.md). + * + * @category MCP Server State + */ +export const enum McpAuthRequiredReason { + /** No token has been provided yet (HTTP 401, no prior token). */ + Required = 'required', + /** A previously valid token expired or was revoked (HTTP 401). */ + Expired = 'expired', + /** + * Step-up auth: a token is present but its scopes are insufficient for + * the requested operation (HTTP 403 with + * `WWW-Authenticate: Bearer error="insufficient_scope"`). + * + * Unlike {@link Required} and {@link Expired} — which typically surface + * before any tool work is in flight — `InsufficientScope` is almost + * always triggered by an MCP request issued mid-turn (a `tools/call`, + * `resources/read`, etc.). The host SHOULD pair the + * {@link McpServerStatusAuthRequired} transition with + * {@link SessionStatus.InputNeeded} on + * {@link SessionSummary.status | the session} so the activity becomes + * visible at the session-summary level, and clients SHOULD watch for + * this kind on any + * {@link McpServerCustomization | MCP server} backing a running tool + * call so they can present an explicit "grant more access" affordance + * tied to the blocked tool call. + */ + InsufficientScope = 'insufficientScope', +} + +/** + * Server is registered with the host but has not yet started. + * + * @category MCP Server State + */ +export interface McpServerStatusStarting { + kind: McpServerStatusKind.Starting; +} + +/** + * Server is running and serving requests. + * + * @category MCP Server State + */ +export interface McpServerStatusReady { + kind: McpServerStatusKind.Ready; +} + +/** + * Server is reachable but cannot serve requests until the client + * authenticates. Mirrors the discovery flow defined by + * [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) + * (Protected Resource Metadata) and the OAuth 2.1 / RFC 6750 challenge + * semantics required by the MCP authorization spec. + * + * Clients react to this state by calling the existing `authenticate` + * command with the {@link ProtectedResourceMetadata.resource | resource} + * carried here. There is **no** `notify/authRequired` notification for + * MCP servers — the action stream is the single source of truth. + * + * When the transition is triggered by a request issued during a turn + * — most commonly + * {@link McpAuthRequiredReason.InsufficientScope | `InsufficientScope`} + * surfacing mid-tool-call — the host SHOULD also raise + * {@link SessionStatus.InputNeeded} on the session so the block is + * visible at the summary level. Clients SHOULD watch this status on + * any MCP server backing a running tool call and surface an explicit + * affordance (e.g. a "grant additional access" prompt) tied to that + * tool call, rather than relying on the user to notice the + * customization’s status badge. + * + * @category MCP Server State + */ +export interface McpServerStatusAuthRequired { + kind: McpServerStatusKind.AuthRequired; + /** Why authentication is required. */ + reason: McpAuthRequiredReason; + /** + * RFC 9728 Protected Resource Metadata. The `resource` field is the + * canonical MCP server URI per RFC 8707, used as the OAuth `resource` + * indicator. `authorization_servers` is REQUIRED by the MCP + * authorization spec. + */ + resource: ProtectedResourceMetadata; + /** + * Scopes required for the current challenge, parsed from the + * `WWW-Authenticate: Bearer scope="…"` header (or `scopes_supported` + * fallback). Authoritative for the next authorization request — clients + * MUST NOT assume any subset/superset relationship to + * `resource.scopes_supported`. + */ + requiredScopes?: string[]; + /** Human-readable hint, typically from the OAuth `error_description`. */ + description?: string; +} + +/** + * Server failed to start, crashed, or otherwise transitioned to a + * non-recoverable error. Use {@link McpServerStatusKind.AuthRequired} + * for authentication failures. + * + * @category MCP Server State + */ +export interface McpServerStatusError { + kind: McpServerStatusKind.Error; + /** Error details. */ + error: ErrorInfo; +} + +/** + * Server has been shut down. The host MAY remove the server from the + * session entirely shortly after this state. + * + * @category MCP Server State + */ +export interface McpServerStatusStopped { + kind: McpServerStatusKind.Stopped; +} + +/** + * Discriminated union of all MCP server statuses. Discriminated by `kind`. + * + * @category MCP Server State + */ +export type McpServerStatus = + | McpServerStatusStarting + | McpServerStatusReady + | McpServerStatusAuthRequired + | McpServerStatusError + | McpServerStatusStopped; diff --git a/types/common/actions.ts b/types/common/actions.ts index cbf19e7b..79d648f9 100644 --- a/types/common/actions.ts +++ b/types/common/actions.ts @@ -49,6 +49,7 @@ import type { SessionCustomizationToggledAction, SessionCustomizationUpdatedAction, SessionCustomizationRemovedAction, + SessionMcpServerStatusChangedAction, SessionTruncatedAction, SessionIsReadChangedAction, SessionIsArchivedChangedAction, @@ -127,6 +128,7 @@ export const enum ActionType { SessionCustomizationToggled = 'session/customizationToggled', SessionCustomizationUpdated = 'session/customizationUpdated', SessionCustomizationRemoved = 'session/customizationRemoved', + SessionMcpServerStatusChanged = 'session/mcpServerStatusChanged', SessionTruncated = 'session/truncated', SessionIsReadChanged = 'session/isReadChanged', SessionIsArchivedChanged = 'session/isArchivedChanged', @@ -226,6 +228,7 @@ export type StateAction = | SessionCustomizationToggledAction | SessionCustomizationUpdatedAction | SessionCustomizationRemovedAction + | SessionMcpServerStatusChangedAction | SessionTruncatedAction | SessionIsReadChangedAction | SessionIsArchivedChangedAction diff --git a/types/common/commands.ts b/types/common/commands.ts index f8cd5f9a..35e75256 100644 --- a/types/common/commands.ts +++ b/types/common/commands.ts @@ -72,6 +72,40 @@ export interface InitializeParams extends BaseParams { * user-facing strings such as confirmation option labels. */ locale?: string; + /** + * Optional client capability declarations. + * + * Servers SHOULD only advertise features whose corresponding client + * capability is set here. Absent means "not declared" — the server + * MUST assume the client does not support the feature. + */ + capabilities?: ClientCapabilities; +} + +/** + * Optional capabilities a client declares during `initialize`. + * + * Each field is a presence flag: an empty object `{}` means "supported", + * absence means "not supported". Sub-fields on individual capabilities + * are reserved for future per-capability options. + * + * @category Commands + */ +export interface ClientCapabilities { + /** + * Client can render + * [MCP Apps](https://github.com/modelcontextprotocol/ext-apps) — i.e. + * it can host the View sandbox, run the `ui/*` protocol against it, + * and forward `mcp://`-channel traffic on the App's behalf. + * + * Hosts SHOULD only populate + * {@link McpServerCustomization.mcpApp | `McpServerCustomization.mcpApp`} + * (and expose the corresponding + * {@link McpServerCustomization.channel | `mcp://` channel}) when this + * capability is declared. Clients that omit it MUST treat + * App-bearing tool calls as ordinary MCP tool calls. + */ + mcpApps?: Record; } /** diff --git a/types/index.ts b/types/index.ts index 13e1b5ae..f93c4893 100644 --- a/types/index.ts +++ b/types/index.ts @@ -219,6 +219,7 @@ export { export type { InitializeParams, InitializeResult, + ClientCapabilities, PingParams, ReconnectParams, ReconnectReplayResult, diff --git a/types/test-cases/reducers/017-turncomplete-force-cancels-in-progress-tool-calls.json b/types/test-cases/reducers/017-turncomplete-force-cancels-in-progress-tool-calls.json index 6d4b14c2..17e0a28f 100644 --- a/types/test-cases/reducers/017-turncomplete-force-cancels-in-progress-tool-calls.json +++ b/types/test-cases/reducers/017-turncomplete-force-cancels-in-progress-tool-calls.json @@ -64,7 +64,7 @@ "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command", - "toolClientId": null, + "contributor": null, "_meta": null, "invocationMessage": "", "toolInput": null, diff --git a/types/test-cases/reducers/019-tool-call-full-lifecycle-start-delta-ready-confirmed-complete.json b/types/test-cases/reducers/019-tool-call-full-lifecycle-start-delta-ready-confirmed-complete.json index 033d49af..d4ac1ec5 100644 --- a/types/test-cases/reducers/019-tool-call-full-lifecycle-start-delta-ready-confirmed-complete.json +++ b/types/test-cases/reducers/019-tool-call-full-lifecycle-start-delta-ready-confirmed-complete.json @@ -90,7 +90,7 @@ "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command", - "toolClientId": null, + "contributor": null, "_meta": null, "invocationMessage": "Run: ls -la", "toolInput": "ls -la", diff --git a/types/test-cases/reducers/020-tool-call-ready-with-auto-confirm-transitions-to-running.json b/types/test-cases/reducers/020-tool-call-ready-with-auto-confirm-transitions-to-running.json index c788591a..c61b0d5d 100644 --- a/types/test-cases/reducers/020-tool-call-ready-with-auto-confirm-transitions-to-running.json +++ b/types/test-cases/reducers/020-tool-call-ready-with-auto-confirm-transitions-to-running.json @@ -67,7 +67,7 @@ "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command", - "toolClientId": null, + "contributor": null, "_meta": null, "invocationMessage": "Run", "toolInput": null, diff --git a/types/test-cases/reducers/021-tool-call-denied-transitions-to-cancelled.json b/types/test-cases/reducers/021-tool-call-denied-transitions-to-cancelled.json index b1c237e8..d76222d7 100644 --- a/types/test-cases/reducers/021-tool-call-denied-transitions-to-cancelled.json +++ b/types/test-cases/reducers/021-tool-call-denied-transitions-to-cancelled.json @@ -73,7 +73,7 @@ "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command", - "toolClientId": null, + "contributor": null, "_meta": null, "invocationMessage": "Run: rm -rf /", "toolInput": null, diff --git a/types/test-cases/reducers/022-tool-call-result-confirmation-pending-approved.json b/types/test-cases/reducers/022-tool-call-result-confirmation-pending-approved.json index e7bcc69c..238d17dc 100644 --- a/types/test-cases/reducers/022-tool-call-result-confirmation-pending-approved.json +++ b/types/test-cases/reducers/022-tool-call-result-confirmation-pending-approved.json @@ -83,7 +83,7 @@ "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command", - "toolClientId": null, + "contributor": null, "_meta": null, "invocationMessage": "Run", "toolInput": null, diff --git a/types/test-cases/reducers/023-tool-call-result-denied-cancelled-with-result-denied-reason.json b/types/test-cases/reducers/023-tool-call-result-denied-cancelled-with-result-denied-reason.json index 9d0bf10e..3629807a 100644 --- a/types/test-cases/reducers/023-tool-call-result-denied-cancelled-with-result-denied-reason.json +++ b/types/test-cases/reducers/023-tool-call-result-denied-cancelled-with-result-denied-reason.json @@ -83,7 +83,7 @@ "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command", - "toolClientId": null, + "contributor": null, "_meta": null, "invocationMessage": "Run", "toolInput": null, diff --git a/types/test-cases/reducers/024-tool-call-complete-from-pending-confirmation-defaults-confirmed.json b/types/test-cases/reducers/024-tool-call-complete-from-pending-confirmation-defaults-confirmed.json index d64a0670..df5d0904 100644 --- a/types/test-cases/reducers/024-tool-call-complete-from-pending-confirmation-defaults-confirmed.json +++ b/types/test-cases/reducers/024-tool-call-complete-from-pending-confirmation-defaults-confirmed.json @@ -75,7 +75,7 @@ "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command", - "toolClientId": null, + "contributor": null, "_meta": null, "invocationMessage": "Run", "toolInput": null, diff --git a/types/test-cases/reducers/026-toolcallready-transitions-running-tool-back-to-pending-confirmation.json b/types/test-cases/reducers/026-toolcallready-transitions-running-tool-back-to-pending-confirmation.json index f8655c00..b06f89c3 100644 --- a/types/test-cases/reducers/026-toolcallready-transitions-running-tool-back-to-pending-confirmation.json +++ b/types/test-cases/reducers/026-toolcallready-transitions-running-tool-back-to-pending-confirmation.json @@ -77,7 +77,7 @@ "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command", - "toolClientId": null, + "contributor": null, "_meta": null, "invocationMessage": "Run: rm -rf /tmp/test", "toolInput": null, diff --git a/types/test-cases/reducers/027-toolcallready-re-confirmation-approved-transitions-back-to-running.json b/types/test-cases/reducers/027-toolcallready-re-confirmation-approved-transitions-back-to-running.json index 65b41dc0..e0628e8a 100644 --- a/types/test-cases/reducers/027-toolcallready-re-confirmation-approved-transitions-back-to-running.json +++ b/types/test-cases/reducers/027-toolcallready-re-confirmation-approved-transitions-back-to-running.json @@ -80,7 +80,7 @@ "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command", - "toolClientId": null, + "contributor": null, "_meta": null, "invocationMessage": "Permission needed", "toolInput": null, diff --git a/types/test-cases/reducers/028-toolcallready-re-confirmation-denied-transitions-to-cancelled.json b/types/test-cases/reducers/028-toolcallready-re-confirmation-denied-transitions-to-cancelled.json index 879e9d6c..48d208dd 100644 --- a/types/test-cases/reducers/028-toolcallready-re-confirmation-denied-transitions-to-cancelled.json +++ b/types/test-cases/reducers/028-toolcallready-re-confirmation-denied-transitions-to-cancelled.json @@ -80,7 +80,7 @@ "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command", - "toolClientId": null, + "contributor": null, "_meta": null, "invocationMessage": "Permission needed", "toolInput": null, diff --git a/types/test-cases/reducers/029-toolcallready-ignores-non-streaming-non-running-tool-calls.json b/types/test-cases/reducers/029-toolcallready-ignores-non-streaming-non-running-tool-calls.json index 2ea5a345..4f72e4f7 100644 --- a/types/test-cases/reducers/029-toolcallready-ignores-non-streaming-non-running-tool-calls.json +++ b/types/test-cases/reducers/029-toolcallready-ignores-non-streaming-non-running-tool-calls.json @@ -28,7 +28,7 @@ "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command", - "toolClientId": null, + "contributor": null, "_meta": null, "invocationMessage": "Run", "toolInput": null, @@ -74,7 +74,7 @@ "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command", - "toolClientId": null, + "contributor": null, "_meta": null, "invocationMessage": "Run", "toolInput": null, diff --git a/types/test-cases/reducers/070-full-turn-flow-with-tool-calls-and-re-confirmation.json b/types/test-cases/reducers/070-full-turn-flow-with-tool-calls-and-re-confirmation.json index d6e2c44c..66458da7 100644 --- a/types/test-cases/reducers/070-full-turn-flow-with-tool-calls-and-re-confirmation.json +++ b/types/test-cases/reducers/070-full-turn-flow-with-tool-calls-and-re-confirmation.json @@ -177,7 +177,7 @@ "toolCallId": "tc1", "toolName": "edit", "displayName": "Edit File", - "toolClientId": null, + "contributor": null, "_meta": null, "invocationMessage": "Edit main.ts", "toolInput": null, @@ -193,7 +193,7 @@ "toolCallId": "tc2", "toolName": "write", "displayName": "Write File", - "toolClientId": null, + "contributor": null, "_meta": null, "invocationMessage": "Write to /tmp/out", "toolInput": null, diff --git a/types/test-cases/reducers/099-endturn-force-cancels-running-tool-call.json b/types/test-cases/reducers/099-endturn-force-cancels-running-tool-call.json index 6fc1b1f4..9cfceeb6 100644 --- a/types/test-cases/reducers/099-endturn-force-cancels-running-tool-call.json +++ b/types/test-cases/reducers/099-endturn-force-cancels-running-tool-call.json @@ -70,7 +70,7 @@ "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command", - "toolClientId": null, + "contributor": null, "_meta": null, "invocationMessage": "Running command", "toolInput": "ls -la", diff --git a/types/test-cases/reducers/111-toolcall-pending-confirmation-sets-input-needed-status.json b/types/test-cases/reducers/111-toolcall-pending-confirmation-sets-input-needed-status.json index 4c1245fd..85cc1863 100644 --- a/types/test-cases/reducers/111-toolcall-pending-confirmation-sets-input-needed-status.json +++ b/types/test-cases/reducers/111-toolcall-pending-confirmation-sets-input-needed-status.json @@ -67,7 +67,7 @@ "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command", - "toolClientId": null, + "contributor": null, "_meta": null, "invocationMessage": "Run: rm -rf /tmp/test", "toolInput": "rm -rf /tmp/test", diff --git a/types/test-cases/reducers/112-toolcall-pending-result-confirmation-sets-input-needed-status.json b/types/test-cases/reducers/112-toolcall-pending-result-confirmation-sets-input-needed-status.json index bda016d9..375f7512 100644 --- a/types/test-cases/reducers/112-toolcall-pending-result-confirmation-sets-input-needed-status.json +++ b/types/test-cases/reducers/112-toolcall-pending-result-confirmation-sets-input-needed-status.json @@ -77,7 +77,7 @@ "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command", - "toolClientId": null, + "contributor": null, "_meta": null, "invocationMessage": "Run", "toolInput": null, diff --git a/types/test-cases/reducers/127-toolcallready-with-confirmation-options.json b/types/test-cases/reducers/127-toolcallready-with-confirmation-options.json index eef16159..06aadb19 100644 --- a/types/test-cases/reducers/127-toolcallready-with-confirmation-options.json +++ b/types/test-cases/reducers/127-toolcallready-with-confirmation-options.json @@ -92,7 +92,7 @@ "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command", - "toolClientId": null, + "contributor": null, "_meta": null, "invocationMessage": "Run: echo hello", "toolInput": null, diff --git a/types/test-cases/reducers/128-toolcallconfirmed-approved-with-selectedoption.json b/types/test-cases/reducers/128-toolcallconfirmed-approved-with-selectedoption.json index 519b3610..759ebd19 100644 --- a/types/test-cases/reducers/128-toolcallconfirmed-approved-with-selectedoption.json +++ b/types/test-cases/reducers/128-toolcallconfirmed-approved-with-selectedoption.json @@ -94,7 +94,7 @@ "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command", - "toolClientId": null, + "contributor": null, "_meta": null, "invocationMessage": "Run: echo hello", "toolInput": null, diff --git a/types/test-cases/reducers/129-toolcallconfirmed-denied-with-selectedoption.json b/types/test-cases/reducers/129-toolcallconfirmed-denied-with-selectedoption.json index d5c683e1..91d9eb76 100644 --- a/types/test-cases/reducers/129-toolcallconfirmed-denied-with-selectedoption.json +++ b/types/test-cases/reducers/129-toolcallconfirmed-denied-with-selectedoption.json @@ -95,7 +95,7 @@ "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command", - "toolClientId": null, + "contributor": null, "_meta": null, "invocationMessage": "Run: rm -rf /", "toolInput": null, diff --git a/types/test-cases/reducers/130-selectedoption-carries-through-to-completed.json b/types/test-cases/reducers/130-selectedoption-carries-through-to-completed.json index 323381c7..e685e0ba 100644 --- a/types/test-cases/reducers/130-selectedoption-carries-through-to-completed.json +++ b/types/test-cases/reducers/130-selectedoption-carries-through-to-completed.json @@ -97,7 +97,7 @@ "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command", - "toolClientId": null, + "contributor": null, "_meta": null, "invocationMessage": "Run: echo hello", "toolInput": null, diff --git a/types/test-cases/reducers/131-selectedoption-carries-through-result-confirmation.json b/types/test-cases/reducers/131-selectedoption-carries-through-result-confirmation.json index 8670f458..cfeee2f1 100644 --- a/types/test-cases/reducers/131-selectedoption-carries-through-result-confirmation.json +++ b/types/test-cases/reducers/131-selectedoption-carries-through-result-confirmation.json @@ -98,7 +98,7 @@ "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command", - "toolClientId": null, + "contributor": null, "_meta": null, "invocationMessage": "Run: echo hello", "toolInput": null, diff --git a/types/test-cases/reducers/132-selectedoption-carries-through-result-denied.json b/types/test-cases/reducers/132-selectedoption-carries-through-result-denied.json index 500c3348..7c7d9351 100644 --- a/types/test-cases/reducers/132-selectedoption-carries-through-result-denied.json +++ b/types/test-cases/reducers/132-selectedoption-carries-through-result-denied.json @@ -98,7 +98,7 @@ "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command", - "toolClientId": null, + "contributor": null, "_meta": null, "invocationMessage": "Run: echo hello", "toolInput": null, diff --git a/types/version/registry.ts b/types/version/registry.ts index f2cfaa21..22fa43b1 100644 --- a/types/version/registry.ts +++ b/types/version/registry.ts @@ -110,6 +110,7 @@ export const ACTION_INTRODUCED_IN: { readonly [K in StateAction['type']]: string [ActionType.SessionCustomizationToggled]: '0.1.0', [ActionType.SessionCustomizationUpdated]: '0.1.0', [ActionType.SessionCustomizationRemoved]: '0.2.0', + [ActionType.SessionMcpServerStatusChanged]: '0.3.0', [ActionType.SessionTruncated]: '0.1.0', [ActionType.SessionIsReadChanged]: '0.1.0', [ActionType.SessionIsArchivedChanged]: '0.1.0', From 97fe8d959fdef8b9e2ea3083d7127e5e3f9958fc Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 2 Jun 2026 21:42:43 -0700 Subject: [PATCH 2/4] finalize --- .../general-instructions.instructions.md | 4 + CHANGELOG.md | 10 +- clients/go/CHANGELOG.md | 27 + clients/go/ahp/reducers.go | 98 +++- clients/go/ahptypes/actions.generated.go | 54 +- clients/go/ahptypes/commands.generated.go | 26 + clients/go/ahptypes/state.generated.go | 500 +++++++++++++++--- clients/kotlin/CHANGELOG.md | 26 + .../generated/Actions.generated.kt | 30 +- .../generated/Commands.generated.kt | 28 +- .../generated/State.generated.kt | 464 +++++++++++++--- clients/rust/CHANGELOG.md | 27 + clients/rust/crates/ahp-types/src/actions.rs | 61 ++- clients/rust/crates/ahp-types/src/commands.rs | 30 ++ clients/rust/crates/ahp-types/src/state.rs | 411 +++++++++++--- clients/rust/crates/ahp/src/client.rs | 1 + clients/rust/crates/ahp/src/reducers.rs | 90 +++- .../Generated/Actions.generated.swift | 41 +- .../Generated/Commands.generated.swift | 31 +- .../Generated/State.generated.swift | 468 ++++++++++++---- .../AgentHostProtocol/NativeReducer.swift | 51 +- .../Sources/AgentHostProtocol/Reducers.swift | 50 +- .../ToolCallStateExtensions.swift | 14 +- clients/swift/CHANGELOG.md | 28 + clients/typescript/CHANGELOG.md | 27 + docs/guide/customizations.md | 4 +- docs/guide/mcp.md | 18 +- docs/specification/mcp-channel.md | 2 +- schema/actions.schema.json | 64 +-- schema/commands.schema.json | 63 ++- schema/errors.schema.json | 47 +- schema/notifications.schema.json | 32 +- schema/state.schema.json | 46 +- scripts/generate-go.ts | 52 +- scripts/generate-kotlin.ts | 46 +- scripts/generate-rust.ts | 54 +- scripts/generate-swift.ts | 46 +- types/action-origin.generated.ts | 8 +- types/channels-session/actions.ts | 18 +- types/channels-session/reducer.ts | 6 +- types/channels-session/state.ts | 51 +- types/common/actions.ts | 6 +- types/version/registry.ts | 2 +- 43 files changed, 2554 insertions(+), 608 deletions(-) diff --git a/.github/instructions/general-instructions.instructions.md b/.github/instructions/general-instructions.instructions.md index b81ff60f..35ba2c19 100644 --- a/.github/instructions/general-instructions.instructions.md +++ b/.github/instructions/general-instructions.instructions.md @@ -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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 53fb91df..0809216d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,21 +30,21 @@ Spec version: `0.3.0` ### Added - `McpServerCustomization` now models MCP servers as first-class session - customizations: `enabled`, `runtimeStatus` (a discriminated - `McpServerStatus` union covering `starting`, `ready`, `authRequired`, + 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). -- `McpServerStatusAuthRequired` carries `ProtectedResourceMetadata` plus +- `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/mcpServerStatusChanged` action — narrow upsert of - `runtimeStatus` + `channel` on an existing `McpServerCustomization` +- 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. diff --git a/clients/go/CHANGELOG.md b/clients/go/CHANGELOG.md index 76f016f7..fc979d02 100644 --- a/clients/go/CHANGELOG.md +++ b/clients/go/CHANGELOG.md @@ -14,6 +14,33 @@ tag whose matching `## [X.Y.Z]` heading is missing from this file. ## [Unreleased] +### 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`. + +### 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`. diff --git a/clients/go/ahp/reducers.go b/clients/go/ahp/reducers.go index 457d4cd0..570f5aad 100644 --- a/clients/go/ahp/reducers.go +++ b/clients/go/ahp/reducers.go @@ -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{} } @@ -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, @@ -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 } @@ -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 } } @@ -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 @@ -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: @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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{} diff --git a/clients/go/ahptypes/actions.generated.go b/clients/go/ahptypes/actions.generated.go index f1e9c43e..cdbf8e85 100644 --- a/clients/go/ahptypes/actions.generated.go +++ b/clients/go/ahptypes/actions.generated.go @@ -54,6 +54,7 @@ const ( ActionTypeSessionCustomizationToggled ActionType = "session/customizationToggled" ActionTypeSessionCustomizationUpdated ActionType = "session/customizationUpdated" ActionTypeSessionCustomizationRemoved ActionType = "session/customizationRemoved" + ActionTypeSessionMcpServerStateChanged ActionType = "session/mcpServerStateChanged" ActionTypeSessionTruncated ActionType = "session/truncated" ActionTypeSessionIsReadChanged ActionType = "session/isReadChanged" ActionTypeSessionIsArchivedChanged ActionType = "session/isArchivedChanged" @@ -179,9 +180,11 @@ type SessionResponsePartAction struct { // A tool call begins — parameters are streaming from the LM. // -// For client-provided tools, the server sets `toolClientId` to identify the -// owning client. That client is responsible for executing the tool once it -// reaches the `running` state and dispatching `session/toolCallComplete`. +// The server sets {@link ToolCallContributor | `contributor`} to identify +// the origin of the tool. For client-provided tools, the named client is +// responsible for executing the tool once it reaches the `running` state +// and dispatching `session/toolCallComplete`. For MCP-served tools, the +// server executes the call against the named `McpServerCustomization`. type SessionToolCallStartAction struct { // Turn identifier TurnId string `json:"turnId"` @@ -199,9 +202,9 @@ type SessionToolCallStartAction struct { ToolName string `json:"toolName"` // Human-readable tool name DisplayName string `json:"displayName"` - // If this tool is provided by a client, the `clientId` of the owning client. - // Absent for server-side tools. - ToolClientId *string `json:"toolClientId,omitempty"` + // Reference to the contributor of the tool being called. Absent for + // server-side tools that are not contributed by a client or MCP server. + Contributor *ToolCallContributor `json:"contributor,omitempty"` } // Streaming partial parameters for a tool call. @@ -620,6 +623,38 @@ type SessionCustomizationRemovedAction struct { Id string `json:"id"` } +// Updates the runtime fields of an existing +// {@link McpServerCustomization} — narrow alternative to +// {@link SessionCustomizationUpdatedAction} for the high-frequency +// `starting` ↔ `ready` ↔ `authRequired` transitions. +// +// Locates the target entry by `id`, searching both the top-level +// customization list and the `children` array of every container. +// Replaces the entry's {@link McpServerCustomization.state | `state`} +// and {@link McpServerCustomization.channel | `channel`} +// (full-replacement semantics: omit `channel` to clear an existing +// channel URI). Other fields of the customization are preserved. +// +// Is a no-op when no matching `McpServerCustomization` is found. To +// update any other field (name, icons, `mcpApp` capabilities, etc.) use +// {@link SessionCustomizationUpdatedAction} instead. +// +// When the transition is to {@link McpServerStatus.AuthRequired} +// because of a request issued mid-turn, the host SHOULD also raise +// {@link SessionStatus.InputNeeded} on the session — see +// {@link McpServerAuthRequiredState} for the rationale. +type SessionMcpServerStateChangedAction struct { + Type ActionType `json:"type"` + // The id of the {@link McpServerCustomization} to update. + Id string `json:"id"` + // The new lifecycle state. + State McpServerState `json:"state"` + // Updated `mcp://` side-channel URI. Full-replacement: omit to clear + // an existing channel (typical when leaving + // {@link McpServerStatus.Ready | `Ready`}). + Channel *URI `json:"channel,omitempty"` +} + // Truncates a session's history. If `turnId` is provided, all turns after that // turn are removed and the specified turn is kept. If `turnId` is omitted, all // turns are removed. @@ -934,6 +969,7 @@ func (*SessionCustomizationsChangedAction) isStateAction() {} func (*SessionCustomizationToggledAction) isStateAction() {} func (*SessionCustomizationUpdatedAction) isStateAction() {} func (*SessionCustomizationRemovedAction) isStateAction() {} +func (*SessionMcpServerStateChangedAction) isStateAction() {} func (*SessionTruncatedAction) isStateAction() {} func (*SessionConfigChangedAction) isStateAction() {} func (*SessionMetaChangedAction) isStateAction() {} @@ -1205,6 +1241,12 @@ func (u *StateAction) UnmarshalJSON(data []byte) error { return err } u.Value = &value + case "session/mcpServerStateChanged": + var value SessionMcpServerStateChangedAction + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value case "session/truncated": var value SessionTruncatedAction if err := json.Unmarshal(data, &value); err != nil { diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index 49a49c6c..a48150c5 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -100,6 +100,12 @@ type InitializeParams struct { // (e.g. `"en-US"`, `"ja"`). The server SHOULD use this to localise // user-facing strings such as confirmation option labels. Locale *string `json:"locale,omitempty"` + // Optional client capability declarations. + // + // Servers SHOULD only advertise features whose corresponding client + // capability is set here. Absent means "not declared" — the server + // MUST assume the client does not support the feature. + Capabilities *ClientCapabilities `json:"capabilities,omitempty"` } // Result of the `initialize` command. @@ -133,6 +139,26 @@ type InitializeResult struct { Telemetry *TelemetryCapabilities `json:"telemetry,omitempty"` } +// Optional capabilities a client declares during `initialize`. +// +// Each field is a presence flag: an empty object `{}` means "supported", +// absence means "not supported". Sub-fields on individual capabilities +// are reserved for future per-capability options. +type ClientCapabilities struct { + // Client can render + // [MCP Apps](https://github.com/modelcontextprotocol/ext-apps) — i.e. + // it can host the View sandbox, run the `ui/*` protocol against it, + // and forward `mcp://`-channel traffic on the App's behalf. + // + // Hosts SHOULD only populate + // {@link McpServerCustomization.mcpApp | `McpServerCustomization.mcpApp`} + // (and expose the corresponding + // {@link McpServerCustomization.channel | `mcp://` channel}) when this + // capability is declared. Clients that omit it MUST treat + // App-bearing tool calls as ordinary MCP tool calls. + McpApps map[string]json.RawMessage `json:"mcpApps,omitempty"` +} + // Re-establishes a dropped connection. The server replays missed actions or // provides fresh snapshots. type ReconnectParams struct { diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index a65a5135..d7be9feb 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -186,6 +186,13 @@ const ( ConfirmationOptionKindDeny ConfirmationOptionKind = "deny" ) +type ToolCallContributorKind string + +const ( + ToolCallContributorKindClient ToolCallContributorKind = "client" + ToolCallContributorKindMCP ToolCallContributorKind = "mcp" +) + // Discriminant for tool result content types. type ToolResultContentType string @@ -201,10 +208,12 @@ const ( // Discriminant for the kind of customization. // // Top-level entries in {@link SessionState.customizations} and -// {@link AgentInfo.customizations} are always -// {@link CustomizationType.Plugin | `Plugin`} or -// {@link CustomizationType.Directory | `Directory`}; the remaining -// types appear only as children of those containers. +// {@link AgentInfo.customizations} are either container customizations +// ({@link CustomizationType.Plugin | `Plugin`} or +// {@link CustomizationType.Directory | `Directory`}) or +// {@link CustomizationType.McpServer | `McpServer`} entries surfaced +// directly by the host. The remaining types appear only as children of +// a container. type CustomizationType string const ( @@ -236,6 +245,55 @@ const ( TerminalClaimKindSession TerminalClaimKind = "session" ) +// Discriminant for the {@link McpServerState} union. +type McpServerStatus string + +const ( + // Server has been registered but is not yet running. + McpServerStatusStarting McpServerStatus = "starting" + // Server is running and serving requests. + McpServerStatusReady McpServerStatus = "ready" + // Server is reachable but requires additional authentication before it + // can start, or before it can serve a particular request. Carries the + // RFC 9728 Protected Resource Metadata the client needs to obtain a + // token; the client then pushes the token via the existing + // `authenticate` command. + McpServerStatusAuthRequired McpServerStatus = "authRequired" + // Server failed to start, crashed, or otherwise transitioned to a fatal error. + McpServerStatusError McpServerStatus = "error" + // Server has been shut down. + McpServerStatusStopped McpServerStatus = "stopped" +) + +// Why an MCP server is currently in the {@link McpServerStatus.AuthRequired} +// state. Mirrors the three failure modes defined by the +// [MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization.md). +type McpAuthRequiredReason string + +const ( + // No token has been provided yet (HTTP 401, no prior token). + McpAuthRequiredReasonRequired McpAuthRequiredReason = "required" + // A previously valid token expired or was revoked (HTTP 401). + McpAuthRequiredReasonExpired McpAuthRequiredReason = "expired" + // Step-up auth: a token is present but its scopes are insufficient for + // the requested operation (HTTP 403 with + // `WWW-Authenticate: Bearer error="insufficient_scope"`). + // + // Unlike {@link Required} and {@link Expired} — which typically surface + // before any tool work is in flight — `InsufficientScope` is almost + // always triggered by an MCP request issued mid-turn (a `tools/call`, + // `resources/read`, etc.). The host SHOULD pair the + // {@link McpServerAuthRequiredState} transition with + // {@link SessionStatus.InputNeeded} on + // {@link SessionSummary.status | the session} so the activity becomes + // visible at the session-summary level, and clients SHOULD watch for + // this kind on any + // {@link McpServerCustomization | MCP server} backing a running tool + // call so they can present an explicit "grant more access" affordance + // tied to the blocked tool call. + McpAuthRequiredReasonInsufficientScope McpAuthRequiredReason = "insufficientScope" +) + // Computation lifecycle of a {@link ChangesetState}. type ChangesetStatus string @@ -384,13 +442,15 @@ type AgentInfo struct { ProtectedResources []ProtectedResourceMetadata `json:"protectedResources,omitempty"` // Customizations associated with this agent. // - // Always container customizations — + // Either container customizations — // {@link PluginCustomization | `PluginCustomization`} entries the agent // bundles, plus {@link DirectoryCustomization | `DirectoryCustomization`} - // entries it watches in any workspace it's used with. When a session is - // created with this agent, these entries are augmented (e.g. directory - // URIs are resolved against the workspace, children are parsed) and - // propagated into the session's `customizations` list. + // entries it watches in any workspace it's used with — or top-level + // {@link McpServerCustomization | `McpServerCustomization`} entries + // the agent host declares directly. When a session is created with + // this agent, these entries are augmented (e.g. directory URIs are + // resolved against the workspace, children are parsed) and propagated + // into the session's `customizations` list. Customizations []Customization `json:"customizations,omitempty"` } @@ -526,15 +586,23 @@ type SessionState struct { Config *SessionConfigState `json:"config,omitempty"` // Top-level customizations active in this session. // - // Always container customizations — {@link PluginCustomization} or - // {@link DirectoryCustomization}. Children (agents, skills, prompts, - // rules, hooks, MCP servers) live in each container's - // {@link ContainerCustomizationBase.children | `children`} array. + // Always one of the {@link Customization} variants: + // + // - Container customizations ({@link PluginCustomization}, + // {@link DirectoryCustomization}) whose children — agents, skills, + // prompts, rules, hooks, MCP servers — live in each container's + // {@link ContainerCustomizationBase.children | `children`} array. + // - Top-level {@link McpServerCustomization} entries the host + // surfaces directly (for example a globally-configured MCP server + // that isn't bundled in a plugin or directory). MCP servers may + // also appear as children of a container. // // Client-published plugins arrive via // {@link SessionActiveClient.customizations | `activeClient.customizations`} // and the host propagates them into this list (typically with the - // container's `clientId` set and `children` populated). + // container's `clientId` set and `children` populated). Clients + // publish in container shape only; bare MCP servers at the top level + // are server-originated. Customizations []Customization `json:"customizations,omitempty"` // Additional provider-specific metadata for this session. // @@ -1141,18 +1209,13 @@ type ToolCallStreamingState struct { ToolName string `json:"toolName"` // Human-readable tool name DisplayName string `json:"displayName"` - // If this tool is provided by a client, the `clientId` of the owning client. - // Absent for server-side tools. - // - // When set, the identified client is responsible for executing the tool and - // dispatching `session/toolCallComplete` with the result. - ToolClientId *string `json:"toolClientId,omitempty"` + // Reference to the contributor of the tool being called. + Contributor *ToolCallContributor `json:"contributor,omitempty"` // Additional provider-specific metadata for this tool call. // - // Clients MAY look for well-known keys here to provide enhanced UI. - // For example, a `ptyTerminal` key with `{ input: string; output: string }` - // indicates the tool operated on a terminal (both `input` and `output` may - // contain escape sequences). + // This MAY include a `ui` field corresponding to the MCP Apps (SEP-1865) + // `McpUiToolMeta` found in MCP tool calls, which may be used in combination + // with the {@link contributor} to serve MCP Apps. Meta map[string]json.RawMessage `json:"_meta,omitempty"` Status ToolCallStatus `json:"status"` // Partial parameters accumulated so far @@ -1170,18 +1233,13 @@ type ToolCallPendingConfirmationState struct { ToolName string `json:"toolName"` // Human-readable tool name DisplayName string `json:"displayName"` - // If this tool is provided by a client, the `clientId` of the owning client. - // Absent for server-side tools. - // - // When set, the identified client is responsible for executing the tool and - // dispatching `session/toolCallComplete` with the result. - ToolClientId *string `json:"toolClientId,omitempty"` + // Reference to the contributor of the tool being called. + Contributor *ToolCallContributor `json:"contributor,omitempty"` // Additional provider-specific metadata for this tool call. // - // Clients MAY look for well-known keys here to provide enhanced UI. - // For example, a `ptyTerminal` key with `{ input: string; output: string }` - // indicates the tool operated on a terminal (both `input` and `output` may - // contain escape sequences). + // This MAY include a `ui` field corresponding to the MCP Apps (SEP-1865) + // `McpUiToolMeta` found in MCP tool calls, which may be used in combination + // with the {@link contributor} to serve MCP Apps. Meta map[string]json.RawMessage `json:"_meta,omitempty"` // Message describing what the tool will do InvocationMessage StringOrMarkdown `json:"invocationMessage"` @@ -1209,18 +1267,13 @@ type ToolCallRunningState struct { ToolName string `json:"toolName"` // Human-readable tool name DisplayName string `json:"displayName"` - // If this tool is provided by a client, the `clientId` of the owning client. - // Absent for server-side tools. - // - // When set, the identified client is responsible for executing the tool and - // dispatching `session/toolCallComplete` with the result. - ToolClientId *string `json:"toolClientId,omitempty"` + // Reference to the contributor of the tool being called. + Contributor *ToolCallContributor `json:"contributor,omitempty"` // Additional provider-specific metadata for this tool call. // - // Clients MAY look for well-known keys here to provide enhanced UI. - // For example, a `ptyTerminal` key with `{ input: string; output: string }` - // indicates the tool operated on a terminal (both `input` and `output` may - // contain escape sequences). + // This MAY include a `ui` field corresponding to the MCP Apps (SEP-1865) + // `McpUiToolMeta` found in MCP tool calls, which may be used in combination + // with the {@link contributor} to serve MCP Apps. Meta map[string]json.RawMessage `json:"_meta,omitempty"` // Message describing what the tool will do InvocationMessage StringOrMarkdown `json:"invocationMessage"` @@ -1246,18 +1299,13 @@ type ToolCallPendingResultConfirmationState struct { ToolName string `json:"toolName"` // Human-readable tool name DisplayName string `json:"displayName"` - // If this tool is provided by a client, the `clientId` of the owning client. - // Absent for server-side tools. - // - // When set, the identified client is responsible for executing the tool and - // dispatching `session/toolCallComplete` with the result. - ToolClientId *string `json:"toolClientId,omitempty"` + // Reference to the contributor of the tool being called. + Contributor *ToolCallContributor `json:"contributor,omitempty"` // Additional provider-specific metadata for this tool call. // - // Clients MAY look for well-known keys here to provide enhanced UI. - // For example, a `ptyTerminal` key with `{ input: string; output: string }` - // indicates the tool operated on a terminal (both `input` and `output` may - // contain escape sequences). + // This MAY include a `ui` field corresponding to the MCP Apps (SEP-1865) + // `McpUiToolMeta` found in MCP tool calls, which may be used in combination + // with the {@link contributor} to serve MCP Apps. Meta map[string]json.RawMessage `json:"_meta,omitempty"` // Message describing what the tool will do InvocationMessage StringOrMarkdown `json:"invocationMessage"` @@ -1292,18 +1340,13 @@ type ToolCallCompletedState struct { ToolName string `json:"toolName"` // Human-readable tool name DisplayName string `json:"displayName"` - // If this tool is provided by a client, the `clientId` of the owning client. - // Absent for server-side tools. - // - // When set, the identified client is responsible for executing the tool and - // dispatching `session/toolCallComplete` with the result. - ToolClientId *string `json:"toolClientId,omitempty"` + // Reference to the contributor of the tool being called. + Contributor *ToolCallContributor `json:"contributor,omitempty"` // Additional provider-specific metadata for this tool call. // - // Clients MAY look for well-known keys here to provide enhanced UI. - // For example, a `ptyTerminal` key with `{ input: string; output: string }` - // indicates the tool operated on a terminal (both `input` and `output` may - // contain escape sequences). + // This MAY include a `ui` field corresponding to the MCP Apps (SEP-1865) + // `McpUiToolMeta` found in MCP tool calls, which may be used in combination + // with the {@link contributor} to serve MCP Apps. Meta map[string]json.RawMessage `json:"_meta,omitempty"` // Message describing what the tool will do InvocationMessage StringOrMarkdown `json:"invocationMessage"` @@ -1338,18 +1381,13 @@ type ToolCallCancelledState struct { ToolName string `json:"toolName"` // Human-readable tool name DisplayName string `json:"displayName"` - // If this tool is provided by a client, the `clientId` of the owning client. - // Absent for server-side tools. - // - // When set, the identified client is responsible for executing the tool and - // dispatching `session/toolCallComplete` with the result. - ToolClientId *string `json:"toolClientId,omitempty"` + // Reference to the contributor of the tool being called. + Contributor *ToolCallContributor `json:"contributor,omitempty"` // Additional provider-specific metadata for this tool call. // - // Clients MAY look for well-known keys here to provide enhanced UI. - // For example, a `ptyTerminal` key with `{ input: string; output: string }` - // indicates the tool operated on a terminal (both `input` and `output` may - // contain escape sequences). + // This MAY include a `ui` field corresponding to the MCP Apps (SEP-1865) + // `McpUiToolMeta` found in MCP tool calls, which may be used in combination + // with the {@link contributor} to serve MCP Apps. Meta map[string]json.RawMessage `json:"_meta,omitempty"` // Message describing what the tool will do InvocationMessage StringOrMarkdown `json:"invocationMessage"` @@ -1814,12 +1852,14 @@ type HookCustomization struct { Type CustomizationType `json:"type"` } -// An MCP manifest contributed by a plugin or directory. +// An MCP server contributed by a plugin or directory. // // When the server is declared inline in the containing plugin manifest, // `uri` points at the manifest file and // {@link CustomizationBase.range | `range`} narrows it to the // declaration's span. +// +// The MCP server customization also reflects its current status. type McpServerCustomization struct { // Session-unique opaque identifier. Used by every action that targets a // specific customization. Minted by whoever publishes the customization @@ -1843,6 +1883,166 @@ type McpServerCustomization struct { // Absent when the customization covers the whole resource. Range *TextRange `json:"range,omitempty"` Type CustomizationType `json:"type"` + // Whether this MCP server is currently enabled. + Enabled bool `json:"enabled"` + // Current lifecycle state of the MCP server. + State McpServerState `json:"state"` + // An `mcp://`-protocol channel the client uses to side-channel traffic + // into the upstream MCP server itself. The channel is NOT a fresh raw MCP + // connection: it piggybacks on the AHP transport + // and skips the MCP `initialize` sequence. + // + // The agent host MAY only serve a subset of MCP on this + // channel; the served subset is described by domain-specific + // capabilities such as those in + // {@link McpServerCustomizationApps.capabilities}. + // + // The channel URI SHOULD be stable across the server's lifetime, but + // the agent host MAY change it (for example across a restart) and + // MAY only expose it while the server is in + // {@link McpServerStatus.Ready | `Ready`}. Absence means no + // side-channel is currently available. + Channel *URI `json:"channel,omitempty"` + // MCP App support. This property SHOULD be advertised for MCP servers + // which support apps. + McpApp *McpServerCustomizationApps `json:"mcpApp,omitempty"` +} + +// Information from the agent host needed to render MCP Apps served +// by this MCP server. +type McpServerCustomizationApps struct { + // The subset of MCP App + // [`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx) + // the AHP host can satisfy for Views backed by this server. The + // client feeds these straight through into the `hostCapabilities` of + // the `ui/initialize` response delivered to the View. + Capabilities AhpMcpUiHostCapabilities `json:"capabilities"` +} + +// The subset of MCP App +// [`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx) +// an AHP host can derive from the upstream MCP server (and from AHP's own +// forwarding plumbing). Advertised on +// {@link McpServerCustomizationApps.capabilities} so clients can pass it +// through into the `hostCapabilities` of the `ui/initialize` response +// delivered to an MCP App View. +// +// Field names mirror the MCP Apps spec exactly, so the AHP-side producer +// can pass them straight through into the `hostCapabilities` of the +// `ui/initialize` response delivered to the View. +// +// Capabilities outside this set (`openLinks`, `downloadFile`, `sandbox`, +// `experimental`) are decided locally by whichever AHP client renders the +// View and are NOT part of this AHP-level advertisement — only the +// server-derived subset is. +// +// An agent host MUST only advertise a capability when it actually accepts the +// corresponding methods/notifications on the `mcp://` channel: +// +// - {@link serverTools}: host proxies `tools/list` and `tools/call` to +// the MCP server. When `listChanged` is `true`, the host also forwards +// `notifications/tools/list_changed`. +// - {@link serverResources}: host proxies `resources/read`, +// `resources/list`, and `resources/templates/list` to the MCP server. +// When `listChanged` is `true`, the host also forwards +// `notifications/resources/list_changed`. +// - {@link logging}: host accepts `notifications/message` log entries +// from the App and forwards them via `mcpNotification` (and forwards +// `logging/setLevel` calls to the server). +// - {@link sampling}: host serves `sampling/createMessage` via +// `mcpMethodCall`. When `sampling.tools` is present, the host also +// accepts SEP-1577 `tools` / `toolChoice` / `tool_use` content blocks +// inside `CreateMessageRequest`. +type AhpMcpUiHostCapabilities struct { + // Producer proxies the MCP `tools/*` methods to the upstream server. + ServerTools *json.RawMessage `json:"serverTools,omitempty"` + // Producer proxies the MCP `resources/*` methods to the upstream server. + ServerResources *json.RawMessage `json:"serverResources,omitempty"` + // Producer accepts `notifications/message` log entries from the App via `mcpNotification`. + Logging map[string]json.RawMessage `json:"logging,omitempty"` + // Producer serves `sampling/createMessage` via `mcpMethodCall`. + Sampling *json.RawMessage `json:"sampling,omitempty"` +} + +// Server is registered with the host but has not yet started. +type McpServerStartingState struct { + Kind McpServerStatus `json:"kind"` +} + +// Server is running and serving requests. +type McpServerReadyState struct { + Kind McpServerStatus `json:"kind"` +} + +// Server is reachable but cannot serve requests until the client +// authenticates. Mirrors the discovery flow defined by +// [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) +// (Protected Resource Metadata) and the OAuth 2.1 / RFC 6750 challenge +// semantics required by the MCP authorization spec. +// +// Clients react to this state by calling the existing `authenticate` +// command with the {@link ProtectedResourceMetadata.resource | resource} +// carried here. There is **no** `notify/authRequired` notification for +// MCP servers — the action stream is the single source of truth. +// +// When the transition is triggered by a request issued during a turn +// — most commonly +// {@link McpAuthRequiredReason.InsufficientScope | `InsufficientScope`} +// surfacing mid-tool-call — the host SHOULD also raise +// {@link SessionStatus.InputNeeded} on the session so the block is +// visible at the summary level. Clients SHOULD watch this status on +// any MCP server backing a running tool call and surface an explicit +// affordance (e.g. a "grant additional access" prompt) tied to that +// tool call, rather than relying on the user to notice the +// customization’s status badge. +type McpServerAuthRequiredState struct { + Kind McpServerStatus `json:"kind"` + // Why authentication is required. + Reason McpAuthRequiredReason `json:"reason"` + // RFC 9728 Protected Resource Metadata. The `resource` field is the + // canonical MCP server URI per RFC 8707, used as the OAuth `resource` + // indicator. `authorization_servers` is REQUIRED by the MCP + // authorization spec. + Resource ProtectedResourceMetadata `json:"resource"` + // Scopes required for the current challenge, parsed from the + // `WWW-Authenticate: Bearer scope="…"` header (or `scopes_supported` + // fallback). Authoritative for the next authorization request — clients + // MUST NOT assume any subset/superset relationship to + // `resource.scopes_supported`. + RequiredScopes []string `json:"requiredScopes,omitempty"` + // Human-readable hint, typically from the OAuth `error_description`. + Description *string `json:"description,omitempty"` +} + +// Server failed to start, crashed, or otherwise transitioned to a +// non-recoverable error. Use {@link McpServerStatus.AuthRequired} +// for authentication failures. +type McpServerErrorState struct { + Kind McpServerStatus `json:"kind"` + // Error details. + Error ErrorInfo `json:"error"` +} + +// Server has been shut down. The host MAY remove the server from the +// session entirely shortly after this state. +type McpServerStoppedState struct { + Kind McpServerStatus `json:"kind"` +} + +type ToolCallClientContributor struct { + Kind ToolCallContributorKind `json:"kind"` + // If this tool is provided by a client, the `clientId` of the owning client. + // Absent for server-side tools. + // + // When set, the identified client is responsible for executing the tool and + // dispatching `session/toolCallComplete` with the result. + ClientId string `json:"clientId"` +} + +type ToolCallMcpContributor struct { + Kind ToolCallContributorKind `json:"kind"` + // Customization ID of the corresponding MCP server in {@link SessionState.customizations}. + CustomizationId string `json:"customizationId"` } // Describes a file modification with before/after state and diff metadata. @@ -2841,7 +3041,7 @@ func (u MessageAttachment) MarshalJSON() ([]byte, error) { return json.Marshal(u.Value) } -// Customization is a top-level customization (plugin or directory). +// Customization is a top-level customization (plugin, directory, or bare MCP server). type Customization struct { Value isCustomization } @@ -2852,6 +3052,7 @@ type isCustomization interface{ isCustomization() } func (*PluginCustomization) isCustomization() {} func (*DirectoryCustomization) isCustomization() {} +func (*McpServerCustomization) isCustomization() {} // CustomizationUnknown carries an unrecognized Customization variant — typically a discriminator value introduced by a newer protocol version. The original JSON object is preserved verbatim so that re-encoding round-trips faithfully. type CustomizationUnknown struct { @@ -2879,6 +3080,12 @@ func (u *Customization) UnmarshalJSON(data []byte) error { return err } u.Value = &value + case "mcpServer": + var value McpServerCustomization + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value default: raw := make(json.RawMessage, len(data)) copy(raw, data) @@ -3063,6 +3270,147 @@ func (u CustomizationLoadState) MarshalJSON() ([]byte, error) { return json.Marshal(u.Value) } +// McpServerState is the discriminated lifecycle status of an MCP server customization. +type McpServerState struct { + Value isMcpServerState +} + +// isMcpServerState is the marker interface implemented by every +// concrete variant of McpServerState. +type isMcpServerState interface{ isMcpServerState() } + +func (*McpServerStartingState) isMcpServerState() {} +func (*McpServerReadyState) isMcpServerState() {} +func (*McpServerAuthRequiredState) isMcpServerState() {} +func (*McpServerErrorState) isMcpServerState() {} +func (*McpServerStoppedState) isMcpServerState() {} + +// McpServerStateUnknown carries an unrecognized McpServerState variant — typically a discriminator value introduced by a newer protocol version. The original JSON object is preserved verbatim so that re-encoding round-trips faithfully. +type McpServerStateUnknown struct { + Raw json.RawMessage +} + +func (*McpServerStateUnknown) isMcpServerState() {} + +// UnmarshalJSON decodes the variant indicated by the "kind" discriminator. +func (u *McpServerState) UnmarshalJSON(data []byte) error { + disc, _, err := readDiscriminator(data, "kind") + if err != nil { + return err + } + switch disc { + case "starting": + var value McpServerStartingState + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "ready": + var value McpServerReadyState + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "authRequired": + var value McpServerAuthRequiredState + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "error": + var value McpServerErrorState + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "stopped": + var value McpServerStoppedState + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + default: + raw := make(json.RawMessage, len(data)) + copy(raw, data) + u.Value = &McpServerStateUnknown{Raw: raw} + } + return nil +} + +// MarshalJSON encodes the active variant back to JSON. +func (u McpServerState) MarshalJSON() ([]byte, error) { + if unk, ok := u.Value.(*McpServerStateUnknown); ok { + if len(unk.Raw) == 0 { + return []byte("null"), nil + } + return unk.Raw, nil + } + if u.Value == nil { + return []byte("null"), nil + } + return json.Marshal(u.Value) +} + +// ToolCallContributor identifies the contributor (client or MCP server) of a tool call. +type ToolCallContributor struct { + Value isToolCallContributor +} + +// isToolCallContributor is the marker interface implemented by every +// concrete variant of ToolCallContributor. +type isToolCallContributor interface{ isToolCallContributor() } + +func (*ToolCallClientContributor) isToolCallContributor() {} +func (*ToolCallMcpContributor) isToolCallContributor() {} + +// ToolCallContributorUnknown carries an unrecognized ToolCallContributor variant — typically a discriminator value introduced by a newer protocol version. The original JSON object is preserved verbatim so that re-encoding round-trips faithfully. +type ToolCallContributorUnknown struct { + Raw json.RawMessage +} + +func (*ToolCallContributorUnknown) isToolCallContributor() {} + +// UnmarshalJSON decodes the variant indicated by the "kind" discriminator. +func (u *ToolCallContributor) UnmarshalJSON(data []byte) error { + disc, _, err := readDiscriminator(data, "kind") + if err != nil { + return err + } + switch disc { + case "client": + var value ToolCallClientContributor + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "mcp": + var value ToolCallMcpContributor + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + default: + raw := make(json.RawMessage, len(data)) + copy(raw, data) + u.Value = &ToolCallContributorUnknown{Raw: raw} + } + return nil +} + +// MarshalJSON encodes the active variant back to JSON. +func (u ToolCallContributor) MarshalJSON() ([]byte, error) { + if unk, ok := u.Value.(*ToolCallContributorUnknown); ok { + if len(unk.Raw) == 0 { + return []byte("null"), nil + } + return unk.Raw, nil + } + if u.Value == nil { + return []byte("null"), nil + } + return json.Marshal(u.Value) +} + // SnapshotState is the state payload of a snapshot — root, session, // terminal, or changeset state. The active variant is chosen by which // pointer field is non-nil; UnmarshalJSON probes for required fields in diff --git a/clients/kotlin/CHANGELOG.md b/clients/kotlin/CHANGELOG.md index a3d7a1a0..43842199 100644 --- a/clients/kotlin/CHANGELOG.md +++ b/clients/kotlin/CHANGELOG.md @@ -15,6 +15,32 @@ versions (`*-SNAPSHOT`) are explicitly rejected by the publish pipeline; bump ## [Unreleased] +### Added + +- `McpServerCustomization` now exposes the full MCP lifecycle: `enabled`, + the discriminated `McpServerState` sealed interface + (`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.McpServer` top-level variant — hosts MAY surface bare + MCP servers directly rather than only inside a plugin or directory. +- `SessionMcpServerStateChangedAction` — narrow upsert of + `state` + `channel` on an existing MCP server customization + by id. +- `ClientCapabilities` data class on `InitializeParams.capabilities` + with first entry `mcpApps`. + +### Changed + +- `ToolCallBase.toolClientId: String?` replaced by + `ToolCallBase.contributor: ToolCallContributor?` (sealed interface + with `Client(clientId)` and `Mcp(customizationId)` variants). + `SessionToolCallStartAction` carries the new `contributor` field as + well. + ## [0.2.0] — 2026-05-28 Implements AHP `0.2.0`. diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt index b9deb84a..b7cdd821 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt @@ -96,6 +96,8 @@ enum class ActionType { SESSION_CUSTOMIZATION_UPDATED, @SerialName("session/customizationRemoved") SESSION_CUSTOMIZATION_REMOVED, + @SerialName("session/mcpServerStateChanged") + SESSION_MCP_SERVER_STATE_CHANGED, @SerialName("session/truncated") SESSION_TRUNCATED, @SerialName("session/isReadChanged") @@ -281,10 +283,10 @@ data class SessionToolCallStartAction( */ val displayName: String, /** - * If this tool is provided by a client, the `clientId` of the owning client. - * Absent for server-side tools. + * Reference to the contributor of the tool being called. Absent for + * server-side tools that are not contributed by a client or MCP server. */ - val toolClientId: String? = null + val contributor: ToolCallContributor? = null ) @Serializable @@ -733,6 +735,25 @@ data class SessionCustomizationRemovedAction( val id: String ) +@Serializable +data class SessionMcpServerStateChangedAction( + val type: ActionType, + /** + * The id of the {@link McpServerCustomization} to update. + */ + val id: String, + /** + * The new lifecycle state. + */ + val state: McpServerState, + /** + * Updated `mcp://` side-channel URI. Full-replacement: omit to clear + * an existing channel (typical when leaving + * {@link McpServerStatus.Ready | `Ready`}). + */ + val channel: String? = null +) + @Serializable data class SessionTruncatedAction( val type: ActionType, @@ -1034,6 +1055,7 @@ sealed interface StateAction @JvmInline value class StateActionSessionCustomizationToggled(val value: SessionCustomizationToggledAction) : StateAction @JvmInline value class StateActionSessionCustomizationUpdated(val value: SessionCustomizationUpdatedAction) : StateAction @JvmInline value class StateActionSessionCustomizationRemoved(val value: SessionCustomizationRemovedAction) : StateAction +@JvmInline value class StateActionSessionMcpServerStateChanged(val value: SessionMcpServerStateChangedAction) : StateAction @JvmInline value class StateActionSessionTruncated(val value: SessionTruncatedAction) : StateAction @JvmInline value class StateActionSessionConfigChanged(val value: SessionConfigChangedAction) : StateAction @JvmInline value class StateActionSessionMetaChanged(val value: SessionMetaChangedAction) : StateAction @@ -1110,6 +1132,7 @@ internal object StateActionSerializer : KSerializer { "session/customizationToggled" -> StateActionSessionCustomizationToggled(input.json.decodeFromJsonElement(SessionCustomizationToggledAction.serializer(), element)) "session/customizationUpdated" -> StateActionSessionCustomizationUpdated(input.json.decodeFromJsonElement(SessionCustomizationUpdatedAction.serializer(), element)) "session/customizationRemoved" -> StateActionSessionCustomizationRemoved(input.json.decodeFromJsonElement(SessionCustomizationRemovedAction.serializer(), element)) + "session/mcpServerStateChanged" -> StateActionSessionMcpServerStateChanged(input.json.decodeFromJsonElement(SessionMcpServerStateChangedAction.serializer(), element)) "session/truncated" -> StateActionSessionTruncated(input.json.decodeFromJsonElement(SessionTruncatedAction.serializer(), element)) "session/configChanged" -> StateActionSessionConfigChanged(input.json.decodeFromJsonElement(SessionConfigChangedAction.serializer(), element)) "session/metaChanged" -> StateActionSessionMetaChanged(input.json.decodeFromJsonElement(SessionMetaChangedAction.serializer(), element)) @@ -1179,6 +1202,7 @@ internal object StateActionSerializer : KSerializer { is StateActionSessionCustomizationToggled -> output.json.encodeToJsonElement(SessionCustomizationToggledAction.serializer(), value.value) is StateActionSessionCustomizationUpdated -> output.json.encodeToJsonElement(SessionCustomizationUpdatedAction.serializer(), value.value) is StateActionSessionCustomizationRemoved -> output.json.encodeToJsonElement(SessionCustomizationRemovedAction.serializer(), value.value) + is StateActionSessionMcpServerStateChanged -> output.json.encodeToJsonElement(SessionMcpServerStateChangedAction.serializer(), value.value) is StateActionSessionTruncated -> output.json.encodeToJsonElement(SessionTruncatedAction.serializer(), value.value) is StateActionSessionConfigChanged -> output.json.encodeToJsonElement(SessionConfigChangedAction.serializer(), value.value) is StateActionSessionMetaChanged -> output.json.encodeToJsonElement(SessionMetaChangedAction.serializer(), value.value) diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt index b71f2527..76b06987 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt @@ -132,7 +132,15 @@ data class InitializeParams( * (e.g. `"en-US"`, `"ja"`). The server SHOULD use this to localise * user-facing strings such as confirmation option labels. */ - val locale: String? = null + val locale: String? = null, + /** + * Optional client capability declarations. + * + * Servers SHOULD only advertise features whose corresponding client + * capability is set here. Absent means "not declared" — the server + * MUST assume the client does not support the feature. + */ + val capabilities: ClientCapabilities? = null ) @Serializable @@ -172,6 +180,24 @@ data class InitializeResult( val telemetry: TelemetryCapabilities? = null ) +@Serializable +data class ClientCapabilities( + /** + * Client can render + * [MCP Apps](https://github.com/modelcontextprotocol/ext-apps) — i.e. + * it can host the View sandbox, run the `ui/*` protocol against it, + * and forward `mcp://`-channel traffic on the App's behalf. + * + * Hosts SHOULD only populate + * {@link McpServerCustomization.mcpApp | `McpServerCustomization.mcpApp`} + * (and expose the corresponding + * {@link McpServerCustomization.channel | `mcp://` channel}) when this + * capability is declared. Clients that omit it MUST treat + * App-bearing tool calls as ordinary MCP tool calls. + */ + val mcpApps: Map? = null +) + @Serializable data class ReconnectParams( /** diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt index 82848baa..1ef60d50 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt @@ -353,6 +353,14 @@ enum class ConfirmationOptionKind { DENY } +@Serializable +enum class ToolCallContributorKind { + @SerialName("client") + CLIENT, + @SerialName("mcp") + MCP +} + /** * Discriminant for tool result content types. */ @@ -376,10 +384,12 @@ enum class ToolResultContentType { * Discriminant for the kind of customization. * * Top-level entries in {@link SessionState.customizations} and - * {@link AgentInfo.customizations} are always - * {@link CustomizationType.Plugin | `Plugin`} or - * {@link CustomizationType.Directory | `Directory`}; the remaining - * types appear only as children of those containers. + * {@link AgentInfo.customizations} are either container customizations + * ({@link CustomizationType.Plugin | `Plugin`} or + * {@link CustomizationType.Directory | `Directory`}) or + * {@link CustomizationType.McpServer | `McpServer`} entries surfaced + * directly by the host. The remaining types appear only as children of + * a container. */ @Serializable enum class CustomizationType { @@ -427,6 +437,81 @@ enum class TerminalClaimKind { SESSION } +/** + * Discriminant for the {@link McpServerState} union. + */ +@Serializable +enum class McpServerStatus { + /** + * Server has been registered but is not yet running. + */ + @SerialName("starting") + STARTING, + /** + * Server is running and serving requests. + */ + @SerialName("ready") + READY, + /** + * Server is reachable but requires additional authentication before it + * can start, or before it can serve a particular request. Carries the + * RFC 9728 Protected Resource Metadata the client needs to obtain a + * token; the client then pushes the token via the existing + * `authenticate` command. + */ + @SerialName("authRequired") + AUTH_REQUIRED, + /** + * Server failed to start, crashed, or otherwise transitioned to a fatal error. + */ + @SerialName("error") + ERROR, + /** + * Server has been shut down. + */ + @SerialName("stopped") + STOPPED +} + +/** + * Why an MCP server is currently in the {@link McpServerStatus.AuthRequired} + * state. Mirrors the three failure modes defined by the + * [MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization.md). + */ +@Serializable +enum class McpAuthRequiredReason { + /** + * No token has been provided yet (HTTP 401, no prior token). + */ + @SerialName("required") + REQUIRED, + /** + * A previously valid token expired or was revoked (HTTP 401). + */ + @SerialName("expired") + EXPIRED, + /** + * Step-up auth: a token is present but its scopes are insufficient for + * the requested operation (HTTP 403 with + * `WWW-Authenticate: Bearer error="insufficient_scope"`). + * + * Unlike {@link Required} and {@link Expired} — which typically surface + * before any tool work is in flight — `InsufficientScope` is almost + * always triggered by an MCP request issued mid-turn (a `tools/call`, + * `resources/read`, etc.). The host SHOULD pair the + * {@link McpServerAuthRequiredState} transition with + * {@link SessionStatus.InputNeeded} on + * {@link SessionSummary.status | the session} so the activity becomes + * visible at the session-summary level, and clients SHOULD watch for + * this kind on any + * {@link McpServerCustomization | MCP server} backing a running tool + * call so they can present an explicit "grant more access" affordance + * tied to the blocked tool call. + */ + @SerialName("insufficientScope") + INSUFFICIENT_SCOPE +} + /** * Computation lifecycle of a {@link ChangesetState}. */ @@ -661,13 +746,15 @@ data class AgentInfo( /** * Customizations associated with this agent. * - * Always container customizations — + * Either container customizations — * {@link PluginCustomization | `PluginCustomization`} entries the agent * bundles, plus {@link DirectoryCustomization | `DirectoryCustomization`} - * entries it watches in any workspace it's used with. When a session is - * created with this agent, these entries are augmented (e.g. directory - * URIs are resolved against the workspace, children are parsed) and - * propagated into the session's `customizations` list. + * entries it watches in any workspace it's used with — or top-level + * {@link McpServerCustomization | `McpServerCustomization`} entries + * the agent host declares directly. When a session is created with + * this agent, these entries are augmented (e.g. directory URIs are + * resolved against the workspace, children are parsed) and propagated + * into the session's `customizations` list. */ val customizations: List? = null ) @@ -859,15 +946,23 @@ data class SessionState( /** * Top-level customizations active in this session. * - * Always container customizations — {@link PluginCustomization} or - * {@link DirectoryCustomization}. Children (agents, skills, prompts, - * rules, hooks, MCP servers) live in each container's + * Always one of the {@link Customization} variants: + * + * - Container customizations ({@link PluginCustomization}, + * {@link DirectoryCustomization}) whose children — agents, skills, + * prompts, rules, hooks, MCP servers — live in each container's * {@link ContainerCustomizationBase.children | `children`} array. + * - Top-level {@link McpServerCustomization} entries the host + * surfaces directly (for example a globally-configured MCP server + * that isn't bundled in a plugin or directory). MCP servers may + * also appear as children of a container. * * Client-published plugins arrive via * {@link SessionActiveClient.customizations | `activeClient.customizations`} * and the host propagates them into this list (typically with the - * container's `clientId` set and `children` populated). + * container's `clientId` set and `children` populated). Clients + * publish in container shape only; bare MCP servers at the top level + * are server-originated. */ val customizations: List? = null, /** @@ -1665,20 +1760,15 @@ data class ToolCallStreamingState( */ val displayName: String, /** - * If this tool is provided by a client, the `clientId` of the owning client. - * Absent for server-side tools. - * - * When set, the identified client is responsible for executing the tool and - * dispatching `session/toolCallComplete` with the result. + * Reference to the contributor of the tool being called. */ - val toolClientId: String? = null, + val contributor: ToolCallContributor? = null, /** * Additional provider-specific metadata for this tool call. * - * Clients MAY look for well-known keys here to provide enhanced UI. - * For example, a `ptyTerminal` key with `{ input: string; output: string }` - * indicates the tool operated on a terminal (both `input` and `output` may - * contain escape sequences). + * This MAY include a `ui` field corresponding to the MCP Apps (SEP-1865) + * `McpUiToolMeta` found in MCP tool calls, which may be used in combination + * with the {@link contributor} to serve MCP Apps. */ @SerialName("_meta") val meta: Map? = null, @@ -1708,20 +1798,15 @@ data class ToolCallPendingConfirmationState( */ val displayName: String, /** - * If this tool is provided by a client, the `clientId` of the owning client. - * Absent for server-side tools. - * - * When set, the identified client is responsible for executing the tool and - * dispatching `session/toolCallComplete` with the result. + * Reference to the contributor of the tool being called. */ - val toolClientId: String? = null, + val contributor: ToolCallContributor? = null, /** * Additional provider-specific metadata for this tool call. * - * Clients MAY look for well-known keys here to provide enhanced UI. - * For example, a `ptyTerminal` key with `{ input: string; output: string }` - * indicates the tool operated on a terminal (both `input` and `output` may - * contain escape sequences). + * This MAY include a `ui` field corresponding to the MCP Apps (SEP-1865) + * `McpUiToolMeta` found in MCP tool calls, which may be used in combination + * with the {@link contributor} to serve MCP Apps. */ @SerialName("_meta") val meta: Map? = null, @@ -1770,20 +1855,15 @@ data class ToolCallRunningState( */ val displayName: String, /** - * If this tool is provided by a client, the `clientId` of the owning client. - * Absent for server-side tools. - * - * When set, the identified client is responsible for executing the tool and - * dispatching `session/toolCallComplete` with the result. + * Reference to the contributor of the tool being called. */ - val toolClientId: String? = null, + val contributor: ToolCallContributor? = null, /** * Additional provider-specific metadata for this tool call. * - * Clients MAY look for well-known keys here to provide enhanced UI. - * For example, a `ptyTerminal` key with `{ input: string; output: string }` - * indicates the tool operated on a terminal (both `input` and `output` may - * contain escape sequences). + * This MAY include a `ui` field corresponding to the MCP Apps (SEP-1865) + * `McpUiToolMeta` found in MCP tool calls, which may be used in combination + * with the {@link contributor} to serve MCP Apps. */ @SerialName("_meta") val meta: Map? = null, @@ -1828,20 +1908,15 @@ data class ToolCallPendingResultConfirmationState( */ val displayName: String, /** - * If this tool is provided by a client, the `clientId` of the owning client. - * Absent for server-side tools. - * - * When set, the identified client is responsible for executing the tool and - * dispatching `session/toolCallComplete` with the result. + * Reference to the contributor of the tool being called. */ - val toolClientId: String? = null, + val contributor: ToolCallContributor? = null, /** * Additional provider-specific metadata for this tool call. * - * Clients MAY look for well-known keys here to provide enhanced UI. - * For example, a `ptyTerminal` key with `{ input: string; output: string }` - * indicates the tool operated on a terminal (both `input` and `output` may - * contain escape sequences). + * This MAY include a `ui` field corresponding to the MCP Apps (SEP-1865) + * `McpUiToolMeta` found in MCP tool calls, which may be used in combination + * with the {@link contributor} to serve MCP Apps. */ @SerialName("_meta") val meta: Map? = null, @@ -1903,20 +1978,15 @@ data class ToolCallCompletedState( */ val displayName: String, /** - * If this tool is provided by a client, the `clientId` of the owning client. - * Absent for server-side tools. - * - * When set, the identified client is responsible for executing the tool and - * dispatching `session/toolCallComplete` with the result. + * Reference to the contributor of the tool being called. */ - val toolClientId: String? = null, + val contributor: ToolCallContributor? = null, /** * Additional provider-specific metadata for this tool call. * - * Clients MAY look for well-known keys here to provide enhanced UI. - * For example, a `ptyTerminal` key with `{ input: string; output: string }` - * indicates the tool operated on a terminal (both `input` and `output` may - * contain escape sequences). + * This MAY include a `ui` field corresponding to the MCP Apps (SEP-1865) + * `McpUiToolMeta` found in MCP tool calls, which may be used in combination + * with the {@link contributor} to serve MCP Apps. */ @SerialName("_meta") val meta: Map? = null, @@ -1978,20 +2048,15 @@ data class ToolCallCancelledState( */ val displayName: String, /** - * If this tool is provided by a client, the `clientId` of the owning client. - * Absent for server-side tools. - * - * When set, the identified client is responsible for executing the tool and - * dispatching `session/toolCallComplete` with the result. + * Reference to the contributor of the tool being called. */ - val toolClientId: String? = null, + val contributor: ToolCallContributor? = null, /** * Additional provider-specific metadata for this tool call. * - * Clients MAY look for well-known keys here to provide enhanced UI. - * For example, a `ptyTerminal` key with `{ input: string; output: string }` - * indicates the tool operated on a terminal (both `input` and `output` may - * contain escape sequences). + * This MAY include a `ui` field corresponding to the MCP Apps (SEP-1865) + * `McpUiToolMeta` found in MCP tool calls, which may be used in combination + * with the {@link contributor} to serve MCP Apps. */ @SerialName("_meta") val meta: Map? = null, @@ -2662,7 +2727,144 @@ data class McpServerCustomization( * Absent when the customization covers the whole resource. */ val range: TextRange? = null, - val type: CustomizationType + val type: CustomizationType, + /** + * Whether this MCP server is currently enabled. + */ + val enabled: Boolean, + /** + * Current lifecycle state of the MCP server. + */ + val state: McpServerState, + /** + * An `mcp://`-protocol channel the client uses to side-channel traffic + * into the upstream MCP server itself. The channel is NOT a fresh raw MCP + * connection: it piggybacks on the AHP transport + * and skips the MCP `initialize` sequence. + * + * The agent host MAY only serve a subset of MCP on this + * channel; the served subset is described by domain-specific + * capabilities such as those in + * {@link McpServerCustomizationApps.capabilities}. + * + * The channel URI SHOULD be stable across the server's lifetime, but + * the agent host MAY change it (for example across a restart) and + * MAY only expose it while the server is in + * {@link McpServerStatus.Ready | `Ready`}. Absence means no + * side-channel is currently available. + */ + val channel: String? = null, + /** + * MCP App support. This property SHOULD be advertised for MCP servers + * which support apps. + */ + val mcpApp: McpServerCustomizationApps? = null +) + +@Serializable +data class McpServerCustomizationApps( + /** + * The subset of MCP App + * [`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx) + * the AHP host can satisfy for Views backed by this server. The + * client feeds these straight through into the `hostCapabilities` of + * the `ui/initialize` response delivered to the View. + */ + val capabilities: AhpMcpUiHostCapabilities +) + +@Serializable +data class AhpMcpUiHostCapabilities( + /** + * Producer proxies the MCP `tools/*` methods to the upstream server. + */ + val serverTools: JsonElement? = null, + /** + * Producer proxies the MCP `resources/*` methods to the upstream server. + */ + val serverResources: JsonElement? = null, + /** + * Producer accepts `notifications/message` log entries from the App via `mcpNotification`. + */ + val logging: Map? = null, + /** + * Producer serves `sampling/createMessage` via `mcpMethodCall`. + */ + val sampling: JsonElement? = null +) + +@Serializable +data class McpServerStartingState( + val kind: McpServerStatus +) + +@Serializable +data class McpServerReadyState( + val kind: McpServerStatus +) + +@Serializable +data class McpServerAuthRequiredState( + val kind: McpServerStatus, + /** + * Why authentication is required. + */ + val reason: McpAuthRequiredReason, + /** + * RFC 9728 Protected Resource Metadata. The `resource` field is the + * canonical MCP server URI per RFC 8707, used as the OAuth `resource` + * indicator. `authorization_servers` is REQUIRED by the MCP + * authorization spec. + */ + val resource: ProtectedResourceMetadata, + /** + * Scopes required for the current challenge, parsed from the + * `WWW-Authenticate: Bearer scope="…"` header (or `scopes_supported` + * fallback). Authoritative for the next authorization request — clients + * MUST NOT assume any subset/superset relationship to + * `resource.scopes_supported`. + */ + val requiredScopes: List? = null, + /** + * Human-readable hint, typically from the OAuth `error_description`. + */ + val description: String? = null +) + +@Serializable +data class McpServerErrorState( + val kind: McpServerStatus, + /** + * Error details. + */ + val error: ErrorInfo +) + +@Serializable +data class McpServerStoppedState( + val kind: McpServerStatus +) + +@Serializable +data class ToolCallClientContributor( + val kind: ToolCallContributorKind, + /** + * If this tool is provided by a client, the `clientId` of the owning client. + * Absent for server-side tools. + * + * When set, the identified client is responsible for executing the tool and + * dispatching `session/toolCallComplete` with the result. + */ + val clientId: String +) + +@Serializable +data class ToolCallMcpContributor( + val kind: ToolCallContributorKind, + /** + * Customization ID of the corresponding MCP server in {@link SessionState.customizations}. + */ + val customizationId: String ) @Serializable @@ -3531,6 +3733,8 @@ sealed interface Customization value class CustomizationPlugin(val value: PluginCustomization) : Customization @JvmInline value class CustomizationDirectory(val value: DirectoryCustomization) : Customization +@JvmInline +value class CustomizationMcpServer(val value: McpServerCustomization) : Customization /** * Forward-compat catch-all for unknown Customization discriminators. * @@ -3557,6 +3761,7 @@ internal object CustomizationSerializer : KSerializer { return when (discriminant) { "plugin" -> CustomizationPlugin(input.json.decodeFromJsonElement(PluginCustomization.serializer(), element)) "directory" -> CustomizationDirectory(input.json.decodeFromJsonElement(DirectoryCustomization.serializer(), element)) + "mcpServer" -> CustomizationMcpServer(input.json.decodeFromJsonElement(McpServerCustomization.serializer(), element)) else -> CustomizationUnknown(obj) } } @@ -3567,6 +3772,7 @@ internal object CustomizationSerializer : KSerializer { val element: JsonElement = when (value) { is CustomizationPlugin -> output.json.encodeToJsonElement(PluginCustomization.serializer(), value.value) is CustomizationDirectory -> output.json.encodeToJsonElement(DirectoryCustomization.serializer(), value.value) + is CustomizationMcpServer -> output.json.encodeToJsonElement(McpServerCustomization.serializer(), value.value) is CustomizationUnknown -> value.raw } output.encodeJsonElement(element) @@ -3695,6 +3901,116 @@ internal object CustomizationLoadStateSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("McpServerState") + + override fun deserialize(decoder: Decoder): McpServerState { + val input = decoder as? JsonDecoder + ?: error("McpServerState can only be deserialized from JSON") + val element = input.decodeJsonElement() + val obj = element as? JsonObject + ?: error("Expected JsonObject for McpServerState") + val discriminant = (obj["kind"] as? JsonPrimitive)?.content + ?: return McpServerStateUnknown(obj) + return when (discriminant) { + "starting" -> McpServerStateStarting(input.json.decodeFromJsonElement(McpServerStartingState.serializer(), element)) + "ready" -> McpServerStateReady(input.json.decodeFromJsonElement(McpServerReadyState.serializer(), element)) + "authRequired" -> McpServerStateAuthRequired(input.json.decodeFromJsonElement(McpServerAuthRequiredState.serializer(), element)) + "error" -> McpServerStateError(input.json.decodeFromJsonElement(McpServerErrorState.serializer(), element)) + "stopped" -> McpServerStateStopped(input.json.decodeFromJsonElement(McpServerStoppedState.serializer(), element)) + else -> McpServerStateUnknown(obj) + } + } + + override fun serialize(encoder: Encoder, value: McpServerState) { + val output = encoder as? JsonEncoder + ?: error("McpServerState can only be serialized to JSON") + val element: JsonElement = when (value) { + is McpServerStateStarting -> output.json.encodeToJsonElement(McpServerStartingState.serializer(), value.value) + is McpServerStateReady -> output.json.encodeToJsonElement(McpServerReadyState.serializer(), value.value) + is McpServerStateAuthRequired -> output.json.encodeToJsonElement(McpServerAuthRequiredState.serializer(), value.value) + is McpServerStateError -> output.json.encodeToJsonElement(McpServerErrorState.serializer(), value.value) + is McpServerStateStopped -> output.json.encodeToJsonElement(McpServerStoppedState.serializer(), value.value) + is McpServerStateUnknown -> value.raw + } + output.encodeJsonElement(element) + } +} + +@Serializable(with = ToolCallContributorSerializer::class) +sealed interface ToolCallContributor + +@JvmInline +value class ToolCallContributorClient(val value: ToolCallClientContributor) : ToolCallContributor +@JvmInline +value class ToolCallContributorMcp(val value: ToolCallMcpContributor) : ToolCallContributor +/** + * Forward-compat catch-all for unknown ToolCallContributor discriminators. + * + * Older clients may receive newer wire variants they don't recognise; capturing + * the raw `JsonObject` lets such payloads round-trip through the client unchanged. + * Reducers handle this variant conservatively on a per-union basis (typically + * as a no-op, but see `Reducers.kt` for the exact treatment). + */ +@JvmInline +value class ToolCallContributorUnknown(val raw: JsonObject) : ToolCallContributor + +internal object ToolCallContributorSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("ToolCallContributor") + + override fun deserialize(decoder: Decoder): ToolCallContributor { + val input = decoder as? JsonDecoder + ?: error("ToolCallContributor can only be deserialized from JSON") + val element = input.decodeJsonElement() + val obj = element as? JsonObject + ?: error("Expected JsonObject for ToolCallContributor") + val discriminant = (obj["kind"] as? JsonPrimitive)?.content + ?: return ToolCallContributorUnknown(obj) + return when (discriminant) { + "client" -> ToolCallContributorClient(input.json.decodeFromJsonElement(ToolCallClientContributor.serializer(), element)) + "mcp" -> ToolCallContributorMcp(input.json.decodeFromJsonElement(ToolCallMcpContributor.serializer(), element)) + else -> ToolCallContributorUnknown(obj) + } + } + + override fun serialize(encoder: Encoder, value: ToolCallContributor) { + val output = encoder as? JsonEncoder + ?: error("ToolCallContributor can only be serialized to JSON") + val element: JsonElement = when (value) { + is ToolCallContributorClient -> output.json.encodeToJsonElement(ToolCallClientContributor.serializer(), value.value) + is ToolCallContributorMcp -> output.json.encodeToJsonElement(ToolCallMcpContributor.serializer(), value.value) + is ToolCallContributorUnknown -> value.raw + } + output.encodeJsonElement(element) + } +} + @Serializable(with = ToolResultContentSerializer::class) sealed interface ToolResultContent { @JvmInline value class Text(val value: ToolResultTextContent) : ToolResultContent diff --git a/clients/rust/CHANGELOG.md b/clients/rust/CHANGELOG.md index fe6d5208..dee419cf 100644 --- a/clients/rust/CHANGELOG.md +++ b/clients/rust/CHANGELOG.md @@ -15,6 +15,33 @@ matching `## [X.Y.Z]` heading is missing from this file. ## [Unreleased] +### Added + +- `McpServerCustomization` now exposes the full MCP lifecycle: `enabled`, + the discriminated `McpServerState` enum + (`Starting`/`Ready`/`AuthRequired`/`Error`/`Stopped`), optional + `channel` URI for the `mcp://` side-channel, and optional `mcp_app` + block carrying `AhpMcpUiHostCapabilities` for MCP Apps. +- `McpServerAuthRequiredState` variant carries `ProtectedResourceMetadata` + plus `reason` / `required_scopes` / `description` so the existing + `authenticate` command can drive per-server auth. +- `Customization::McpServer` top-level variant — hosts MAY now surface + bare MCP servers directly rather than only inside a plugin or + directory. +- `SessionMcpServerStateChanged` action and matching reducer arm — + narrow upsert of `state` + `channel` on an existing MCP + server customization by id. +- `ClientCapabilities` struct on `InitializeParams.capabilities` with + first entry `mcp_apps`. + +### Changed + +- `ToolCallBase.tool_client_id: Option` replaced by + `ToolCallBase.contributor: Option` (enum with + `Client { client_id }` and `Mcp { customization_id }` variants). + `SessionToolCallStartAction` carries the new `contributor` field as + well. The reducer follows the rename. + ## [0.2.0] — 2026-05-28 Implements AHP `0.2.0`. Bumps the `ahp-types`, `ahp`, and `ahp-ws` crates diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index 895370b4..1f7e088f 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -13,10 +13,11 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; use crate::state::{ AgentInfo, AgentSelection, ChangesetFile, ChangesetOperation, ChangesetStatus, - ChangesetSummary, ConfirmationOption, Customization, ErrorInfo, Message, ModelSelection, - PendingMessageKind, ResponsePart, SessionActiveClient, SessionInputAnswer, SessionInputRequest, - SessionInputResponseKind, TerminalClaim, TerminalInfo, ToolCallCancellationReason, - ToolCallConfirmationReason, ToolCallResult, ToolDefinition, ToolResultContent, UsageInfo, + ChangesetSummary, ConfirmationOption, Customization, ErrorInfo, McpServerState, Message, + ModelSelection, PendingMessageKind, ResponsePart, SessionActiveClient, SessionInputAnswer, + SessionInputRequest, SessionInputResponseKind, TerminalClaim, TerminalInfo, + ToolCallCancellationReason, ToolCallConfirmationReason, ToolCallContributor, ToolCallResult, + ToolDefinition, ToolResultContent, UsageInfo, }; // ─── ActionType ────────────────────────────────────────────────────── @@ -94,6 +95,8 @@ pub enum ActionType { SessionCustomizationUpdated, #[serde(rename = "session/customizationRemoved")] SessionCustomizationRemoved, + #[serde(rename = "session/mcpServerStateChanged")] + SessionMcpServerStateChanged, #[serde(rename = "session/truncated")] SessionTruncated, #[serde(rename = "session/isReadChanged")] @@ -264,9 +267,11 @@ pub struct SessionResponsePartAction { /// A tool call begins — parameters are streaming from the LM. /// -/// For client-provided tools, the server sets `toolClientId` to identify the -/// owning client. That client is responsible for executing the tool once it -/// reaches the `running` state and dispatching `session/toolCallComplete`. +/// The server sets {@link ToolCallContributor | `contributor`} to identify +/// the origin of the tool. For client-provided tools, the named client is +/// responsible for executing the tool once it reaches the `running` state +/// and dispatching `session/toolCallComplete`. For MCP-served tools, the +/// server executes the call against the named `McpServerCustomization`. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionToolCallStartAction { @@ -286,10 +291,10 @@ pub struct SessionToolCallStartAction { pub tool_name: String, /// Human-readable tool name pub display_name: String, - /// If this tool is provided by a client, the `clientId` of the owning client. - /// Absent for server-side tools. + /// Reference to the contributor of the tool being called. Absent for + /// server-side tools that are not contributed by a client or MCP server. #[serde(default, skip_serializing_if = "Option::is_none")] - pub tool_client_id: Option, + pub contributor: Option, } /// Streaming partial parameters for a tool call. @@ -770,6 +775,40 @@ pub struct SessionCustomizationRemovedAction { pub id: String, } +/// Updates the runtime fields of an existing +/// {@link McpServerCustomization} — narrow alternative to +/// {@link SessionCustomizationUpdatedAction} for the high-frequency +/// `starting` ↔ `ready` ↔ `authRequired` transitions. +/// +/// Locates the target entry by `id`, searching both the top-level +/// customization list and the `children` array of every container. +/// Replaces the entry's {@link McpServerCustomization.state | `state`} +/// and {@link McpServerCustomization.channel | `channel`} +/// (full-replacement semantics: omit `channel` to clear an existing +/// channel URI). Other fields of the customization are preserved. +/// +/// Is a no-op when no matching `McpServerCustomization` is found. To +/// update any other field (name, icons, `mcpApp` capabilities, etc.) use +/// {@link SessionCustomizationUpdatedAction} instead. +/// +/// When the transition is to {@link McpServerStatus.AuthRequired} +/// because of a request issued mid-turn, the host SHOULD also raise +/// {@link SessionStatus.InputNeeded} on the session — see +/// {@link McpServerAuthRequiredState} for the rationale. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionMcpServerStateChangedAction { + /// The id of the {@link McpServerCustomization} to update. + pub id: String, + /// The new lifecycle state. + pub state: McpServerState, + /// Updated `mcp://` side-channel URI. Full-replacement: omit to clear + /// an existing channel (typical when leaving + /// {@link McpServerStatus.Ready | `Ready`}). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub channel: Option, +} + /// Truncates a session's history. If `turnId` is provided, all turns after that /// turn are removed and the specified turn is kept. If `turnId` is omitted, all /// turns are removed. @@ -1146,6 +1185,8 @@ pub enum StateAction { SessionCustomizationUpdated(SessionCustomizationUpdatedAction), #[serde(rename = "session/customizationRemoved")] SessionCustomizationRemoved(SessionCustomizationRemovedAction), + #[serde(rename = "session/mcpServerStateChanged")] + SessionMcpServerStateChanged(SessionMcpServerStateChangedAction), #[serde(rename = "session/truncated")] SessionTruncated(SessionTruncatedAction), #[serde(rename = "session/configChanged")] diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index 4b18fa92..50a1a199 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -117,6 +117,13 @@ pub struct InitializeParams { /// user-facing strings such as confirmation option labels. #[serde(default, skip_serializing_if = "Option::is_none")] pub locale: Option, + /// Optional client capability declarations. + /// + /// Servers SHOULD only advertise features whose corresponding client + /// capability is set here. Absent means "not declared" — the server + /// MUST assume the client does not support the feature. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub capabilities: Option, } /// Result of the `initialize` command. @@ -155,6 +162,29 @@ pub struct InitializeResult { pub telemetry: Option, } +/// Optional capabilities a client declares during `initialize`. +/// +/// Each field is a presence flag: an empty object `{}` means "supported", +/// absence means "not supported". Sub-fields on individual capabilities +/// are reserved for future per-capability options. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct ClientCapabilities { + /// Client can render + /// [MCP Apps](https://github.com/modelcontextprotocol/ext-apps) — i.e. + /// it can host the View sandbox, run the `ui/*` protocol against it, + /// and forward `mcp://`-channel traffic on the App's behalf. + /// + /// Hosts SHOULD only populate + /// {@link McpServerCustomization.mcpApp | `McpServerCustomization.mcpApp`} + /// (and expose the corresponding + /// {@link McpServerCustomization.channel | `mcp://` channel}) when this + /// capability is declared. Clients that omit it MUST treat + /// App-bearing tool calls as ordinary MCP tool calls. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub mcp_apps: Option, +} + /// Re-establishes a dropped connection. The server replays missed actions or /// provides fresh snapshots. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index c502e107..13ff5b1b 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -214,6 +214,14 @@ pub enum ConfirmationOptionKind { Deny, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum ToolCallContributorKind { + #[serde(rename = "client")] + Client, + #[serde(rename = "mcp")] + MCP, +} + /// Discriminant for tool result content types. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ToolResultContentType { @@ -234,10 +242,12 @@ pub enum ToolResultContentType { /// Discriminant for the kind of customization. /// /// Top-level entries in {@link SessionState.customizations} and -/// {@link AgentInfo.customizations} are always -/// {@link CustomizationType.Plugin | `Plugin`} or -/// {@link CustomizationType.Directory | `Directory`}; the remaining -/// types appear only as children of those containers. +/// {@link AgentInfo.customizations} are either container customizations +/// ({@link CustomizationType.Plugin | `Plugin`} or +/// {@link CustomizationType.Directory | `Directory`}) or +/// {@link CustomizationType.McpServer | `McpServer`} entries surfaced +/// directly by the host. The remaining types appear only as children of +/// a container. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum CustomizationType { #[serde(rename = "plugin")] @@ -280,6 +290,61 @@ pub enum TerminalClaimKind { Session, } +/// Discriminant for the {@link McpServerState} union. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum McpServerStatus { + /// Server has been registered but is not yet running. + #[serde(rename = "starting")] + Starting, + /// Server is running and serving requests. + #[serde(rename = "ready")] + Ready, + /// Server is reachable but requires additional authentication before it + /// can start, or before it can serve a particular request. Carries the + /// RFC 9728 Protected Resource Metadata the client needs to obtain a + /// token; the client then pushes the token via the existing + /// `authenticate` command. + #[serde(rename = "authRequired")] + AuthRequired, + /// Server failed to start, crashed, or otherwise transitioned to a fatal error. + #[serde(rename = "error")] + Error, + /// Server has been shut down. + #[serde(rename = "stopped")] + Stopped, +} + +/// Why an MCP server is currently in the {@link McpServerStatus.AuthRequired} +/// state. Mirrors the three failure modes defined by the +/// [MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization.md). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum McpAuthRequiredReason { + /// No token has been provided yet (HTTP 401, no prior token). + #[serde(rename = "required")] + Required, + /// A previously valid token expired or was revoked (HTTP 401). + #[serde(rename = "expired")] + Expired, + /// Step-up auth: a token is present but its scopes are insufficient for + /// the requested operation (HTTP 403 with + /// `WWW-Authenticate: Bearer error="insufficient_scope"`). + /// + /// Unlike {@link Required} and {@link Expired} — which typically surface + /// before any tool work is in flight — `InsufficientScope` is almost + /// always triggered by an MCP request issued mid-turn (a `tools/call`, + /// `resources/read`, etc.). The host SHOULD pair the + /// {@link McpServerAuthRequiredState} transition with + /// {@link SessionStatus.InputNeeded} on + /// {@link SessionSummary.status | the session} so the activity becomes + /// visible at the session-summary level, and clients SHOULD watch for + /// this kind on any + /// {@link McpServerCustomization | MCP server} backing a running tool + /// call so they can present an explicit "grant more access" affordance + /// tied to the blocked tool call. + #[serde(rename = "insufficientScope")] + InsufficientScope, +} + /// Computation lifecycle of a {@link ChangesetState}. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ChangesetStatus { @@ -503,13 +568,15 @@ pub struct AgentInfo { pub protected_resources: Option>, /// Customizations associated with this agent. /// - /// Always container customizations — + /// Either container customizations — /// {@link PluginCustomization | `PluginCustomization`} entries the agent /// bundles, plus {@link DirectoryCustomization | `DirectoryCustomization`} - /// entries it watches in any workspace it's used with. When a session is - /// created with this agent, these entries are augmented (e.g. directory - /// URIs are resolved against the workspace, children are parsed) and - /// propagated into the session's `customizations` list. + /// entries it watches in any workspace it's used with — or top-level + /// {@link McpServerCustomization | `McpServerCustomization`} entries + /// the agent host declares directly. When a session is created with + /// this agent, these entries are augmented (e.g. directory URIs are + /// resolved against the workspace, children are parsed) and propagated + /// into the session's `customizations` list. #[serde(default, skip_serializing_if = "Option::is_none")] pub customizations: Option>, } @@ -684,15 +751,23 @@ pub struct SessionState { pub config: Option, /// Top-level customizations active in this session. /// - /// Always container customizations — {@link PluginCustomization} or - /// {@link DirectoryCustomization}. Children (agents, skills, prompts, - /// rules, hooks, MCP servers) live in each container's - /// {@link ContainerCustomizationBase.children | `children`} array. + /// Always one of the {@link Customization} variants: + /// + /// - Container customizations ({@link PluginCustomization}, + /// {@link DirectoryCustomization}) whose children — agents, skills, + /// prompts, rules, hooks, MCP servers — live in each container's + /// {@link ContainerCustomizationBase.children | `children`} array. + /// - Top-level {@link McpServerCustomization} entries the host + /// surfaces directly (for example a globally-configured MCP server + /// that isn't bundled in a plugin or directory). MCP servers may + /// also appear as children of a container. /// /// Client-published plugins arrive via /// {@link SessionActiveClient.customizations | `activeClient.customizations`} /// and the host propagates them into this list (typically with the - /// container's `clientId` set and `children` populated). + /// container's `clientId` set and `children` populated). Clients + /// publish in container shape only; bare MCP servers at the top level + /// are server-originated. #[serde(default, skip_serializing_if = "Option::is_none")] pub customizations: Option>, /// Additional provider-specific metadata for this session. @@ -1425,19 +1500,14 @@ pub struct ToolCallStreamingState { pub tool_name: String, /// Human-readable tool name pub display_name: String, - /// If this tool is provided by a client, the `clientId` of the owning client. - /// Absent for server-side tools. - /// - /// When set, the identified client is responsible for executing the tool and - /// dispatching `session/toolCallComplete` with the result. + /// Reference to the contributor of the tool being called. #[serde(default, skip_serializing_if = "Option::is_none")] - pub tool_client_id: Option, + pub contributor: Option, /// Additional provider-specific metadata for this tool call. /// - /// Clients MAY look for well-known keys here to provide enhanced UI. - /// For example, a `ptyTerminal` key with `{ input: string; output: string }` - /// indicates the tool operated on a terminal (both `input` and `output` may - /// contain escape sequences). + /// This MAY include a `ui` field corresponding to the MCP Apps (SEP-1865) + /// `McpUiToolMeta` found in MCP tool calls, which may be used in combination + /// with the {@link contributor} to serve MCP Apps. #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, /// Partial parameters accumulated so far @@ -1459,19 +1529,14 @@ pub struct ToolCallPendingConfirmationState { pub tool_name: String, /// Human-readable tool name pub display_name: String, - /// If this tool is provided by a client, the `clientId` of the owning client. - /// Absent for server-side tools. - /// - /// When set, the identified client is responsible for executing the tool and - /// dispatching `session/toolCallComplete` with the result. + /// Reference to the contributor of the tool being called. #[serde(default, skip_serializing_if = "Option::is_none")] - pub tool_client_id: Option, + pub contributor: Option, /// Additional provider-specific metadata for this tool call. /// - /// Clients MAY look for well-known keys here to provide enhanced UI. - /// For example, a `ptyTerminal` key with `{ input: string; output: string }` - /// indicates the tool operated on a terminal (both `input` and `output` may - /// contain escape sequences). + /// This MAY include a `ui` field corresponding to the MCP Apps (SEP-1865) + /// `McpUiToolMeta` found in MCP tool calls, which may be used in combination + /// with the {@link contributor} to serve MCP Apps. #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, /// Message describing what the tool will do @@ -1506,19 +1571,14 @@ pub struct ToolCallRunningState { pub tool_name: String, /// Human-readable tool name pub display_name: String, - /// If this tool is provided by a client, the `clientId` of the owning client. - /// Absent for server-side tools. - /// - /// When set, the identified client is responsible for executing the tool and - /// dispatching `session/toolCallComplete` with the result. + /// Reference to the contributor of the tool being called. #[serde(default, skip_serializing_if = "Option::is_none")] - pub tool_client_id: Option, + pub contributor: Option, /// Additional provider-specific metadata for this tool call. /// - /// Clients MAY look for well-known keys here to provide enhanced UI. - /// For example, a `ptyTerminal` key with `{ input: string; output: string }` - /// indicates the tool operated on a terminal (both `input` and `output` may - /// contain escape sequences). + /// This MAY include a `ui` field corresponding to the MCP Apps (SEP-1865) + /// `McpUiToolMeta` found in MCP tool calls, which may be used in combination + /// with the {@link contributor} to serve MCP Apps. #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, /// Message describing what the tool will do @@ -1549,19 +1609,14 @@ pub struct ToolCallPendingResultConfirmationState { pub tool_name: String, /// Human-readable tool name pub display_name: String, - /// If this tool is provided by a client, the `clientId` of the owning client. - /// Absent for server-side tools. - /// - /// When set, the identified client is responsible for executing the tool and - /// dispatching `session/toolCallComplete` with the result. + /// Reference to the contributor of the tool being called. #[serde(default, skip_serializing_if = "Option::is_none")] - pub tool_client_id: Option, + pub contributor: Option, /// Additional provider-specific metadata for this tool call. /// - /// Clients MAY look for well-known keys here to provide enhanced UI. - /// For example, a `ptyTerminal` key with `{ input: string; output: string }` - /// indicates the tool operated on a terminal (both `input` and `output` may - /// contain escape sequences). + /// This MAY include a `ui` field corresponding to the MCP Apps (SEP-1865) + /// `McpUiToolMeta` found in MCP tool calls, which may be used in combination + /// with the {@link contributor} to serve MCP Apps. #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, /// Message describing what the tool will do @@ -1603,19 +1658,14 @@ pub struct ToolCallCompletedState { pub tool_name: String, /// Human-readable tool name pub display_name: String, - /// If this tool is provided by a client, the `clientId` of the owning client. - /// Absent for server-side tools. - /// - /// When set, the identified client is responsible for executing the tool and - /// dispatching `session/toolCallComplete` with the result. + /// Reference to the contributor of the tool being called. #[serde(default, skip_serializing_if = "Option::is_none")] - pub tool_client_id: Option, + pub contributor: Option, /// Additional provider-specific metadata for this tool call. /// - /// Clients MAY look for well-known keys here to provide enhanced UI. - /// For example, a `ptyTerminal` key with `{ input: string; output: string }` - /// indicates the tool operated on a terminal (both `input` and `output` may - /// contain escape sequences). + /// This MAY include a `ui` field corresponding to the MCP Apps (SEP-1865) + /// `McpUiToolMeta` found in MCP tool calls, which may be used in combination + /// with the {@link contributor} to serve MCP Apps. #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, /// Message describing what the tool will do @@ -1657,19 +1707,14 @@ pub struct ToolCallCancelledState { pub tool_name: String, /// Human-readable tool name pub display_name: String, - /// If this tool is provided by a client, the `clientId` of the owning client. - /// Absent for server-side tools. - /// - /// When set, the identified client is responsible for executing the tool and - /// dispatching `session/toolCallComplete` with the result. + /// Reference to the contributor of the tool being called. #[serde(default, skip_serializing_if = "Option::is_none")] - pub tool_client_id: Option, + pub contributor: Option, /// Additional provider-specific metadata for this tool call. /// - /// Clients MAY look for well-known keys here to provide enhanced UI. - /// For example, a `ptyTerminal` key with `{ input: string; output: string }` - /// indicates the tool operated on a terminal (both `input` and `output` may - /// contain escape sequences). + /// This MAY include a `ui` field corresponding to the MCP Apps (SEP-1865) + /// `McpUiToolMeta` found in MCP tool calls, which may be used in combination + /// with the {@link contributor} to serve MCP Apps. #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, /// Message describing what the tool will do @@ -2209,12 +2254,14 @@ pub struct HookCustomization { pub range: Option, } -/// An MCP manifest contributed by a plugin or directory. +/// An MCP server contributed by a plugin or directory. /// /// When the server is declared inline in the containing plugin manifest, /// `uri` points at the manifest file and /// {@link CustomizationBase.range | `range`} narrows it to the /// declaration's span. +/// +/// The MCP server customization also reflects its current status. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct McpServerCustomization { @@ -2241,6 +2288,182 @@ pub struct McpServerCustomization { /// Absent when the customization covers the whole resource. #[serde(default, skip_serializing_if = "Option::is_none")] pub range: Option, + /// Whether this MCP server is currently enabled. + pub enabled: bool, + /// Current lifecycle state of the MCP server. + pub state: McpServerState, + /// An `mcp://`-protocol channel the client uses to side-channel traffic + /// into the upstream MCP server itself. The channel is NOT a fresh raw MCP + /// connection: it piggybacks on the AHP transport + /// and skips the MCP `initialize` sequence. + /// + /// The agent host MAY only serve a subset of MCP on this + /// channel; the served subset is described by domain-specific + /// capabilities such as those in + /// {@link McpServerCustomizationApps.capabilities}. + /// + /// The channel URI SHOULD be stable across the server's lifetime, but + /// the agent host MAY change it (for example across a restart) and + /// MAY only expose it while the server is in + /// {@link McpServerStatus.Ready | `Ready`}. Absence means no + /// side-channel is currently available. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub channel: Option, + /// MCP App support. This property SHOULD be advertised for MCP servers + /// which support apps. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub mcp_app: Option, +} + +/// Information from the agent host needed to render MCP Apps served +/// by this MCP server. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpServerCustomizationApps { + /// The subset of MCP App + /// [`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx) + /// the AHP host can satisfy for Views backed by this server. The + /// client feeds these straight through into the `hostCapabilities` of + /// the `ui/initialize` response delivered to the View. + pub capabilities: AhpMcpUiHostCapabilities, +} + +/// The subset of MCP App +/// [`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx) +/// an AHP host can derive from the upstream MCP server (and from AHP's own +/// forwarding plumbing). Advertised on +/// {@link McpServerCustomizationApps.capabilities} so clients can pass it +/// through into the `hostCapabilities` of the `ui/initialize` response +/// delivered to an MCP App View. +/// +/// Field names mirror the MCP Apps spec exactly, so the AHP-side producer +/// can pass them straight through into the `hostCapabilities` of the +/// `ui/initialize` response delivered to the View. +/// +/// Capabilities outside this set (`openLinks`, `downloadFile`, `sandbox`, +/// `experimental`) are decided locally by whichever AHP client renders the +/// View and are NOT part of this AHP-level advertisement — only the +/// server-derived subset is. +/// +/// An agent host MUST only advertise a capability when it actually accepts the +/// corresponding methods/notifications on the `mcp://` channel: +/// +/// - {@link serverTools}: host proxies `tools/list` and `tools/call` to +/// the MCP server. When `listChanged` is `true`, the host also forwards +/// `notifications/tools/list_changed`. +/// - {@link serverResources}: host proxies `resources/read`, +/// `resources/list`, and `resources/templates/list` to the MCP server. +/// When `listChanged` is `true`, the host also forwards +/// `notifications/resources/list_changed`. +/// - {@link logging}: host accepts `notifications/message` log entries +/// from the App and forwards them via `mcpNotification` (and forwards +/// `logging/setLevel` calls to the server). +/// - {@link sampling}: host serves `sampling/createMessage` via +/// `mcpMethodCall`. When `sampling.tools` is present, the host also +/// accepts SEP-1577 `tools` / `toolChoice` / `tool_use` content blocks +/// inside `CreateMessageRequest`. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct AhpMcpUiHostCapabilities { + /// Producer proxies the MCP `tools/*` methods to the upstream server. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub server_tools: Option, + /// Producer proxies the MCP `resources/*` methods to the upstream server. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub server_resources: Option, + /// Producer accepts `notifications/message` log entries from the App via `mcpNotification`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub logging: Option, + /// Producer serves `sampling/createMessage` via `mcpMethodCall`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub sampling: Option, +} + +/// Server is registered with the host but has not yet started. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpServerStartingState {} + +/// Server is running and serving requests. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpServerReadyState {} + +/// Server is reachable but cannot serve requests until the client +/// authenticates. Mirrors the discovery flow defined by +/// [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) +/// (Protected Resource Metadata) and the OAuth 2.1 / RFC 6750 challenge +/// semantics required by the MCP authorization spec. +/// +/// Clients react to this state by calling the existing `authenticate` +/// command with the {@link ProtectedResourceMetadata.resource | resource} +/// carried here. There is **no** `notify/authRequired` notification for +/// MCP servers — the action stream is the single source of truth. +/// +/// When the transition is triggered by a request issued during a turn +/// — most commonly +/// {@link McpAuthRequiredReason.InsufficientScope | `InsufficientScope`} +/// surfacing mid-tool-call — the host SHOULD also raise +/// {@link SessionStatus.InputNeeded} on the session so the block is +/// visible at the summary level. Clients SHOULD watch this status on +/// any MCP server backing a running tool call and surface an explicit +/// affordance (e.g. a "grant additional access" prompt) tied to that +/// tool call, rather than relying on the user to notice the +/// customization’s status badge. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpServerAuthRequiredState { + /// Why authentication is required. + pub reason: McpAuthRequiredReason, + /// RFC 9728 Protected Resource Metadata. The `resource` field is the + /// canonical MCP server URI per RFC 8707, used as the OAuth `resource` + /// indicator. `authorization_servers` is REQUIRED by the MCP + /// authorization spec. + pub resource: ProtectedResourceMetadata, + /// Scopes required for the current challenge, parsed from the + /// `WWW-Authenticate: Bearer scope="…"` header (or `scopes_supported` + /// fallback). Authoritative for the next authorization request — clients + /// MUST NOT assume any subset/superset relationship to + /// `resource.scopes_supported`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub required_scopes: Option>, + /// Human-readable hint, typically from the OAuth `error_description`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, +} + +/// Server failed to start, crashed, or otherwise transitioned to a +/// non-recoverable error. Use {@link McpServerStatus.AuthRequired} +/// for authentication failures. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpServerErrorState { + /// Error details. + pub error: ErrorInfo, +} + +/// Server has been shut down. The host MAY remove the server from the +/// session entirely shortly after this state. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpServerStoppedState {} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ToolCallClientContributor { + /// If this tool is provided by a client, the `clientId` of the owning client. + /// Absent for server-side tools. + /// + /// When set, the identified client is responsible for executing the tool and + /// dispatching `session/toolCallComplete` with the result. + pub client_id: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ToolCallMcpContributor { + /// Customization ID of the corresponding MCP server in {@link SessionState.customizations}. + pub customization_id: String, } /// Describes a file modification with before/after state and diff metadata. @@ -2789,7 +3012,7 @@ pub enum MessageAttachment { Unknown(serde_json::Value), } -/// A top-level customization (plugin or directory). +/// A top-level customization (plugin, directory, or bare MCP server). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "type")] pub enum Customization { @@ -2797,6 +3020,8 @@ pub enum Customization { Plugin(PluginCustomization), #[serde(rename = "directory")] Directory(DirectoryCustomization), + #[serde(rename = "mcpServer")] + McpServer(McpServerCustomization), /// Unknown or future variant — preserved as raw JSON for round-trip fidelity. /// Reducers treat this as a no-op. #[serde(untagged)] @@ -2843,6 +3068,40 @@ pub enum CustomizationLoadState { Unknown(serde_json::Value), } +/// Discriminated lifecycle status of an MCP server customization. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "kind")] +pub enum McpServerState { + #[serde(rename = "starting")] + Starting(McpServerStartingState), + #[serde(rename = "ready")] + Ready(McpServerReadyState), + #[serde(rename = "authRequired")] + AuthRequired(McpServerAuthRequiredState), + #[serde(rename = "error")] + Error(McpServerErrorState), + #[serde(rename = "stopped")] + Stopped(McpServerStoppedState), + /// Unknown or future variant — preserved as raw JSON for round-trip fidelity. + /// Reducers treat this as a no-op. + #[serde(untagged)] + Unknown(serde_json::Value), +} + +/// Reference to the contributor of the tool being called. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "kind")] +pub enum ToolCallContributor { + #[serde(rename = "client")] + Client(ToolCallClientContributor), + #[serde(rename = "mcp")] + Mcp(ToolCallMcpContributor), + /// Unknown or future variant — preserved as raw JSON for round-trip fidelity. + /// Reducers treat this as a no-op. + #[serde(untagged)] + Unknown(serde_json::Value), +} + /// The state payload of a snapshot — root, session, terminal, or /// changeset state. /// diff --git a/clients/rust/crates/ahp/src/client.rs b/clients/rust/crates/ahp/src/client.rs index 42370af3..cddd0044 100644 --- a/clients/rust/crates/ahp/src/client.rs +++ b/clients/rust/crates/ahp/src/client.rs @@ -364,6 +364,7 @@ impl Client { Some(initial_subscriptions) }, locale: None, + capabilities: None, }; self.request("initialize", params).await } diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index 8fe1b693..d3651d60 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -58,9 +58,9 @@ use ahp_types::state::{ PendingMessageKind, ResponsePart, RootState, SessionInputRequest, SessionLifecycle, SessionState, SessionStatus, TerminalCommandPart, TerminalContentPart, TerminalState, TerminalUnclassifiedPart, ToolCallCancellationReason, ToolCallCancelledState, - ToolCallCompletedState, ToolCallConfirmationReason, ToolCallPendingConfirmationState, - ToolCallPendingResultConfirmationState, ToolCallResponsePart, ToolCallRunningState, - ToolCallState, ToolCallStreamingState, Turn, TurnState, + ToolCallCompletedState, ToolCallConfirmationReason, ToolCallContributor, + ToolCallPendingConfirmationState, ToolCallPendingResultConfirmationState, ToolCallResponsePart, + ToolCallRunningState, ToolCallState, ToolCallStreamingState, Turn, TurnState, }; /// What happened when an action was applied. @@ -100,7 +100,7 @@ fn tool_call_meta( String, String, String, - Option, + Option, Option>, ) { match tc { @@ -108,42 +108,42 @@ fn tool_call_meta( s.tool_call_id.clone(), s.tool_name.clone(), s.display_name.clone(), - s.tool_client_id.clone(), + s.contributor.clone(), s.meta.clone(), ), ToolCallState::PendingConfirmation(s) => ( s.tool_call_id.clone(), s.tool_name.clone(), s.display_name.clone(), - s.tool_client_id.clone(), + s.contributor.clone(), s.meta.clone(), ), ToolCallState::Running(s) => ( s.tool_call_id.clone(), s.tool_name.clone(), s.display_name.clone(), - s.tool_client_id.clone(), + s.contributor.clone(), s.meta.clone(), ), ToolCallState::PendingResultConfirmation(s) => ( s.tool_call_id.clone(), s.tool_name.clone(), s.display_name.clone(), - s.tool_client_id.clone(), + s.contributor.clone(), s.meta.clone(), ), ToolCallState::Completed(s) => ( s.tool_call_id.clone(), s.tool_name.clone(), s.display_name.clone(), - s.tool_client_id.clone(), + s.contributor.clone(), s.meta.clone(), ), ToolCallState::Cancelled(s) => ( s.tool_call_id.clone(), s.tool_name.clone(), s.display_name.clone(), - s.tool_client_id.clone(), + s.contributor.clone(), s.meta.clone(), ), ToolCallState::Unknown(_) => (String::new(), String::new(), String::new(), None, None), @@ -241,7 +241,7 @@ fn end_turn( ResponsePart::ToolCall(Box::new(ToolCallResponsePart { tool_call: tc })) } _ => { - let (tool_call_id, tool_name, display_name, tool_client_id, meta) = + let (tool_call_id, tool_name, display_name, contributor, meta) = tool_call_meta(&tc); let invocation_message = match &tc { ToolCallState::Streaming(s) => { @@ -265,7 +265,7 @@ fn end_turn( tool_call_id, tool_name, display_name, - tool_client_id, + contributor, meta, invocation_message, tool_input, @@ -324,6 +324,7 @@ fn customization_id(c: &Customization) -> Option<&str> { match c { Customization::Plugin(p) => Some(p.id.as_str()), Customization::Directory(d) => Some(d.id.as_str()), + Customization::McpServer(m) => Some(m.id.as_str()), Customization::Unknown(_) => None, } } @@ -344,7 +345,7 @@ fn container_children_mut(c: &mut Customization) -> Option<&mut Vec p.children.as_mut(), Customization::Directory(d) => d.children.as_mut(), - Customization::Unknown(_) => None, + Customization::McpServer(_) | Customization::Unknown(_) => None, } } @@ -352,6 +353,7 @@ fn set_container_enabled(c: &mut Customization, enabled: bool) { match c { Customization::Plugin(p) => p.enabled = enabled, Customization::Directory(d) => d.enabled = enabled, + Customization::McpServer(m) => m.enabled = enabled, Customization::Unknown(_) => {} } } @@ -388,7 +390,7 @@ where tool_call_id: String::new(), tool_name: String::new(), display_name: String::new(), - tool_client_id: None, + contributor: None, meta: None, invocation_message: Default::default(), tool_input: None, @@ -531,7 +533,7 @@ pub fn apply_action_to_session(state: &mut SessionState, action: &StateAction) - tool_call_id: a.tool_call_id.clone(), tool_name: a.tool_name.clone(), display_name: a.display_name.clone(), - tool_client_id: a.tool_client_id.clone(), + contributor: a.contributor.clone(), meta: a.meta.clone(), partial_input: None, invocation_message: None, @@ -712,6 +714,42 @@ pub fn apply_action_to_session(state: &mut SessionState, action: &StateAction) - } ReduceOutcome::NoOp } + StateAction::SessionMcpServerStateChanged(a) => { + let Some(list) = state.customizations.as_mut() else { + return ReduceOutcome::NoOp; + }; + if let Some(idx) = list + .iter() + .position(|c| customization_id(c) == Some(a.id.as_str())) + { + match &mut list[idx] { + Customization::McpServer(m) => { + m.state = a.state.clone(); + m.channel = a.channel.clone(); + return ReduceOutcome::Applied; + } + // Top-level entry exists but isn't an MCP server: no-op. + _ => return ReduceOutcome::NoOp, + } + } + for container in list.iter_mut() { + let Some(children) = container_children_mut(container) else { + continue; + }; + if let Some(idx) = children + .iter() + .position(|c| child_id_of(c) == Some(a.id.as_str())) + { + if let ChildCustomization::McpServer(m) = &mut children[idx] { + m.state = a.state.clone(); + m.channel = a.channel.clone(); + return ReduceOutcome::Applied; + } + return ReduceOutcome::NoOp; + } + } + ReduceOutcome::NoOp + } StateAction::SessionTruncated(a) => apply_truncated(state, a.turn_id.as_deref()), StateAction::SessionInputRequested(a) => { upsert_input_request(state, a.request.clone()); @@ -850,7 +888,7 @@ fn apply_tool_call_ready( a: &SessionToolCallReadyAction, ) -> ReduceOutcome { update_tool_call(state, &a.turn_id, &a.tool_call_id, |tc| { - let (tool_call_id, tool_name, display_name, tool_client_id, meta) = tool_call_meta(&tc); + let (tool_call_id, tool_name, display_name, contributor, meta) = tool_call_meta(&tc); match tc { ToolCallState::Streaming(_) | ToolCallState::Running(_) => { if let Some(confirmed) = a.confirmed { @@ -858,7 +896,7 @@ fn apply_tool_call_ready( tool_call_id, tool_name, display_name, - tool_client_id, + contributor, meta, invocation_message: a.invocation_message.clone(), tool_input: a.tool_input.clone(), @@ -871,7 +909,7 @@ fn apply_tool_call_ready( tool_call_id, tool_name, display_name, - tool_client_id, + contributor, meta, invocation_message: a.invocation_message.clone(), tool_input: a.tool_input.clone(), @@ -909,7 +947,7 @@ fn apply_tool_call_confirmed( let tool_call_id = s.tool_call_id; let tool_name = s.tool_name; let display_name = s.display_name; - let tool_client_id = s.tool_client_id; + let contributor = s.contributor; let meta = s.meta; let invocation_message = s.invocation_message; let tool_input = s.tool_input; @@ -918,7 +956,7 @@ fn apply_tool_call_confirmed( tool_call_id, tool_name, display_name, - tool_client_id, + contributor, meta, invocation_message, tool_input: a.edited_tool_input.clone().or(tool_input), @@ -931,7 +969,7 @@ fn apply_tool_call_confirmed( tool_call_id, tool_name, display_name, - tool_client_id, + contributor, meta, invocation_message, tool_input, @@ -949,7 +987,7 @@ fn apply_tool_call_complete( a: &SessionToolCallCompleteAction, ) -> ReduceOutcome { update_tool_call(state, &a.turn_id, &a.tool_call_id, |tc| { - let (tool_call_id, tool_name, display_name, tool_client_id, meta) = tool_call_meta(&tc); + let (tool_call_id, tool_name, display_name, contributor, meta) = tool_call_meta(&tc); let (invocation_message, tool_input, confirmed, selected_option) = match tc { ToolCallState::Running(s) => ( s.invocation_message, @@ -970,7 +1008,7 @@ fn apply_tool_call_complete( tool_call_id, tool_name, display_name, - tool_client_id, + contributor, meta, invocation_message, tool_input, @@ -987,7 +1025,7 @@ fn apply_tool_call_complete( tool_call_id, tool_name, display_name, - tool_client_id, + contributor, meta, invocation_message, tool_input, @@ -1016,7 +1054,7 @@ fn apply_tool_call_result_confirmed( tool_call_id: s.tool_call_id, tool_name: s.tool_name, display_name: s.display_name, - tool_client_id: s.tool_client_id, + contributor: s.contributor, meta: s.meta, invocation_message: s.invocation_message, tool_input: s.tool_input, @@ -1033,7 +1071,7 @@ fn apply_tool_call_result_confirmed( tool_call_id: s.tool_call_id, tool_name: s.tool_name, display_name: s.display_name, - tool_client_id: s.tool_client_id, + contributor: s.contributor, meta: s.meta, invocation_message: s.invocation_message, tool_input: s.tool_input, diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift index 2e5511eb..941d2585 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift @@ -41,6 +41,7 @@ public enum ActionType: String, Codable, Sendable { case sessionCustomizationToggled = "session/customizationToggled" case sessionCustomizationUpdated = "session/customizationUpdated" case sessionCustomizationRemoved = "session/customizationRemoved" + case sessionMcpServerStateChanged = "session/mcpServerStateChanged" case sessionTruncated = "session/truncated" case sessionIsReadChanged = "session/isReadChanged" case sessionIsArchivedChanged = "session/isArchivedChanged" @@ -240,9 +241,9 @@ public struct SessionToolCallStartAction: Codable, Sendable { public var toolName: String /// Human-readable tool name public var displayName: String - /// If this tool is provided by a client, the `clientId` of the owning client. - /// Absent for server-side tools. - public var toolClientId: String? + /// Reference to the contributor of the tool being called. Absent for + /// server-side tools that are not contributed by a client or MCP server. + public var contributor: ToolCallContributor? enum CodingKeys: String, CodingKey { case turnId @@ -251,7 +252,7 @@ public struct SessionToolCallStartAction: Codable, Sendable { case type case toolName case displayName - case toolClientId + case contributor } public init( @@ -261,7 +262,7 @@ public struct SessionToolCallStartAction: Codable, Sendable { type: ActionType, toolName: String, displayName: String, - toolClientId: String? = nil + contributor: ToolCallContributor? = nil ) { self.turnId = turnId self.toolCallId = toolCallId @@ -269,7 +270,7 @@ public struct SessionToolCallStartAction: Codable, Sendable { self.type = type self.toolName = toolName self.displayName = displayName - self.toolClientId = toolClientId + self.contributor = contributor } } @@ -928,6 +929,30 @@ public struct SessionCustomizationRemovedAction: Codable, Sendable { } } +public struct SessionMcpServerStateChangedAction: Codable, Sendable { + public var type: ActionType + /// The id of the {@link McpServerCustomization} to update. + public var id: String + /// The new lifecycle state. + public var state: McpServerState + /// Updated `mcp://` side-channel URI. Full-replacement: omit to clear + /// an existing channel (typical when leaving + /// {@link McpServerStatus.Ready | `Ready`}). + public var channel: String? + + public init( + type: ActionType, + id: String, + state: McpServerState, + channel: String? = nil + ) { + self.type = type + self.id = id + self.state = state + self.channel = channel + } +} + public struct SessionTruncatedAction: Codable, Sendable { public var type: ActionType /// Keep turns up to and including this turn. Omit to clear all turns. @@ -1345,6 +1370,7 @@ public enum StateAction: Codable, Sendable { case sessionCustomizationToggled(SessionCustomizationToggledAction) case sessionCustomizationUpdated(SessionCustomizationUpdatedAction) case sessionCustomizationRemoved(SessionCustomizationRemovedAction) + case sessionMcpServerStatusChanged(SessionMcpServerStateChangedAction) case sessionTruncated(SessionTruncatedAction) case sessionConfigChanged(SessionConfigChangedAction) case sessionMetaChanged(SessionMetaChangedAction) @@ -1453,6 +1479,8 @@ public enum StateAction: Codable, Sendable { self = .sessionCustomizationUpdated(try SessionCustomizationUpdatedAction(from: decoder)) case "session/customizationRemoved": self = .sessionCustomizationRemoved(try SessionCustomizationRemovedAction(from: decoder)) + case "session/mcpServerStateChanged": + self = .sessionMcpServerStatusChanged(try SessionMcpServerStateChangedAction(from: decoder)) case "session/truncated": self = .sessionTruncated(try SessionTruncatedAction(from: decoder)) case "session/configChanged": @@ -1544,6 +1572,7 @@ public enum StateAction: Codable, Sendable { case .sessionCustomizationToggled(let v): try v.encode(to: encoder) case .sessionCustomizationUpdated(let v): try v.encode(to: encoder) case .sessionCustomizationRemoved(let v): try v.encode(to: encoder) + case .sessionMcpServerStatusChanged(let v): try v.encode(to: encoder) case .sessionTruncated(let v): try v.encode(to: encoder) case .sessionConfigChanged(let v): try v.encode(to: encoder) case .sessionMetaChanged(let v): try v.encode(to: encoder) diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift index f79aa009..13b5db17 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -77,19 +77,27 @@ public struct InitializeParams: Codable, Sendable { /// (e.g. `"en-US"`, `"ja"`). The server SHOULD use this to localise /// user-facing strings such as confirmation option labels. public var locale: String? + /// Optional client capability declarations. + /// + /// Servers SHOULD only advertise features whose corresponding client + /// capability is set here. Absent means "not declared" — the server + /// MUST assume the client does not support the feature. + public var capabilities: ClientCapabilities? public init( channel: String, protocolVersions: [String], clientId: String, initialSubscriptions: [String]? = nil, - locale: String? = nil + locale: String? = nil, + capabilities: ClientCapabilities? = nil ) { self.channel = channel self.protocolVersions = protocolVersions self.clientId = clientId self.initialSubscriptions = initialSubscriptions self.locale = locale + self.capabilities = capabilities } } @@ -133,6 +141,27 @@ public struct InitializeResult: Codable, Sendable { } } +public struct ClientCapabilities: Codable, Sendable { + /// Client can render + /// [MCP Apps](https://github.com/modelcontextprotocol/ext-apps) — i.e. + /// it can host the View sandbox, run the `ui/*` protocol against it, + /// and forward `mcp://`-channel traffic on the App's behalf. + /// + /// Hosts SHOULD only populate + /// {@link McpServerCustomization.mcpApp | `McpServerCustomization.mcpApp`} + /// (and expose the corresponding + /// {@link McpServerCustomization.channel | `mcp://` channel}) when this + /// capability is declared. Clients that omit it MUST treat + /// App-bearing tool calls as ordinary MCP tool calls. + public var mcpApps: [String: AnyCodable]? + + public init( + mcpApps: [String: AnyCodable]? = nil + ) { + self.mcpApps = mcpApps + } +} + public struct ReconnectParams: Codable, Sendable { /// Channel URI this command targets. public var channel: String diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 6e6f1c0f..077740e8 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -178,6 +178,11 @@ public enum ConfirmationOptionKind: String, Codable, Sendable { case deny = "deny" } +public enum ToolCallContributorKind: String, Codable, Sendable { + case client = "client" + case mCP = "mcp" +} + /// Discriminant for tool result content types. public enum ToolResultContentType: String, Codable, Sendable { case text = "text" @@ -191,10 +196,12 @@ public enum ToolResultContentType: String, Codable, Sendable { /// Discriminant for the kind of customization. /// /// Top-level entries in {@link SessionState.customizations} and -/// {@link AgentInfo.customizations} are always -/// {@link CustomizationType.Plugin | `Plugin`} or -/// {@link CustomizationType.Directory | `Directory`}; the remaining -/// types appear only as children of those containers. +/// {@link AgentInfo.customizations} are either container customizations +/// ({@link CustomizationType.Plugin | `Plugin`} or +/// {@link CustomizationType.Directory | `Directory`}) or +/// {@link CustomizationType.McpServer | `McpServer`} entries surfaced +/// directly by the host. The remaining types appear only as children of +/// a container. public enum CustomizationType: String, Codable, Sendable { case plugin = "plugin" case directory = "directory" @@ -220,6 +227,51 @@ public enum TerminalClaimKind: String, Codable, Sendable { case session = "session" } +/// Discriminant for the {@link McpServerState} union. +public enum McpServerStatus: String, Codable, Sendable { + /// Server has been registered but is not yet running. + case starting = "starting" + /// Server is running and serving requests. + case ready = "ready" + /// Server is reachable but requires additional authentication before it + /// can start, or before it can serve a particular request. Carries the + /// RFC 9728 Protected Resource Metadata the client needs to obtain a + /// token; the client then pushes the token via the existing + /// `authenticate` command. + case authRequired = "authRequired" + /// Server failed to start, crashed, or otherwise transitioned to a fatal error. + case error = "error" + /// Server has been shut down. + case stopped = "stopped" +} + +/// Why an MCP server is currently in the {@link McpServerStatus.AuthRequired} +/// state. Mirrors the three failure modes defined by the +/// [MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization.md). +public enum McpAuthRequiredReason: String, Codable, Sendable { + /// No token has been provided yet (HTTP 401, no prior token). + case required = "required" + /// A previously valid token expired or was revoked (HTTP 401). + case expired = "expired" + /// Step-up auth: a token is present but its scopes are insufficient for + /// the requested operation (HTTP 403 with + /// `WWW-Authenticate: Bearer error="insufficient_scope"`). + /// + /// Unlike {@link Required} and {@link Expired} — which typically surface + /// before any tool work is in flight — `InsufficientScope` is almost + /// always triggered by an MCP request issued mid-turn (a `tools/call`, + /// `resources/read`, etc.). The host SHOULD pair the + /// {@link McpServerAuthRequiredState} transition with + /// {@link SessionStatus.InputNeeded} on + /// {@link SessionSummary.status | the session} so the activity becomes + /// visible at the session-summary level, and clients SHOULD watch for + /// this kind on any + /// {@link McpServerCustomization | MCP server} backing a running tool + /// call so they can present an explicit "grant more access" affordance + /// tied to the blocked tool call. + case insufficientScope = "insufficientScope" +} + /// Computation lifecycle of a {@link ChangesetState}. public enum ChangesetStatus: String, Codable, Sendable { /// The server is still computing the contents of this changeset. @@ -429,13 +481,15 @@ public struct AgentInfo: Codable, Sendable { public var protectedResources: [ProtectedResourceMetadata]? /// Customizations associated with this agent. /// - /// Always container customizations — + /// Either container customizations — /// {@link PluginCustomization | `PluginCustomization`} entries the agent /// bundles, plus {@link DirectoryCustomization | `DirectoryCustomization`} - /// entries it watches in any workspace it's used with. When a session is - /// created with this agent, these entries are augmented (e.g. directory - /// URIs are resolved against the workspace, children are parsed) and - /// propagated into the session's `customizations` list. + /// entries it watches in any workspace it's used with — or top-level + /// {@link McpServerCustomization | `McpServerCustomization`} entries + /// the agent host declares directly. When a session is created with + /// this agent, these entries are augmented (e.g. directory URIs are + /// resolved against the workspace, children are parsed) and propagated + /// into the session's `customizations` list. public var customizations: [Customization]? public init( @@ -660,15 +714,23 @@ public struct SessionState: Codable, Sendable { public var config: SessionConfigState? /// Top-level customizations active in this session. /// - /// Always container customizations — {@link PluginCustomization} or - /// {@link DirectoryCustomization}. Children (agents, skills, prompts, - /// rules, hooks, MCP servers) live in each container's + /// Always one of the {@link Customization} variants: + /// + /// - Container customizations ({@link PluginCustomization}, + /// {@link DirectoryCustomization}) whose children — agents, skills, + /// prompts, rules, hooks, MCP servers — live in each container's /// {@link ContainerCustomizationBase.children | `children`} array. + /// - Top-level {@link McpServerCustomization} entries the host + /// surfaces directly (for example a globally-configured MCP server + /// that isn't bundled in a plugin or directory). MCP servers may + /// also appear as children of a container. /// /// Client-published plugins arrive via /// {@link SessionActiveClient.customizations | `activeClient.customizations`} /// and the host propagates them into this list (typically with the - /// container's `clientId` set and `children` populated). + /// container's `clientId` set and `children` populated). Clients + /// publish in container shape only; bare MCP servers at the top level + /// are server-originated. public var customizations: [Customization]? /// Additional provider-specific metadata for this session. /// @@ -1667,18 +1729,13 @@ public struct ToolCallStreamingState: Codable, Sendable { public var toolName: String /// Human-readable tool name public var displayName: String - /// If this tool is provided by a client, the `clientId` of the owning client. - /// Absent for server-side tools. - /// - /// When set, the identified client is responsible for executing the tool and - /// dispatching `session/toolCallComplete` with the result. - public var toolClientId: String? + /// Reference to the contributor of the tool being called. + public var contributor: ToolCallContributor? /// Additional provider-specific metadata for this tool call. /// - /// Clients MAY look for well-known keys here to provide enhanced UI. - /// For example, a `ptyTerminal` key with `{ input: string; output: string }` - /// indicates the tool operated on a terminal (both `input` and `output` may - /// contain escape sequences). + /// This MAY include a `ui` field corresponding to the MCP Apps (SEP-1865) + /// `McpUiToolMeta` found in MCP tool calls, which may be used in combination + /// with the {@link contributor} to serve MCP Apps. public var meta: [String: AnyCodable]? public var status: ToolCallStatus /// Partial parameters accumulated so far @@ -1690,7 +1747,7 @@ public struct ToolCallStreamingState: Codable, Sendable { case toolCallId case toolName case displayName - case toolClientId + case contributor case meta = "_meta" case status case partialInput @@ -1701,7 +1758,7 @@ public struct ToolCallStreamingState: Codable, Sendable { toolCallId: String, toolName: String, displayName: String, - toolClientId: String? = nil, + contributor: ToolCallContributor? = nil, meta: [String: AnyCodable]? = nil, status: ToolCallStatus, partialInput: String? = nil, @@ -1710,7 +1767,7 @@ public struct ToolCallStreamingState: Codable, Sendable { self.toolCallId = toolCallId self.toolName = toolName self.displayName = displayName - self.toolClientId = toolClientId + self.contributor = contributor self.meta = meta self.status = status self.partialInput = partialInput @@ -1725,18 +1782,13 @@ public struct ToolCallPendingConfirmationState: Codable, Sendable { public var toolName: String /// Human-readable tool name public var displayName: String - /// If this tool is provided by a client, the `clientId` of the owning client. - /// Absent for server-side tools. - /// - /// When set, the identified client is responsible for executing the tool and - /// dispatching `session/toolCallComplete` with the result. - public var toolClientId: String? + /// Reference to the contributor of the tool being called. + public var contributor: ToolCallContributor? /// Additional provider-specific metadata for this tool call. /// - /// Clients MAY look for well-known keys here to provide enhanced UI. - /// For example, a `ptyTerminal` key with `{ input: string; output: string }` - /// indicates the tool operated on a terminal (both `input` and `output` may - /// contain escape sequences). + /// This MAY include a `ui` field corresponding to the MCP Apps (SEP-1865) + /// `McpUiToolMeta` found in MCP tool calls, which may be used in combination + /// with the {@link contributor} to serve MCP Apps. public var meta: [String: AnyCodable]? /// Message describing what the tool will do public var invocationMessage: StringOrMarkdown @@ -1759,7 +1811,7 @@ public struct ToolCallPendingConfirmationState: Codable, Sendable { case toolCallId case toolName case displayName - case toolClientId + case contributor case meta = "_meta" case invocationMessage case toolInput @@ -1774,7 +1826,7 @@ public struct ToolCallPendingConfirmationState: Codable, Sendable { toolCallId: String, toolName: String, displayName: String, - toolClientId: String? = nil, + contributor: ToolCallContributor? = nil, meta: [String: AnyCodable]? = nil, invocationMessage: StringOrMarkdown, toolInput: String? = nil, @@ -1787,7 +1839,7 @@ public struct ToolCallPendingConfirmationState: Codable, Sendable { self.toolCallId = toolCallId self.toolName = toolName self.displayName = displayName - self.toolClientId = toolClientId + self.contributor = contributor self.meta = meta self.invocationMessage = invocationMessage self.toolInput = toolInput @@ -1806,18 +1858,13 @@ public struct ToolCallRunningState: Codable, Sendable { public var toolName: String /// Human-readable tool name public var displayName: String - /// If this tool is provided by a client, the `clientId` of the owning client. - /// Absent for server-side tools. - /// - /// When set, the identified client is responsible for executing the tool and - /// dispatching `session/toolCallComplete` with the result. - public var toolClientId: String? + /// Reference to the contributor of the tool being called. + public var contributor: ToolCallContributor? /// Additional provider-specific metadata for this tool call. /// - /// Clients MAY look for well-known keys here to provide enhanced UI. - /// For example, a `ptyTerminal` key with `{ input: string; output: string }` - /// indicates the tool operated on a terminal (both `input` and `output` may - /// contain escape sequences). + /// This MAY include a `ui` field corresponding to the MCP Apps (SEP-1865) + /// `McpUiToolMeta` found in MCP tool calls, which may be used in combination + /// with the {@link contributor} to serve MCP Apps. public var meta: [String: AnyCodable]? /// Message describing what the tool will do public var invocationMessage: StringOrMarkdown @@ -1838,7 +1885,7 @@ public struct ToolCallRunningState: Codable, Sendable { case toolCallId case toolName case displayName - case toolClientId + case contributor case meta = "_meta" case invocationMessage case toolInput @@ -1852,7 +1899,7 @@ public struct ToolCallRunningState: Codable, Sendable { toolCallId: String, toolName: String, displayName: String, - toolClientId: String? = nil, + contributor: ToolCallContributor? = nil, meta: [String: AnyCodable]? = nil, invocationMessage: StringOrMarkdown, toolInput: String? = nil, @@ -1864,7 +1911,7 @@ public struct ToolCallRunningState: Codable, Sendable { self.toolCallId = toolCallId self.toolName = toolName self.displayName = displayName - self.toolClientId = toolClientId + self.contributor = contributor self.meta = meta self.invocationMessage = invocationMessage self.toolInput = toolInput @@ -1882,18 +1929,13 @@ public struct ToolCallPendingResultConfirmationState: Codable, Sendable { public var toolName: String /// Human-readable tool name public var displayName: String - /// If this tool is provided by a client, the `clientId` of the owning client. - /// Absent for server-side tools. - /// - /// When set, the identified client is responsible for executing the tool and - /// dispatching `session/toolCallComplete` with the result. - public var toolClientId: String? + /// Reference to the contributor of the tool being called. + public var contributor: ToolCallContributor? /// Additional provider-specific metadata for this tool call. /// - /// Clients MAY look for well-known keys here to provide enhanced UI. - /// For example, a `ptyTerminal` key with `{ input: string; output: string }` - /// indicates the tool operated on a terminal (both `input` and `output` may - /// contain escape sequences). + /// This MAY include a `ui` field corresponding to the MCP Apps (SEP-1865) + /// `McpUiToolMeta` found in MCP tool calls, which may be used in combination + /// with the {@link contributor} to serve MCP Apps. public var meta: [String: AnyCodable]? /// Message describing what the tool will do public var invocationMessage: StringOrMarkdown @@ -1923,7 +1965,7 @@ public struct ToolCallPendingResultConfirmationState: Codable, Sendable { case toolCallId case toolName case displayName - case toolClientId + case contributor case meta = "_meta" case invocationMessage case toolInput @@ -1941,7 +1983,7 @@ public struct ToolCallPendingResultConfirmationState: Codable, Sendable { toolCallId: String, toolName: String, displayName: String, - toolClientId: String? = nil, + contributor: ToolCallContributor? = nil, meta: [String: AnyCodable]? = nil, invocationMessage: StringOrMarkdown, toolInput: String? = nil, @@ -1957,7 +1999,7 @@ public struct ToolCallPendingResultConfirmationState: Codable, Sendable { self.toolCallId = toolCallId self.toolName = toolName self.displayName = displayName - self.toolClientId = toolClientId + self.contributor = contributor self.meta = meta self.invocationMessage = invocationMessage self.toolInput = toolInput @@ -1979,18 +2021,13 @@ public struct ToolCallCompletedState: Codable, Sendable { public var toolName: String /// Human-readable tool name public var displayName: String - /// If this tool is provided by a client, the `clientId` of the owning client. - /// Absent for server-side tools. - /// - /// When set, the identified client is responsible for executing the tool and - /// dispatching `session/toolCallComplete` with the result. - public var toolClientId: String? + /// Reference to the contributor of the tool being called. + public var contributor: ToolCallContributor? /// Additional provider-specific metadata for this tool call. /// - /// Clients MAY look for well-known keys here to provide enhanced UI. - /// For example, a `ptyTerminal` key with `{ input: string; output: string }` - /// indicates the tool operated on a terminal (both `input` and `output` may - /// contain escape sequences). + /// This MAY include a `ui` field corresponding to the MCP Apps (SEP-1865) + /// `McpUiToolMeta` found in MCP tool calls, which may be used in combination + /// with the {@link contributor} to serve MCP Apps. public var meta: [String: AnyCodable]? /// Message describing what the tool will do public var invocationMessage: StringOrMarkdown @@ -2020,7 +2057,7 @@ public struct ToolCallCompletedState: Codable, Sendable { case toolCallId case toolName case displayName - case toolClientId + case contributor case meta = "_meta" case invocationMessage case toolInput @@ -2038,7 +2075,7 @@ public struct ToolCallCompletedState: Codable, Sendable { toolCallId: String, toolName: String, displayName: String, - toolClientId: String? = nil, + contributor: ToolCallContributor? = nil, meta: [String: AnyCodable]? = nil, invocationMessage: StringOrMarkdown, toolInput: String? = nil, @@ -2054,7 +2091,7 @@ public struct ToolCallCompletedState: Codable, Sendable { self.toolCallId = toolCallId self.toolName = toolName self.displayName = displayName - self.toolClientId = toolClientId + self.contributor = contributor self.meta = meta self.invocationMessage = invocationMessage self.toolInput = toolInput @@ -2076,18 +2113,13 @@ public struct ToolCallCancelledState: Codable, Sendable { public var toolName: String /// Human-readable tool name public var displayName: String - /// If this tool is provided by a client, the `clientId` of the owning client. - /// Absent for server-side tools. - /// - /// When set, the identified client is responsible for executing the tool and - /// dispatching `session/toolCallComplete` with the result. - public var toolClientId: String? + /// Reference to the contributor of the tool being called. + public var contributor: ToolCallContributor? /// Additional provider-specific metadata for this tool call. /// - /// Clients MAY look for well-known keys here to provide enhanced UI. - /// For example, a `ptyTerminal` key with `{ input: string; output: string }` - /// indicates the tool operated on a terminal (both `input` and `output` may - /// contain escape sequences). + /// This MAY include a `ui` field corresponding to the MCP Apps (SEP-1865) + /// `McpUiToolMeta` found in MCP tool calls, which may be used in combination + /// with the {@link contributor} to serve MCP Apps. public var meta: [String: AnyCodable]? /// Message describing what the tool will do public var invocationMessage: StringOrMarkdown @@ -2107,7 +2139,7 @@ public struct ToolCallCancelledState: Codable, Sendable { case toolCallId case toolName case displayName - case toolClientId + case contributor case meta = "_meta" case invocationMessage case toolInput @@ -2122,7 +2154,7 @@ public struct ToolCallCancelledState: Codable, Sendable { toolCallId: String, toolName: String, displayName: String, - toolClientId: String? = nil, + contributor: ToolCallContributor? = nil, meta: [String: AnyCodable]? = nil, invocationMessage: StringOrMarkdown, toolInput: String? = nil, @@ -2135,7 +2167,7 @@ public struct ToolCallCancelledState: Codable, Sendable { self.toolCallId = toolCallId self.toolName = toolName self.displayName = displayName - self.toolClientId = toolClientId + self.contributor = contributor self.meta = meta self.invocationMessage = invocationMessage self.toolInput = toolInput @@ -2885,6 +2917,29 @@ public struct McpServerCustomization: Codable, Sendable { /// Absent when the customization covers the whole resource. public var range: TextRange? public var type: CustomizationType + /// Whether this MCP server is currently enabled. + public var enabled: Bool + /// Current lifecycle state of the MCP server. + public var state: McpServerState + /// An `mcp://`-protocol channel the client uses to side-channel traffic + /// into the upstream MCP server itself. The channel is NOT a fresh raw MCP + /// connection: it piggybacks on the AHP transport + /// and skips the MCP `initialize` sequence. + /// + /// The agent host MAY only serve a subset of MCP on this + /// channel; the served subset is described by domain-specific + /// capabilities such as those in + /// {@link McpServerCustomizationApps.capabilities}. + /// + /// The channel URI SHOULD be stable across the server's lifetime, but + /// the agent host MAY change it (for example across a restart) and + /// MAY only expose it while the server is in + /// {@link McpServerStatus.Ready | `Ready`}. Absence means no + /// side-channel is currently available. + public var channel: String? + /// MCP App support. This property SHOULD be advertised for MCP servers + /// which support apps. + public var mcpApp: McpServerCustomizationApps? public init( id: String, @@ -2892,7 +2947,11 @@ public struct McpServerCustomization: Codable, Sendable { name: String, icons: [Icon]? = nil, range: TextRange? = nil, - type: CustomizationType + type: CustomizationType, + enabled: Bool, + state: McpServerState, + channel: String? = nil, + mcpApp: McpServerCustomizationApps? = nil ) { self.id = id self.uri = uri @@ -2900,6 +2959,157 @@ public struct McpServerCustomization: Codable, Sendable { self.icons = icons self.range = range self.type = type + self.enabled = enabled + self.state = state + self.channel = channel + self.mcpApp = mcpApp + } +} + +public struct McpServerCustomizationApps: Codable, Sendable { + /// The subset of MCP App + /// [`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx) + /// the AHP host can satisfy for Views backed by this server. The + /// client feeds these straight through into the `hostCapabilities` of + /// the `ui/initialize` response delivered to the View. + public var capabilities: AhpMcpUiHostCapabilities + + public init( + capabilities: AhpMcpUiHostCapabilities + ) { + self.capabilities = capabilities + } +} + +public struct AhpMcpUiHostCapabilities: Codable, Sendable { + /// Producer proxies the MCP `tools/*` methods to the upstream server. + public var serverTools: AnyCodable? + /// Producer proxies the MCP `resources/*` methods to the upstream server. + public var serverResources: AnyCodable? + /// Producer accepts `notifications/message` log entries from the App via `mcpNotification`. + public var logging: [String: AnyCodable]? + /// Producer serves `sampling/createMessage` via `mcpMethodCall`. + public var sampling: AnyCodable? + + public init( + serverTools: AnyCodable? = nil, + serverResources: AnyCodable? = nil, + logging: [String: AnyCodable]? = nil, + sampling: AnyCodable? = nil + ) { + self.serverTools = serverTools + self.serverResources = serverResources + self.logging = logging + self.sampling = sampling + } +} + +public struct McpServerStartingState: Codable, Sendable { + public var kind: McpServerStatus + + public init( + kind: McpServerStatus + ) { + self.kind = kind + } +} + +public struct McpServerReadyState: Codable, Sendable { + public var kind: McpServerStatus + + public init( + kind: McpServerStatus + ) { + self.kind = kind + } +} + +public struct McpServerAuthRequiredState: Codable, Sendable { + public var kind: McpServerStatus + /// Why authentication is required. + public var reason: McpAuthRequiredReason + /// RFC 9728 Protected Resource Metadata. The `resource` field is the + /// canonical MCP server URI per RFC 8707, used as the OAuth `resource` + /// indicator. `authorization_servers` is REQUIRED by the MCP + /// authorization spec. + public var resource: ProtectedResourceMetadata + /// Scopes required for the current challenge, parsed from the + /// `WWW-Authenticate: Bearer scope="…"` header (or `scopes_supported` + /// fallback). Authoritative for the next authorization request — clients + /// MUST NOT assume any subset/superset relationship to + /// `resource.scopes_supported`. + public var requiredScopes: [String]? + /// Human-readable hint, typically from the OAuth `error_description`. + public var description: String? + + public init( + kind: McpServerStatus, + reason: McpAuthRequiredReason, + resource: ProtectedResourceMetadata, + requiredScopes: [String]? = nil, + description: String? = nil + ) { + self.kind = kind + self.reason = reason + self.resource = resource + self.requiredScopes = requiredScopes + self.description = description + } +} + +public struct McpServerErrorState: Codable, Sendable { + public var kind: McpServerStatus + /// Error details. + public var error: ErrorInfo + + public init( + kind: McpServerStatus, + error: ErrorInfo + ) { + self.kind = kind + self.error = error + } +} + +public struct McpServerStoppedState: Codable, Sendable { + public var kind: McpServerStatus + + public init( + kind: McpServerStatus + ) { + self.kind = kind + } +} + +public struct ToolCallClientContributor: Codable, Sendable { + public var kind: ToolCallContributorKind + /// If this tool is provided by a client, the `clientId` of the owning client. + /// Absent for server-side tools. + /// + /// When set, the identified client is responsible for executing the tool and + /// dispatching `session/toolCallComplete` with the result. + public var clientId: String + + public init( + kind: ToolCallContributorKind, + clientId: String + ) { + self.kind = kind + self.clientId = clientId + } +} + +public struct ToolCallMcpContributor: Codable, Sendable { + public var kind: ToolCallContributorKind + /// Customization ID of the corresponding MCP server in {@link SessionState.customizations}. + public var customizationId: String + + public init( + kind: ToolCallContributorKind, + customizationId: String + ) { + self.kind = kind + self.customizationId = customizationId } } @@ -3678,6 +3888,7 @@ public enum MessageAttachment: Codable, Sendable { public enum Customization: Codable, Sendable { case plugin(PluginCustomization) case directory(DirectoryCustomization) + case mcpServer(McpServerCustomization) private enum DiscriminantKey: String, CodingKey { case discriminant = "type" @@ -3691,6 +3902,8 @@ public enum Customization: Codable, Sendable { self = .plugin(try PluginCustomization(from: decoder)) case "directory": self = .directory(try DirectoryCustomization(from: decoder)) + case "mcpServer": + self = .mcpServer(try McpServerCustomization(from: decoder)) default: throw DecodingError.dataCorruptedError(forKey: .discriminant, in: container, debugDescription: "Unknown Customization discriminant: \(discriminant)") } @@ -3700,6 +3913,7 @@ public enum Customization: Codable, Sendable { switch self { case .plugin(let value): try value.encode(to: encoder) case .directory(let value): try value.encode(to: encoder) + case .mcpServer(let value): try value.encode(to: encoder) } } } @@ -3786,6 +4000,76 @@ public enum CustomizationLoadState: Codable, Sendable { } } +public enum McpServerState: Codable, Sendable { + case starting(McpServerStartingState) + case ready(McpServerReadyState) + case authRequired(McpServerAuthRequiredState) + case error(McpServerErrorState) + case stopped(McpServerStoppedState) + + private enum DiscriminantKey: String, CodingKey { + case discriminant = "kind" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DiscriminantKey.self) + let discriminant = try container.decode(String.self, forKey: .discriminant) + switch discriminant { + case "starting": + self = .starting(try McpServerStartingState(from: decoder)) + case "ready": + self = .ready(try McpServerReadyState(from: decoder)) + case "authRequired": + self = .authRequired(try McpServerAuthRequiredState(from: decoder)) + case "error": + self = .error(try McpServerErrorState(from: decoder)) + case "stopped": + self = .stopped(try McpServerStoppedState(from: decoder)) + default: + throw DecodingError.dataCorruptedError(forKey: .discriminant, in: container, debugDescription: "Unknown McpServerState discriminant: \(discriminant)") + } + } + + public func encode(to encoder: Encoder) throws { + switch self { + case .starting(let value): try value.encode(to: encoder) + case .ready(let value): try value.encode(to: encoder) + case .authRequired(let value): try value.encode(to: encoder) + case .error(let value): try value.encode(to: encoder) + case .stopped(let value): try value.encode(to: encoder) + } + } +} + +public enum ToolCallContributor: Codable, Sendable { + case client(ToolCallClientContributor) + case mcp(ToolCallMcpContributor) + + private enum DiscriminantKey: String, CodingKey { + case discriminant = "kind" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DiscriminantKey.self) + let discriminant = try container.decode(String.self, forKey: .discriminant) + switch discriminant { + case "client": + self = .client(try ToolCallClientContributor(from: decoder)) + case "mcp": + self = .mcp(try ToolCallMcpContributor(from: decoder)) + default: + throw DecodingError.dataCorruptedError(forKey: .discriminant, in: container, debugDescription: "Unknown ToolCallContributor discriminant: \(discriminant)") + } + } + + public func encode(to encoder: Encoder) throws { + switch self { + case .client(let value): try value.encode(to: encoder) + case .mcp(let value): try value.encode(to: encoder) + } + } +} + public enum ToolResultContent: Codable, Sendable { case text(ToolResultTextContent) case embeddedResource(ToolResultEmbeddedResourceContent) diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/NativeReducer.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/NativeReducer.swift index 416d8d45..6ed7b835 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/NativeReducer.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/NativeReducer.swift @@ -190,7 +190,7 @@ public struct AHPSessionReducer: Reducer { toolCallId: a.toolCallId, toolName: a.toolName, displayName: a.displayName, - toolClientId: a.toolClientId, + contributor: a.contributor, meta: a.meta, status: .streaming )) @@ -220,7 +220,7 @@ public struct AHPSessionReducer: Reducer { toolCallId: base.toolCallId, toolName: base.toolName, displayName: base.displayName, - toolClientId: base.toolClientId, + contributor: base.contributor, meta: base.meta, invocationMessage: a.invocationMessage, toolInput: a.toolInput, @@ -232,7 +232,7 @@ public struct AHPSessionReducer: Reducer { toolCallId: base.toolCallId, toolName: base.toolName, displayName: base.displayName, - toolClientId: base.toolClientId, + contributor: base.contributor, meta: base.meta, invocationMessage: a.invocationMessage, toolInput: a.toolInput, @@ -256,7 +256,7 @@ public struct AHPSessionReducer: Reducer { toolCallId: base.toolCallId, toolName: base.toolName, displayName: base.displayName, - toolClientId: base.toolClientId, + contributor: base.contributor, meta: base.meta, invocationMessage: pending.invocationMessage, toolInput: a.editedToolInput ?? pending.toolInput, @@ -269,7 +269,7 @@ public struct AHPSessionReducer: Reducer { toolCallId: base.toolCallId, toolName: base.toolName, displayName: base.displayName, - toolClientId: base.toolClientId, + contributor: base.contributor, meta: base.meta, invocationMessage: pending.invocationMessage, toolInput: pending.toolInput, @@ -310,7 +310,7 @@ public struct AHPSessionReducer: Reducer { toolCallId: base.toolCallId, toolName: base.toolName, displayName: base.displayName, - toolClientId: base.toolClientId, + contributor: base.contributor, meta: base.meta, invocationMessage: invocationMessage, toolInput: toolInput, @@ -328,7 +328,7 @@ public struct AHPSessionReducer: Reducer { toolCallId: base.toolCallId, toolName: base.toolName, displayName: base.displayName, - toolClientId: base.toolClientId, + contributor: base.contributor, meta: base.meta, invocationMessage: invocationMessage, toolInput: toolInput, @@ -354,7 +354,7 @@ public struct AHPSessionReducer: Reducer { toolCallId: base.toolCallId, toolName: base.toolName, displayName: base.displayName, - toolClientId: base.toolClientId, + contributor: base.contributor, meta: base.meta, invocationMessage: prc.invocationMessage, toolInput: prc.toolInput, @@ -372,7 +372,7 @@ public struct AHPSessionReducer: Reducer { toolCallId: base.toolCallId, toolName: base.toolName, displayName: base.displayName, - toolClientId: base.toolClientId, + contributor: base.contributor, meta: base.meta, invocationMessage: prc.invocationMessage, toolInput: prc.toolInput, @@ -470,6 +470,30 @@ public struct AHPSessionReducer: Reducer { } } + case .sessionMcpServerStatusChanged(let a): + guard var list = state.customizations else { return } + if let topIdx = list.firstIndex(where: { customizationId($0) == a.id }) { + guard case .mcpServer(var entry) = list[topIdx] else { return } + entry.state = a.state + entry.channel = a.channel + list[topIdx] = .mcpServer(entry) + state.customizations = list + return + } + for containerIdx in list.indices { + var container = list[containerIdx] + guard var children = customizationChildren(container) else { continue } + guard let childIdx = children.firstIndex(where: { childId($0) == a.id }) else { continue } + guard case .mcpServer(var child) = children[childIdx] else { continue } + child.state = a.state + child.channel = a.channel + children[childIdx] = .mcpServer(child) + setCustomizationChildren(&container, children) + list[containerIdx] = container + state.customizations = list + return + } + // ── Truncation ──────────────────────────────────────────────────────── case .sessionTruncated(let a): @@ -642,7 +666,7 @@ public struct AHPSessionReducer: Reducer { toolCallId: base.toolCallId, toolName: base.toolName, displayName: base.displayName, - toolClientId: base.toolClientId, + contributor: base.contributor, meta: base.meta, invocationMessage: invocationMessage, toolInput: toolInput, @@ -796,6 +820,7 @@ func customizationId(_ c: Customization) -> String { switch c { case .plugin(let p): return p.id case .directory(let d): return d.id + case .mcpServer(let m): return m.id } } @@ -814,6 +839,7 @@ func customizationChildren(_ c: Customization) -> [ChildCustomization]? { switch c { case .plugin(let p): return p.children case .directory(let d): return d.children + case .mcpServer: return nil } } @@ -825,6 +851,8 @@ func setCustomizationChildren(_ c: inout Customization, _ children: [ChildCustom case .directory(var d): d.children = children c = .directory(d) + case .mcpServer: + break } } @@ -836,6 +864,9 @@ func setCustomizationEnabled(_ c: inout Customization, _ enabled: Bool) { case .directory(var d): d.enabled = enabled c = .directory(d) + case .mcpServer(var m): + m.enabled = enabled + c = .mcpServer(m) } } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift index f6524a86..08b68a1d 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift @@ -144,7 +144,7 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS toolCallId: a.toolCallId, toolName: a.toolName, displayName: a.displayName, - toolClientId: a.toolClientId, + contributor: a.contributor, meta: a.meta, status: .streaming )) @@ -177,7 +177,7 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS toolCallId: base.toolCallId, toolName: base.toolName, displayName: base.displayName, - toolClientId: base.toolClientId, + contributor: base.contributor, meta: base.meta, invocationMessage: a.invocationMessage, toolInput: a.toolInput, @@ -189,7 +189,7 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS toolCallId: base.toolCallId, toolName: base.toolName, displayName: base.displayName, - toolClientId: base.toolClientId, + contributor: base.contributor, meta: base.meta, invocationMessage: a.invocationMessage, toolInput: a.toolInput, @@ -211,7 +211,7 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS toolCallId: base.toolCallId, toolName: base.toolName, displayName: base.displayName, - toolClientId: base.toolClientId, + contributor: base.contributor, meta: base.meta, invocationMessage: pending.invocationMessage, toolInput: a.editedToolInput ?? pending.toolInput, @@ -224,7 +224,7 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS toolCallId: base.toolCallId, toolName: base.toolName, displayName: base.displayName, - toolClientId: base.toolClientId, + contributor: base.contributor, meta: base.meta, invocationMessage: pending.invocationMessage, toolInput: pending.toolInput, @@ -263,7 +263,7 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS toolCallId: base.toolCallId, toolName: base.toolName, displayName: base.displayName, - toolClientId: base.toolClientId, + contributor: base.contributor, meta: base.meta, invocationMessage: invocationMessage, toolInput: toolInput, @@ -281,7 +281,7 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS toolCallId: base.toolCallId, toolName: base.toolName, displayName: base.displayName, - toolClientId: base.toolClientId, + contributor: base.contributor, meta: base.meta, invocationMessage: invocationMessage, toolInput: toolInput, @@ -305,7 +305,7 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS toolCallId: base.toolCallId, toolName: base.toolName, displayName: base.displayName, - toolClientId: base.toolClientId, + contributor: base.contributor, meta: base.meta, invocationMessage: prc.invocationMessage, toolInput: prc.toolInput, @@ -323,7 +323,7 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS toolCallId: base.toolCallId, toolName: base.toolName, displayName: base.displayName, - toolClientId: base.toolClientId, + contributor: base.contributor, meta: base.meta, invocationMessage: prc.invocationMessage, toolInput: prc.toolInput, @@ -456,6 +456,36 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS } return state + case .sessionMcpServerStatusChanged(let a): + guard var list = state.customizations else { return state } + if let topIdx = list.firstIndex(where: { customizationId($0) == a.id }) { + guard case .mcpServer(var entry) = list[topIdx] else { return state } + entry.state = a.state + entry.channel = a.channel + list[topIdx] = .mcpServer(entry) + var next = state + next.customizations = list + return next + } + var changed = false + for containerIdx in list.indices { + var container = list[containerIdx] + guard var children = customizationChildren(container) else { continue } + guard let childIdx = children.firstIndex(where: { childId($0) == a.id }) else { continue } + guard case .mcpServer(var child) = children[childIdx] else { continue } + child.state = a.state + child.channel = a.channel + children[childIdx] = .mcpServer(child) + setCustomizationChildren(&container, children) + list[containerIdx] = container + changed = true + break + } + guard changed else { return state } + var next = state + next.customizations = list + return next + // ── Truncation ──────────────────────────────────────────────────────── case .sessionTruncated(let a): @@ -738,7 +768,7 @@ private func endTurn( toolCallId: base.toolCallId, toolName: base.toolName, displayName: base.displayName, - toolClientId: base.toolClientId, + contributor: base.contributor, meta: base.meta, invocationMessage: invocationMessage, toolInput: toolInput, diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/ToolCallStateExtensions.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/ToolCallStateExtensions.swift index 5a9a7193..dc152560 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/ToolCallStateExtensions.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/ToolCallStateExtensions.swift @@ -28,7 +28,7 @@ public struct ToolCallBaseFields: Sendable { public let toolCallId: String public let toolName: String public let displayName: String - public let toolClientId: String? + public let contributor: ToolCallContributor? public let meta: [String: AnyCodable]? } @@ -38,22 +38,22 @@ extension ToolCallState { switch self { case .streaming(let s): return ToolCallBaseFields(toolCallId: s.toolCallId, toolName: s.toolName, - displayName: s.displayName, toolClientId: s.toolClientId, meta: s.meta) + displayName: s.displayName, contributor: s.contributor, meta: s.meta) case .pendingConfirmation(let s): return ToolCallBaseFields(toolCallId: s.toolCallId, toolName: s.toolName, - displayName: s.displayName, toolClientId: s.toolClientId, meta: s.meta) + displayName: s.displayName, contributor: s.contributor, meta: s.meta) case .running(let s): return ToolCallBaseFields(toolCallId: s.toolCallId, toolName: s.toolName, - displayName: s.displayName, toolClientId: s.toolClientId, meta: s.meta) + displayName: s.displayName, contributor: s.contributor, meta: s.meta) case .pendingResultConfirmation(let s): return ToolCallBaseFields(toolCallId: s.toolCallId, toolName: s.toolName, - displayName: s.displayName, toolClientId: s.toolClientId, meta: s.meta) + displayName: s.displayName, contributor: s.contributor, meta: s.meta) case .completed(let s): return ToolCallBaseFields(toolCallId: s.toolCallId, toolName: s.toolName, - displayName: s.displayName, toolClientId: s.toolClientId, meta: s.meta) + displayName: s.displayName, contributor: s.contributor, meta: s.meta) case .cancelled(let s): return ToolCallBaseFields(toolCallId: s.toolCallId, toolName: s.toolName, - displayName: s.displayName, toolClientId: s.toolClientId, meta: s.meta) + displayName: s.displayName, contributor: s.contributor, meta: s.meta) } } } diff --git a/clients/swift/CHANGELOG.md b/clients/swift/CHANGELOG.md index 69b1ea1b..3ea22e4e 100644 --- a/clients/swift/CHANGELOG.md +++ b/clients/swift/CHANGELOG.md @@ -17,6 +17,34 @@ the tag matches the version pinned in [`VERSION`](VERSION). ## [Unreleased] +### Added + +- `McpServerCustomization` now exposes the full MCP lifecycle: `enabled`, + the discriminated `McpServerState` enum + (`.starting`/`.ready`/`.authRequired`/`.error`/`.stopped`), optional + `channel` URI for the `mcp://` side-channel, and optional `mcpApp` + block carrying `AhpMcpUiHostCapabilities` for MCP Apps. +- `McpServerAuthRequiredState` carries `ProtectedResourceMetadata` + plus `reason` / `requiredScopes` / `description` so the existing + `authenticate` command can drive per-server auth. +- `Customization.mcpServer` top-level case — hosts MAY surface bare + MCP servers directly rather than only inside a plugin or directory. +- `SessionMcpServerStateChangedAction` and matching reducer arm — + narrow upsert of `state` + `channel` on an existing MCP + server customization by id. Wired through both `Reducers.swift` and + the protocol-based `NativeReducer.swift`. +- `ClientCapabilities` struct on `InitializeParams.capabilities` with + first entry `mcpApps`. + +### Changed + +- `ToolCallBase.toolClientId: String?` replaced by + `ToolCallBase.contributor: ToolCallContributor?` (enum with + `.client(ToolCallClientContributor)` and `.mcp(ToolCallMcpContributor)` + cases). `SessionToolCallStartAction` carries the new `contributor` + field as well. `Reducers.swift`, `NativeReducer.swift`, and + `ToolCallStateExtensions.swift` follow the rename. + ## [0.2.0] — 2026-05-28 Implements AHP `0.2.0`. diff --git a/clients/typescript/CHANGELOG.md b/clients/typescript/CHANGELOG.md index 121c4abf..55e42cbf 100644 --- a/clients/typescript/CHANGELOG.md +++ b/clients/typescript/CHANGELOG.md @@ -20,6 +20,33 @@ hotfix escape hatch. ## [Unreleased] +### 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 `McpServerCustomization` + — hosts MAY surface bare MCP servers directly rather than only inside + a plugin or directory. +- `session/mcpServerStateChanged` action and matching reducer case — + narrow upsert of `state` + `channel` on an existing MCP + server customization by id. +- `ClientCapabilities` type on `InitializeParams.capabilities` with + first entry `mcpApps`. + +### Changed + +- `ToolCallBase.toolClientId?: string` replaced by + `ToolCallBase.contributor?: ToolCallContributor` (discriminated union + with `{ kind: 'client'; clientId }` and `{ kind: 'mcp'; customizationId }` + variants). `session/toolCallStart` carries the new `contributor` + field as well. + ## [0.2.0] — 2026-05-28 Implements AHP `0.2.0`. diff --git a/docs/guide/customizations.md b/docs/guide/customizations.md index 22b37d02..193780b8 100644 --- a/docs/guide/customizations.md +++ b/docs/guide/customizations.md @@ -114,10 +114,10 @@ SkillCustomization { type: 'skill'; description?, disableModelInvoc PromptCustomization { type: 'prompt'; description? } RuleCustomization { type: 'rule'; description?, alwaysApply?, globs? } // covers "instruction" formats too HookCustomization { type: 'hook'; event?, matcher? } -McpServerCustomization { type: 'mcpServer'; enabled, runtimeStatus, channel?, mcpApp? } // see /guide/mcp +McpServerCustomization { type: 'mcpServer'; enabled, state, channel?, mcpApp? } // see /guide/mcp ``` -The protocol intentionally omits host-internal execution details (a hook's command/script, an MCP server's `command`/`args`/`env`, etc.). Those stay on the agent host; clients see only what's needed for display, search, and selection. MCP tools and their descriptions surface through the standard tool channels once the server is running. The MCP-specific runtime fields (`runtimeStatus`, `channel`, `mcpApp`) are covered in [MCP Servers](/guide/mcp). +The protocol intentionally omits host-internal execution details (a hook's command/script, an MCP server's `command`/`args`/`env`, etc.). Those stay on the agent host; clients see only what's needed for display, search, and selection. MCP tools and their descriptions surface through the standard tool channels once the server is running. The MCP-specific runtime fields (`state`, `channel`, `mcpApp`) are covered in [MCP Servers](/guide/mcp). Consumers filter by `type` to find the children they care about — for example, the agent picker reads every `AgentCustomization` under any container: diff --git a/docs/guide/mcp.md b/docs/guide/mcp.md index ca740d97..a43a791f 100644 --- a/docs/guide/mcp.md +++ b/docs/guide/mcp.md @@ -34,7 +34,7 @@ McpServerCustomization { icons?: Icon[] range?: TextRange // span inside `uri` for inline declarations enabled: boolean // user-toggleable (see Customizations guide) - runtimeStatus: McpServerStatus // discriminated union — see below + state: McpServerState // discriminated union — see below channel?: URI // optional mcp:// side-channel mcpApp?: McpServerCustomizationApps } @@ -44,7 +44,7 @@ McpServerCustomization { ## Runtime status -`runtimeStatus` is a [discriminated union on `kind`](/reference/session#mcpserverstatus). It is the host's view of the server's lifecycle, separate from `enabled` (which is the user's intent). +`state` is a [discriminated union on `kind`](/reference/session#mcpserverstatus). It is the host's view of the server's lifecycle, separate from `enabled` (which is the user's intent). ```mermaid stateDiagram-v2 @@ -76,7 +76,7 @@ stateDiagram-v2 | `error` | Unrecoverable failure. Carries an `ErrorInfo`. Use `authRequired` for auth-specific failures. | | `stopped` | Shut down. The host MAY remove the entry shortly after. | -High-frequency lifecycle transitions go through the narrow [`session/mcpServerStatusChanged`](/reference/session#sessionmcpserverstatuschangedaction) action, which upserts just `runtimeStatus` (and optionally `channel`) on an existing entry. Use `session/customizationUpdated` for anything else (name, icons, `mcpApp`). +High-frequency lifecycle transitions go through the narrow [`session/mcpServerStateChanged`](/reference/session#sessionmcpserverstatuschangedaction) action, which upserts just `state` (and optionally `channel`) on an existing entry. Use `session/customizationUpdated` for anything else (name, icons, `mcpApp`). ## Authentication @@ -89,7 +89,7 @@ sequenceDiagram participant AS as Authorization Server Note over Host: MCP server returns 401 with PRM - Host->>Client: customizationUpdated (runtimeStatus: authRequired, resource: PRM) + Host->>Client: customizationUpdated (state: authRequired, resource: PRM) Client->>AS: OAuth flow against PRM.authorization_servers AS-->>Client: Bearer token @@ -97,10 +97,10 @@ sequenceDiagram Client->>Host: authenticate({ channel: 'ahp-root://', resource, token }) Host-->>Client: {} - Host->>Client: customizationUpdated (runtimeStatus: ready) + Host->>Client: customizationUpdated (state: ready) ``` -`McpServerStatusAuthRequired` carries: +`McpServerAuthRequiredState` carries: - **`reason`** — `required`, `expired`, or `insufficientScope`. Mirrors the [MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization.md) failure modes. - **`resource`** — [`ProtectedResourceMetadata`](/reference/common#protectedresourcemetadata) per [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728). The `resource` field inside is the canonical MCP server URI (per RFC 8707) and what the client passes back as `authenticate({ resource })`. @@ -112,14 +112,14 @@ sequenceDiagram `reason: 'insufficientScope'` almost always surfaces *during* a tool call — the model invokes an MCP tool, the upstream server responds 403, and a turn that was happily streaming suddenly needs the user to grant more access. AHP couples this case through two signals so it can't be missed: 1. **The host SHOULD raise [`SessionStatus.InputNeeded`](/reference/session#sessionstatus) on the session** when it transitions an MCP server to `authRequired` because of an in-flight request. This makes the block visible at the session-summary level, exactly like a tool confirmation or input request. -2. **Clients SHOULD watch the `runtimeStatus.kind` of any MCP server backing a running tool call** (via [`ToolCallContributor`](/reference/session#toolcallcontributor) — `{ kind: 'mcp', customizationId }`). When that server flips to `authRequired`, the client SHOULD present an explicit affordance tied to *that tool call* (e.g. an inline "grant additional access" button), rather than relying on the user to spot a status badge on the server's customization entry. +2. **Clients SHOULD watch the `state.kind` of any MCP server backing a running tool call** (via [`ToolCallContributor`](/reference/session#toolcallcontributor) — `{ kind: 'mcp', customizationId }`). When that server flips to `authRequired`, the client SHOULD present an explicit affordance tied to *that tool call* (e.g. an inline "grant additional access" button), rather than relying on the user to spot a status badge on the server's customization entry. The same monitoring pattern also covers `reason: 'expired'` mid-turn — the difference is purely whether the user needs to re-authenticate or grant additional scopes. Per-agent protected resources in [`AgentInfo.protectedResources`](/reference/root#agentinfo) cover agents themselves. MCP server resources are advertised here, on the customization, so a single agent can carry an arbitrary number of MCP servers each with their own authorization servers without bloating the root state. ::: tip -The existing `authenticate` command requires `resource` to match one declared by an agent. Hosts that surface MCP server auth via `McpServerStatusAuthRequired` either need to widen that rule or mirror MCP server resources into `AgentInfo.protectedResources` until the dedicated MCP actions land. This is a known gap and will be tightened when the MCP-specific action surface is specified. +The existing `authenticate` command requires `resource` to match one declared by an agent. Hosts that surface MCP server auth via `McpServerAuthRequiredState` either need to widen that rule or mirror MCP server resources into `AgentInfo.protectedResources` until the dedicated MCP actions land. This is a known gap and will be tightened when the MCP-specific action surface is specified. ::: ## Where MCP tools live @@ -246,4 +246,4 @@ The client is responsible for translating any locally-supported `ui/*` request i ## Next steps - [`mcp://` channel](/specification/mcp-channel) — the side-channel clients use to talk to the upstream server. -- [Session Channel Reference](/reference/session) — full type definitions for `McpServerCustomization`, `McpServerStatus`, and friends. +- [Session Channel Reference](/reference/session) — full type definitions for `McpServerCustomization`, `McpServerState`, and friends. diff --git a/docs/specification/mcp-channel.md b/docs/specification/mcp-channel.md index b623f399..87b51d14 100644 --- a/docs/specification/mcp-channel.md +++ b/docs/specification/mcp-channel.md @@ -42,7 +42,7 @@ The channel URI is exposed on the customization that owns it. Today that means [ The URI itself is opaque to the client. Its scheme is `mcp://`; its path and authority are host-defined. - There is at most one channel per customization within a session. -- The host MAY only expose `channel` while the owning customization is in a usable runtime state (for `McpServerCustomization` that's `runtimeStatus.kind === 'ready'`). When that condition no longer holds, the host MAY clear `channel` via the customization's update action. Clients SHOULD treat the channel as unavailable while it is absent. +- The host MAY only expose `channel` while the owning customization is in a usable runtime state (for `McpServerCustomization` that's `state.kind === 'ready'`). When that condition no longer holds, the host MAY clear `channel` via the customization's update action. Clients SHOULD treat the channel as unavailable while it is absent. - The URI SHOULD be stable across the customization's lifetime, but the host MAY change it (for example after a restart). Clients MUST re-read `channel` whenever the customization is updated. - The channel is only present when the owning customization advertises at least one capability set requiring it. Customizations without such an advertisement do not need a side-channel — their state is already covered by AHP's normal flows. diff --git a/schema/actions.schema.json b/schema/actions.schema.json index 7eb4c8f3..6d1ecde9 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -965,30 +965,30 @@ "id" ] }, - "SessionMcpServerStatusChangedAction": { + "SessionMcpServerStateChangedAction": { "type": "object", - "description": "Updates the runtime fields of an existing\n{@link McpServerCustomization} — narrow alternative to\n{@link SessionCustomizationUpdatedAction} for the high-frequency\n`starting` ↔ `ready` ↔ `authRequired` transitions.\n\nLocates the target entry by `id`, searching both the top-level\ncustomization list and the `children` array of every container.\nReplaces the entry's {@link McpServerCustomization.runtimeStatus | `runtimeStatus`}\nand {@link McpServerCustomization.channel | `channel`}\n(full-replacement semantics: omit `channel` to clear an existing\nchannel URI). Other fields of the customization are preserved.\n\nIs a no-op when no matching `McpServerCustomization` is found. To\nupdate any other field (name, icons, `mcpApp` capabilities, etc.) use\n{@link SessionCustomizationUpdatedAction} instead.\n\nWhen the transition is to {@link McpServerStatusKind.AuthRequired}\nbecause of a request issued mid-turn, the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session — see\n{@link McpServerStatusAuthRequired} for the rationale.", + "description": "Updates the runtime fields of an existing\n{@link McpServerCustomization} — narrow alternative to\n{@link SessionCustomizationUpdatedAction} for the high-frequency\n`starting` ↔ `ready` ↔ `authRequired` transitions.\n\nLocates the target entry by `id`, searching both the top-level\ncustomization list and the `children` array of every container.\nReplaces the entry's {@link McpServerCustomization.state | `state`}\nand {@link McpServerCustomization.channel | `channel`}\n(full-replacement semantics: omit `channel` to clear an existing\nchannel URI). Other fields of the customization are preserved.\n\nIs a no-op when no matching `McpServerCustomization` is found. To\nupdate any other field (name, icons, `mcpApp` capabilities, etc.) use\n{@link SessionCustomizationUpdatedAction} instead.\n\nWhen the transition is to {@link McpServerStatus.AuthRequired}\nbecause of a request issued mid-turn, the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session — see\n{@link McpServerAuthRequiredState} for the rationale.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionMcpServerStatusChanged" + "$ref": "#/$defs/ActionType.SessionMcpServerStateChanged" }, "id": { "type": "string", "description": "The id of the {@link McpServerCustomization} to update." }, - "runtimeStatus": { - "$ref": "#/$defs/McpServerStatus", - "description": "The new runtime status." + "state": { + "$ref": "#/$defs/McpServerState", + "description": "The new lifecycle state." }, "channel": { "$ref": "#/$defs/URI", - "description": "Updated `mcp://` side-channel URI. Full-replacement: omit to clear\nan existing channel (typical when leaving\n{@link McpServerStatusKind.Ready | `Ready`})." + "description": "Updated `mcp://` side-channel URI. Full-replacement: omit to clear\nan existing channel (typical when leaving\n{@link McpServerStatus.Ready | `Ready`})." } }, "required": [ "type", "id", - "runtimeStatus" + "state" ] }, "SessionConfigChangedAction": { @@ -4685,13 +4685,13 @@ "type": "boolean", "description": "Whether this MCP server is currently enabled." }, - "runtimeStatus": { - "$ref": "#/$defs/McpServerStatus", - "description": "Current status of the MCP server." + "state": { + "$ref": "#/$defs/McpServerState", + "description": "Current lifecycle state of the MCP server." }, "channel": { "$ref": "#/$defs/URI", - "description": "An `mcp://`-protocol channel the client uses to side-channel traffic\ninto the upstream MCP server itself. The channel is NOT a fresh raw MCP\nconnection: it piggybacks on the AHP transport\nand skips the MCP `initialize` sequence.\n\nThe agent host MAY only serve a subset of MCP on this\nchannel; the served subset is described by domain-specific\ncapabilities such as those in\n{@link McpServerCustomizationApps.capabilities}.\n\nThe channel URI SHOULD be stable across the server's lifetime, but\nthe agent host MAY change it (for example across a restart) and\nMAY only expose it while the server is in\n{@link McpServerStatusKind.Ready | `Ready`}. Absence means no\nside-channel is currently available." + "description": "An `mcp://`-protocol channel the client uses to side-channel traffic\ninto the upstream MCP server itself. The channel is NOT a fresh raw MCP\nconnection: it piggybacks on the AHP transport\nand skips the MCP `initialize` sequence.\n\nThe agent host MAY only serve a subset of MCP on this\nchannel; the served subset is described by domain-specific\ncapabilities such as those in\n{@link McpServerCustomizationApps.capabilities}.\n\nThe channel URI SHOULD be stable across the server's lifetime, but\nthe agent host MAY change it (for example across a restart) and\nMAY only expose it while the server is in\n{@link McpServerStatus.Ready | `Ready`}. Absence means no\nside-channel is currently available." }, "mcpApp": { "$ref": "#/$defs/McpServerCustomizationApps", @@ -4704,7 +4704,7 @@ "name", "type", "enabled", - "runtimeStatus" + "state" ] }, "McpServerCustomizationApps": { @@ -4758,36 +4758,36 @@ } } }, - "McpServerStatusStarting": { + "McpServerStartingState": { "type": "object", "description": "Server is registered with the host but has not yet started.", "properties": { "kind": { - "$ref": "#/$defs/McpServerStatusKind.Starting" + "$ref": "#/$defs/McpServerStatus.Starting" } }, "required": [ "kind" ] }, - "McpServerStatusReady": { + "McpServerReadyState": { "type": "object", "description": "Server is running and serving requests.", "properties": { "kind": { - "$ref": "#/$defs/McpServerStatusKind.Ready" + "$ref": "#/$defs/McpServerStatus.Ready" } }, "required": [ "kind" ] }, - "McpServerStatusAuthRequired": { + "McpServerAuthRequiredState": { "type": "object", "description": "Server is reachable but cannot serve requests until the client\nauthenticates. Mirrors the discovery flow defined by\n[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)\n(Protected Resource Metadata) and the OAuth 2.1 / RFC 6750 challenge\nsemantics required by the MCP authorization spec.\n\nClients react to this state by calling the existing `authenticate`\ncommand with the {@link ProtectedResourceMetadata.resource | resource}\ncarried here. There is **no** `notify/authRequired` notification for\nMCP servers — the action stream is the single source of truth.\n\nWhen the transition is triggered by a request issued during a turn\n— most commonly\n{@link McpAuthRequiredReason.InsufficientScope | `InsufficientScope`}\nsurfacing mid-tool-call — the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session so the block is\nvisible at the summary level. Clients SHOULD watch this status on\nany MCP server backing a running tool call and surface an explicit\naffordance (e.g. a \"grant additional access\" prompt) tied to that\ntool call, rather than relying on the user to notice the\ncustomization’s status badge.", "properties": { "kind": { - "$ref": "#/$defs/McpServerStatusKind.AuthRequired" + "$ref": "#/$defs/McpServerStatus.AuthRequired" }, "reason": { "$ref": "#/$defs/McpAuthRequiredReason", @@ -4815,12 +4815,12 @@ "resource" ] }, - "McpServerStatusError": { + "McpServerErrorState": { "type": "object", - "description": "Server failed to start, crashed, or otherwise transitioned to a\nnon-recoverable error. Use {@link McpServerStatusKind.AuthRequired}\nfor authentication failures.", + "description": "Server failed to start, crashed, or otherwise transitioned to a\nnon-recoverable error. Use {@link McpServerStatus.AuthRequired}\nfor authentication failures.", "properties": { "kind": { - "$ref": "#/$defs/McpServerStatusKind.Error" + "$ref": "#/$defs/McpServerStatus.Error" }, "error": { "$ref": "#/$defs/ErrorInfo", @@ -4832,12 +4832,12 @@ "error" ] }, - "McpServerStatusStopped": { + "McpServerStoppedState": { "type": "object", "description": "Server has been shut down. The host MAY remove the server from the\nsession entirely shortly after this state.", "properties": { "kind": { - "$ref": "#/$defs/McpServerStatusKind.Stopped" + "$ref": "#/$defs/McpServerStatus.Stopped" } }, "required": [ @@ -5477,26 +5477,26 @@ ], "description": "A top-level customization active in a session. Either a container\n({@link PluginCustomization} or {@link DirectoryCustomization}) whose\nleaf customizations live in its\n{@link ContainerCustomizationBase.children | `children`} array, or a\nbare {@link McpServerCustomization} surfaced directly by the host." }, - "McpServerStatus": { + "McpServerState": { "oneOf": [ {}, { - "$ref": "#/$defs/McpServerStatusStarting" + "$ref": "#/$defs/McpServerStartingState" }, { - "$ref": "#/$defs/McpServerStatusReady" + "$ref": "#/$defs/McpServerReadyState" }, { - "$ref": "#/$defs/McpServerStatusAuthRequired" + "$ref": "#/$defs/McpServerAuthRequiredState" }, { - "$ref": "#/$defs/McpServerStatusError" + "$ref": "#/$defs/McpServerErrorState" }, { - "$ref": "#/$defs/McpServerStatusStopped" + "$ref": "#/$defs/McpServerStoppedState" } ], - "description": "Discriminated union of all MCP server statuses. Discriminated by `kind`." + "description": "Discriminated union of all MCP server lifecycle states.\nDiscriminated by `kind` (a {@link McpServerStatus} value)." }, "TerminalClaim": { "oneOf": [ @@ -5639,7 +5639,7 @@ "$ref": "#/$defs/SessionCustomizationRemovedAction" }, { - "$ref": "#/$defs/SessionMcpServerStatusChangedAction" + "$ref": "#/$defs/SessionMcpServerStateChangedAction" }, { "$ref": "#/$defs/SessionTruncatedAction" diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 71f221b2..659037fc 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -49,6 +49,10 @@ "locale": { "type": "string", "description": "IETF BCP 47 language tag indicating the client's preferred locale\n(e.g. `\"en-US\"`, `\"ja\"`). The server SHOULD use this to localise\nuser-facing strings such as confirmation option labels." + }, + "capabilities": { + "$ref": "#/$defs/ClientCapabilities", + "description": "Optional client capability declarations.\n\nServers SHOULD only advertise features whose corresponding client\ncapability is set here. Absent means \"not declared\" — the server\nMUST assume the client does not support the feature." } }, "required": [ @@ -57,6 +61,17 @@ "clientId" ] }, + "ClientCapabilities": { + "type": "object", + "description": "Optional capabilities a client declares during `initialize`.\n\nEach field is a presence flag: an empty object `{}` means \"supported\",\nabsence means \"not supported\". Sub-fields on individual capabilities\nare reserved for future per-capability options.", + "properties": { + "mcpApps": { + "type": "object", + "additionalProperties": {}, + "description": "Client can render\n[MCP Apps](https://github.com/modelcontextprotocol/ext-apps) — i.e.\nit can host the View sandbox, run the `ui/*` protocol against it,\nand forward `mcp://`-channel traffic on the App's behalf.\n\nHosts SHOULD only populate\n{@link McpServerCustomization.mcpApp | `McpServerCustomization.mcpApp`}\n(and expose the corresponding\n{@link McpServerCustomization.channel | `mcp://` channel}) when this\ncapability is declared. Clients that omit it MUST treat\nApp-bearing tool calls as ordinary MCP tool calls." + } + } + }, "InitializeResult": { "type": "object", "description": "Result of the `initialize` command.\n\n`protocolVersion` is the version the server has selected from the client's\n`protocolVersions` list. The client and server MUST use this version for\nthe rest of the connection. If the server cannot speak any of the offered\nversions it MUST return error code `-32005` (`UnsupportedProtocolVersion`)\ninstead of a result.", @@ -4336,13 +4351,13 @@ "type": "boolean", "description": "Whether this MCP server is currently enabled." }, - "runtimeStatus": { - "$ref": "#/$defs/McpServerStatus", - "description": "Current status of the MCP server." + "state": { + "$ref": "#/$defs/McpServerState", + "description": "Current lifecycle state of the MCP server." }, "channel": { "$ref": "#/$defs/URI", - "description": "An `mcp://`-protocol channel the client uses to side-channel traffic\ninto the upstream MCP server itself. The channel is NOT a fresh raw MCP\nconnection: it piggybacks on the AHP transport\nand skips the MCP `initialize` sequence.\n\nThe agent host MAY only serve a subset of MCP on this\nchannel; the served subset is described by domain-specific\ncapabilities such as those in\n{@link McpServerCustomizationApps.capabilities}.\n\nThe channel URI SHOULD be stable across the server's lifetime, but\nthe agent host MAY change it (for example across a restart) and\nMAY only expose it while the server is in\n{@link McpServerStatusKind.Ready | `Ready`}. Absence means no\nside-channel is currently available." + "description": "An `mcp://`-protocol channel the client uses to side-channel traffic\ninto the upstream MCP server itself. The channel is NOT a fresh raw MCP\nconnection: it piggybacks on the AHP transport\nand skips the MCP `initialize` sequence.\n\nThe agent host MAY only serve a subset of MCP on this\nchannel; the served subset is described by domain-specific\ncapabilities such as those in\n{@link McpServerCustomizationApps.capabilities}.\n\nThe channel URI SHOULD be stable across the server's lifetime, but\nthe agent host MAY change it (for example across a restart) and\nMAY only expose it while the server is in\n{@link McpServerStatus.Ready | `Ready`}. Absence means no\nside-channel is currently available." }, "mcpApp": { "$ref": "#/$defs/McpServerCustomizationApps", @@ -4355,7 +4370,7 @@ "name", "type", "enabled", - "runtimeStatus" + "state" ] }, "McpServerCustomizationApps": { @@ -4409,36 +4424,36 @@ } } }, - "McpServerStatusStarting": { + "McpServerStartingState": { "type": "object", "description": "Server is registered with the host but has not yet started.", "properties": { "kind": { - "$ref": "#/$defs/McpServerStatusKind.Starting" + "$ref": "#/$defs/McpServerStatus.Starting" } }, "required": [ "kind" ] }, - "McpServerStatusReady": { + "McpServerReadyState": { "type": "object", "description": "Server is running and serving requests.", "properties": { "kind": { - "$ref": "#/$defs/McpServerStatusKind.Ready" + "$ref": "#/$defs/McpServerStatus.Ready" } }, "required": [ "kind" ] }, - "McpServerStatusAuthRequired": { + "McpServerAuthRequiredState": { "type": "object", "description": "Server is reachable but cannot serve requests until the client\nauthenticates. Mirrors the discovery flow defined by\n[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)\n(Protected Resource Metadata) and the OAuth 2.1 / RFC 6750 challenge\nsemantics required by the MCP authorization spec.\n\nClients react to this state by calling the existing `authenticate`\ncommand with the {@link ProtectedResourceMetadata.resource | resource}\ncarried here. There is **no** `notify/authRequired` notification for\nMCP servers — the action stream is the single source of truth.\n\nWhen the transition is triggered by a request issued during a turn\n— most commonly\n{@link McpAuthRequiredReason.InsufficientScope | `InsufficientScope`}\nsurfacing mid-tool-call — the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session so the block is\nvisible at the summary level. Clients SHOULD watch this status on\nany MCP server backing a running tool call and surface an explicit\naffordance (e.g. a \"grant additional access\" prompt) tied to that\ntool call, rather than relying on the user to notice the\ncustomization’s status badge.", "properties": { "kind": { - "$ref": "#/$defs/McpServerStatusKind.AuthRequired" + "$ref": "#/$defs/McpServerStatus.AuthRequired" }, "reason": { "$ref": "#/$defs/McpAuthRequiredReason", @@ -4466,12 +4481,12 @@ "resource" ] }, - "McpServerStatusError": { + "McpServerErrorState": { "type": "object", - "description": "Server failed to start, crashed, or otherwise transitioned to a\nnon-recoverable error. Use {@link McpServerStatusKind.AuthRequired}\nfor authentication failures.", + "description": "Server failed to start, crashed, or otherwise transitioned to a\nnon-recoverable error. Use {@link McpServerStatus.AuthRequired}\nfor authentication failures.", "properties": { "kind": { - "$ref": "#/$defs/McpServerStatusKind.Error" + "$ref": "#/$defs/McpServerStatus.Error" }, "error": { "$ref": "#/$defs/ErrorInfo", @@ -4483,12 +4498,12 @@ "error" ] }, - "McpServerStatusStopped": { + "McpServerStoppedState": { "type": "object", "description": "Server has been shut down. The host MAY remove the server from the\nsession entirely shortly after this state.", "properties": { "kind": { - "$ref": "#/$defs/McpServerStatusKind.Stopped" + "$ref": "#/$defs/McpServerStatus.Stopped" } }, "required": [ @@ -5845,30 +5860,30 @@ "id" ] }, - "SessionMcpServerStatusChangedAction": { + "SessionMcpServerStateChangedAction": { "type": "object", - "description": "Updates the runtime fields of an existing\n{@link McpServerCustomization} — narrow alternative to\n{@link SessionCustomizationUpdatedAction} for the high-frequency\n`starting` ↔ `ready` ↔ `authRequired` transitions.\n\nLocates the target entry by `id`, searching both the top-level\ncustomization list and the `children` array of every container.\nReplaces the entry's {@link McpServerCustomization.runtimeStatus | `runtimeStatus`}\nand {@link McpServerCustomization.channel | `channel`}\n(full-replacement semantics: omit `channel` to clear an existing\nchannel URI). Other fields of the customization are preserved.\n\nIs a no-op when no matching `McpServerCustomization` is found. To\nupdate any other field (name, icons, `mcpApp` capabilities, etc.) use\n{@link SessionCustomizationUpdatedAction} instead.\n\nWhen the transition is to {@link McpServerStatusKind.AuthRequired}\nbecause of a request issued mid-turn, the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session — see\n{@link McpServerStatusAuthRequired} for the rationale.", + "description": "Updates the runtime fields of an existing\n{@link McpServerCustomization} — narrow alternative to\n{@link SessionCustomizationUpdatedAction} for the high-frequency\n`starting` ↔ `ready` ↔ `authRequired` transitions.\n\nLocates the target entry by `id`, searching both the top-level\ncustomization list and the `children` array of every container.\nReplaces the entry's {@link McpServerCustomization.state | `state`}\nand {@link McpServerCustomization.channel | `channel`}\n(full-replacement semantics: omit `channel` to clear an existing\nchannel URI). Other fields of the customization are preserved.\n\nIs a no-op when no matching `McpServerCustomization` is found. To\nupdate any other field (name, icons, `mcpApp` capabilities, etc.) use\n{@link SessionCustomizationUpdatedAction} instead.\n\nWhen the transition is to {@link McpServerStatus.AuthRequired}\nbecause of a request issued mid-turn, the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session — see\n{@link McpServerAuthRequiredState} for the rationale.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionMcpServerStatusChanged" + "$ref": "#/$defs/ActionType.SessionMcpServerStateChanged" }, "id": { "type": "string", "description": "The id of the {@link McpServerCustomization} to update." }, - "runtimeStatus": { - "$ref": "#/$defs/McpServerStatus", - "description": "The new runtime status." + "state": { + "$ref": "#/$defs/McpServerState", + "description": "The new lifecycle state." }, "channel": { "$ref": "#/$defs/URI", - "description": "Updated `mcp://` side-channel URI. Full-replacement: omit to clear\nan existing channel (typical when leaving\n{@link McpServerStatusKind.Ready | `Ready`})." + "description": "Updated `mcp://` side-channel URI. Full-replacement: omit to clear\nan existing channel (typical when leaving\n{@link McpServerStatus.Ready | `Ready`})." } }, "required": [ "type", "id", - "runtimeStatus" + "state" ] }, "SessionConfigChangedAction": { diff --git a/schema/errors.schema.json b/schema/errors.schema.json index cb959205..d5ffd19a 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -3268,13 +3268,13 @@ "type": "boolean", "description": "Whether this MCP server is currently enabled." }, - "runtimeStatus": { - "$ref": "#/$defs/McpServerStatus", - "description": "Current status of the MCP server." + "state": { + "$ref": "#/$defs/McpServerState", + "description": "Current lifecycle state of the MCP server." }, "channel": { "$ref": "#/$defs/URI", - "description": "An `mcp://`-protocol channel the client uses to side-channel traffic\ninto the upstream MCP server itself. The channel is NOT a fresh raw MCP\nconnection: it piggybacks on the AHP transport\nand skips the MCP `initialize` sequence.\n\nThe agent host MAY only serve a subset of MCP on this\nchannel; the served subset is described by domain-specific\ncapabilities such as those in\n{@link McpServerCustomizationApps.capabilities}.\n\nThe channel URI SHOULD be stable across the server's lifetime, but\nthe agent host MAY change it (for example across a restart) and\nMAY only expose it while the server is in\n{@link McpServerStatusKind.Ready | `Ready`}. Absence means no\nside-channel is currently available." + "description": "An `mcp://`-protocol channel the client uses to side-channel traffic\ninto the upstream MCP server itself. The channel is NOT a fresh raw MCP\nconnection: it piggybacks on the AHP transport\nand skips the MCP `initialize` sequence.\n\nThe agent host MAY only serve a subset of MCP on this\nchannel; the served subset is described by domain-specific\ncapabilities such as those in\n{@link McpServerCustomizationApps.capabilities}.\n\nThe channel URI SHOULD be stable across the server's lifetime, but\nthe agent host MAY change it (for example across a restart) and\nMAY only expose it while the server is in\n{@link McpServerStatus.Ready | `Ready`}. Absence means no\nside-channel is currently available." }, "mcpApp": { "$ref": "#/$defs/McpServerCustomizationApps", @@ -3287,7 +3287,7 @@ "name", "type", "enabled", - "runtimeStatus" + "state" ] }, "McpServerCustomizationApps": { @@ -3341,36 +3341,36 @@ } } }, - "McpServerStatusStarting": { + "McpServerStartingState": { "type": "object", "description": "Server is registered with the host but has not yet started.", "properties": { "kind": { - "$ref": "#/$defs/McpServerStatusKind.Starting" + "$ref": "#/$defs/McpServerStatus.Starting" } }, "required": [ "kind" ] }, - "McpServerStatusReady": { + "McpServerReadyState": { "type": "object", "description": "Server is running and serving requests.", "properties": { "kind": { - "$ref": "#/$defs/McpServerStatusKind.Ready" + "$ref": "#/$defs/McpServerStatus.Ready" } }, "required": [ "kind" ] }, - "McpServerStatusAuthRequired": { + "McpServerAuthRequiredState": { "type": "object", "description": "Server is reachable but cannot serve requests until the client\nauthenticates. Mirrors the discovery flow defined by\n[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)\n(Protected Resource Metadata) and the OAuth 2.1 / RFC 6750 challenge\nsemantics required by the MCP authorization spec.\n\nClients react to this state by calling the existing `authenticate`\ncommand with the {@link ProtectedResourceMetadata.resource | resource}\ncarried here. There is **no** `notify/authRequired` notification for\nMCP servers — the action stream is the single source of truth.\n\nWhen the transition is triggered by a request issued during a turn\n— most commonly\n{@link McpAuthRequiredReason.InsufficientScope | `InsufficientScope`}\nsurfacing mid-tool-call — the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session so the block is\nvisible at the summary level. Clients SHOULD watch this status on\nany MCP server backing a running tool call and surface an explicit\naffordance (e.g. a \"grant additional access\" prompt) tied to that\ntool call, rather than relying on the user to notice the\ncustomization’s status badge.", "properties": { "kind": { - "$ref": "#/$defs/McpServerStatusKind.AuthRequired" + "$ref": "#/$defs/McpServerStatus.AuthRequired" }, "reason": { "$ref": "#/$defs/McpAuthRequiredReason", @@ -3398,12 +3398,12 @@ "resource" ] }, - "McpServerStatusError": { + "McpServerErrorState": { "type": "object", - "description": "Server failed to start, crashed, or otherwise transitioned to a\nnon-recoverable error. Use {@link McpServerStatusKind.AuthRequired}\nfor authentication failures.", + "description": "Server failed to start, crashed, or otherwise transitioned to a\nnon-recoverable error. Use {@link McpServerStatus.AuthRequired}\nfor authentication failures.", "properties": { "kind": { - "$ref": "#/$defs/McpServerStatusKind.Error" + "$ref": "#/$defs/McpServerStatus.Error" }, "error": { "$ref": "#/$defs/ErrorInfo", @@ -3415,12 +3415,12 @@ "error" ] }, - "McpServerStatusStopped": { + "McpServerStoppedState": { "type": "object", "description": "Server has been shut down. The host MAY remove the server from the\nsession entirely shortly after this state.", "properties": { "kind": { - "$ref": "#/$defs/McpServerStatusKind.Stopped" + "$ref": "#/$defs/McpServerStatus.Stopped" } }, "required": [ @@ -3861,6 +3861,10 @@ "locale": { "type": "string", "description": "IETF BCP 47 language tag indicating the client's preferred locale\n(e.g. `\"en-US\"`, `\"ja\"`). The server SHOULD use this to localise\nuser-facing strings such as confirmation option labels." + }, + "capabilities": { + "$ref": "#/$defs/ClientCapabilities", + "description": "Optional client capability declarations.\n\nServers SHOULD only advertise features whose corresponding client\ncapability is set here. Absent means \"not declared\" — the server\nMUST assume the client does not support the feature." } }, "required": [ @@ -3869,6 +3873,17 @@ "clientId" ] }, + "ClientCapabilities": { + "type": "object", + "description": "Optional capabilities a client declares during `initialize`.\n\nEach field is a presence flag: an empty object `{}` means \"supported\",\nabsence means \"not supported\". Sub-fields on individual capabilities\nare reserved for future per-capability options.", + "properties": { + "mcpApps": { + "type": "object", + "additionalProperties": {}, + "description": "Client can render\n[MCP Apps](https://github.com/modelcontextprotocol/ext-apps) — i.e.\nit can host the View sandbox, run the `ui/*` protocol against it,\nand forward `mcp://`-channel traffic on the App's behalf.\n\nHosts SHOULD only populate\n{@link McpServerCustomization.mcpApp | `McpServerCustomization.mcpApp`}\n(and expose the corresponding\n{@link McpServerCustomization.channel | `mcp://` channel}) when this\ncapability is declared. Clients that omit it MUST treat\nApp-bearing tool calls as ordinary MCP tool calls." + } + } + }, "InitializeResult": { "type": "object", "description": "Result of the `initialize` command.\n\n`protocolVersion` is the version the server has selected from the client's\n`protocolVersions` list. The client and server MUST use this version for\nthe rest of the connection. If the server cannot speak any of the offered\nversions it MUST return error code `-32005` (`UnsupportedProtocolVersion`)\ninstead of a result.", diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 51fa6473..8d08d705 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -3396,13 +3396,13 @@ "type": "boolean", "description": "Whether this MCP server is currently enabled." }, - "runtimeStatus": { - "$ref": "#/$defs/McpServerStatus", - "description": "Current status of the MCP server." + "state": { + "$ref": "#/$defs/McpServerState", + "description": "Current lifecycle state of the MCP server." }, "channel": { "$ref": "#/$defs/URI", - "description": "An `mcp://`-protocol channel the client uses to side-channel traffic\ninto the upstream MCP server itself. The channel is NOT a fresh raw MCP\nconnection: it piggybacks on the AHP transport\nand skips the MCP `initialize` sequence.\n\nThe agent host MAY only serve a subset of MCP on this\nchannel; the served subset is described by domain-specific\ncapabilities such as those in\n{@link McpServerCustomizationApps.capabilities}.\n\nThe channel URI SHOULD be stable across the server's lifetime, but\nthe agent host MAY change it (for example across a restart) and\nMAY only expose it while the server is in\n{@link McpServerStatusKind.Ready | `Ready`}. Absence means no\nside-channel is currently available." + "description": "An `mcp://`-protocol channel the client uses to side-channel traffic\ninto the upstream MCP server itself. The channel is NOT a fresh raw MCP\nconnection: it piggybacks on the AHP transport\nand skips the MCP `initialize` sequence.\n\nThe agent host MAY only serve a subset of MCP on this\nchannel; the served subset is described by domain-specific\ncapabilities such as those in\n{@link McpServerCustomizationApps.capabilities}.\n\nThe channel URI SHOULD be stable across the server's lifetime, but\nthe agent host MAY change it (for example across a restart) and\nMAY only expose it while the server is in\n{@link McpServerStatus.Ready | `Ready`}. Absence means no\nside-channel is currently available." }, "mcpApp": { "$ref": "#/$defs/McpServerCustomizationApps", @@ -3415,7 +3415,7 @@ "name", "type", "enabled", - "runtimeStatus" + "state" ] }, "McpServerCustomizationApps": { @@ -3469,36 +3469,36 @@ } } }, - "McpServerStatusStarting": { + "McpServerStartingState": { "type": "object", "description": "Server is registered with the host but has not yet started.", "properties": { "kind": { - "$ref": "#/$defs/McpServerStatusKind.Starting" + "$ref": "#/$defs/McpServerStatus.Starting" } }, "required": [ "kind" ] }, - "McpServerStatusReady": { + "McpServerReadyState": { "type": "object", "description": "Server is running and serving requests.", "properties": { "kind": { - "$ref": "#/$defs/McpServerStatusKind.Ready" + "$ref": "#/$defs/McpServerStatus.Ready" } }, "required": [ "kind" ] }, - "McpServerStatusAuthRequired": { + "McpServerAuthRequiredState": { "type": "object", "description": "Server is reachable but cannot serve requests until the client\nauthenticates. Mirrors the discovery flow defined by\n[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)\n(Protected Resource Metadata) and the OAuth 2.1 / RFC 6750 challenge\nsemantics required by the MCP authorization spec.\n\nClients react to this state by calling the existing `authenticate`\ncommand with the {@link ProtectedResourceMetadata.resource | resource}\ncarried here. There is **no** `notify/authRequired` notification for\nMCP servers — the action stream is the single source of truth.\n\nWhen the transition is triggered by a request issued during a turn\n— most commonly\n{@link McpAuthRequiredReason.InsufficientScope | `InsufficientScope`}\nsurfacing mid-tool-call — the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session so the block is\nvisible at the summary level. Clients SHOULD watch this status on\nany MCP server backing a running tool call and surface an explicit\naffordance (e.g. a \"grant additional access\" prompt) tied to that\ntool call, rather than relying on the user to notice the\ncustomization’s status badge.", "properties": { "kind": { - "$ref": "#/$defs/McpServerStatusKind.AuthRequired" + "$ref": "#/$defs/McpServerStatus.AuthRequired" }, "reason": { "$ref": "#/$defs/McpAuthRequiredReason", @@ -3526,12 +3526,12 @@ "resource" ] }, - "McpServerStatusError": { + "McpServerErrorState": { "type": "object", - "description": "Server failed to start, crashed, or otherwise transitioned to a\nnon-recoverable error. Use {@link McpServerStatusKind.AuthRequired}\nfor authentication failures.", + "description": "Server failed to start, crashed, or otherwise transitioned to a\nnon-recoverable error. Use {@link McpServerStatus.AuthRequired}\nfor authentication failures.", "properties": { "kind": { - "$ref": "#/$defs/McpServerStatusKind.Error" + "$ref": "#/$defs/McpServerStatus.Error" }, "error": { "$ref": "#/$defs/ErrorInfo", @@ -3543,12 +3543,12 @@ "error" ] }, - "McpServerStatusStopped": { + "McpServerStoppedState": { "type": "object", "description": "Server has been shut down. The host MAY remove the server from the\nsession entirely shortly after this state.", "properties": { "kind": { - "$ref": "#/$defs/McpServerStatusKind.Stopped" + "$ref": "#/$defs/McpServerStatus.Stopped" } }, "required": [ diff --git a/schema/state.schema.json b/schema/state.schema.json index e3190d39..b1f06980 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -3179,13 +3179,13 @@ "type": "boolean", "description": "Whether this MCP server is currently enabled." }, - "runtimeStatus": { - "$ref": "#/$defs/McpServerStatus", - "description": "Current status of the MCP server." + "state": { + "$ref": "#/$defs/McpServerState", + "description": "Current lifecycle state of the MCP server." }, "channel": { "$ref": "#/$defs/URI", - "description": "An `mcp://`-protocol channel the client uses to side-channel traffic\ninto the upstream MCP server itself. The channel is NOT a fresh raw MCP\nconnection: it piggybacks on the AHP transport\nand skips the MCP `initialize` sequence.\n\nThe agent host MAY only serve a subset of MCP on this\nchannel; the served subset is described by domain-specific\ncapabilities such as those in\n{@link McpServerCustomizationApps.capabilities}.\n\nThe channel URI SHOULD be stable across the server's lifetime, but\nthe agent host MAY change it (for example across a restart) and\nMAY only expose it while the server is in\n{@link McpServerStatusKind.Ready | `Ready`}. Absence means no\nside-channel is currently available." + "description": "An `mcp://`-protocol channel the client uses to side-channel traffic\ninto the upstream MCP server itself. The channel is NOT a fresh raw MCP\nconnection: it piggybacks on the AHP transport\nand skips the MCP `initialize` sequence.\n\nThe agent host MAY only serve a subset of MCP on this\nchannel; the served subset is described by domain-specific\ncapabilities such as those in\n{@link McpServerCustomizationApps.capabilities}.\n\nThe channel URI SHOULD be stable across the server's lifetime, but\nthe agent host MAY change it (for example across a restart) and\nMAY only expose it while the server is in\n{@link McpServerStatus.Ready | `Ready`}. Absence means no\nside-channel is currently available." }, "mcpApp": { "$ref": "#/$defs/McpServerCustomizationApps", @@ -3198,7 +3198,7 @@ "name", "type", "enabled", - "runtimeStatus" + "state" ] }, "McpServerCustomizationApps": { @@ -3252,36 +3252,36 @@ } } }, - "McpServerStatusStarting": { + "McpServerStartingState": { "type": "object", "description": "Server is registered with the host but has not yet started.", "properties": { "kind": { - "$ref": "#/$defs/McpServerStatusKind.Starting" + "$ref": "#/$defs/McpServerStatus.Starting" } }, "required": [ "kind" ] }, - "McpServerStatusReady": { + "McpServerReadyState": { "type": "object", "description": "Server is running and serving requests.", "properties": { "kind": { - "$ref": "#/$defs/McpServerStatusKind.Ready" + "$ref": "#/$defs/McpServerStatus.Ready" } }, "required": [ "kind" ] }, - "McpServerStatusAuthRequired": { + "McpServerAuthRequiredState": { "type": "object", "description": "Server is reachable but cannot serve requests until the client\nauthenticates. Mirrors the discovery flow defined by\n[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)\n(Protected Resource Metadata) and the OAuth 2.1 / RFC 6750 challenge\nsemantics required by the MCP authorization spec.\n\nClients react to this state by calling the existing `authenticate`\ncommand with the {@link ProtectedResourceMetadata.resource | resource}\ncarried here. There is **no** `notify/authRequired` notification for\nMCP servers — the action stream is the single source of truth.\n\nWhen the transition is triggered by a request issued during a turn\n— most commonly\n{@link McpAuthRequiredReason.InsufficientScope | `InsufficientScope`}\nsurfacing mid-tool-call — the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session so the block is\nvisible at the summary level. Clients SHOULD watch this status on\nany MCP server backing a running tool call and surface an explicit\naffordance (e.g. a \"grant additional access\" prompt) tied to that\ntool call, rather than relying on the user to notice the\ncustomization’s status badge.", "properties": { "kind": { - "$ref": "#/$defs/McpServerStatusKind.AuthRequired" + "$ref": "#/$defs/McpServerStatus.AuthRequired" }, "reason": { "$ref": "#/$defs/McpAuthRequiredReason", @@ -3309,12 +3309,12 @@ "resource" ] }, - "McpServerStatusError": { + "McpServerErrorState": { "type": "object", - "description": "Server failed to start, crashed, or otherwise transitioned to a\nnon-recoverable error. Use {@link McpServerStatusKind.AuthRequired}\nfor authentication failures.", + "description": "Server failed to start, crashed, or otherwise transitioned to a\nnon-recoverable error. Use {@link McpServerStatus.AuthRequired}\nfor authentication failures.", "properties": { "kind": { - "$ref": "#/$defs/McpServerStatusKind.Error" + "$ref": "#/$defs/McpServerStatus.Error" }, "error": { "$ref": "#/$defs/ErrorInfo", @@ -3326,12 +3326,12 @@ "error" ] }, - "McpServerStatusStopped": { + "McpServerStoppedState": { "type": "object", "description": "Server has been shut down. The host MAY remove the server from the\nsession entirely shortly after this state.", "properties": { "kind": { - "$ref": "#/$defs/McpServerStatusKind.Stopped" + "$ref": "#/$defs/McpServerStatus.Stopped" } }, "required": [ @@ -3971,26 +3971,26 @@ ], "description": "A top-level customization active in a session. Either a container\n({@link PluginCustomization} or {@link DirectoryCustomization}) whose\nleaf customizations live in its\n{@link ContainerCustomizationBase.children | `children`} array, or a\nbare {@link McpServerCustomization} surfaced directly by the host." }, - "McpServerStatus": { + "McpServerState": { "oneOf": [ {}, { - "$ref": "#/$defs/McpServerStatusStarting" + "$ref": "#/$defs/McpServerStartingState" }, { - "$ref": "#/$defs/McpServerStatusReady" + "$ref": "#/$defs/McpServerReadyState" }, { - "$ref": "#/$defs/McpServerStatusAuthRequired" + "$ref": "#/$defs/McpServerAuthRequiredState" }, { - "$ref": "#/$defs/McpServerStatusError" + "$ref": "#/$defs/McpServerErrorState" }, { - "$ref": "#/$defs/McpServerStatusStopped" + "$ref": "#/$defs/McpServerStoppedState" } ], - "description": "Discriminated union of all MCP server statuses. Discriminated by `kind`." + "description": "Discriminated union of all MCP server lifecycle states.\nDiscriminated by `kind` (a {@link McpServerStatus} value)." }, "TerminalClaim": { "oneOf": [ diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index 21127544..f6807bb5 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -183,7 +183,9 @@ function mapType(tsType: string): string { const recordMatch = tsType.match(/^Record$/); if (recordMatch) { const inner = recordMatch[1].trim(); - if (inner === 'unknown') return 'map[string]json.RawMessage'; + // `Record` is the MCP-style marker for "empty object"; + // treat it like `Record` so the wire `{}` round-trips. + if (inner === 'unknown' || inner === 'never') return 'map[string]json.RawMessage'; return `map[string]${mapTypeForSliceElem(inner)}`; } @@ -628,8 +630,9 @@ const STATE_ENUMS = [ 'SessionInputResponseKind', 'TurnState', 'MessageAttachmentKind', 'ResponsePartKind', 'ToolCallStatus', 'ToolCallConfirmationReason', 'ToolCallCancellationReason', - 'ConfirmationOptionKind', + 'ConfirmationOptionKind', 'ToolCallContributorKind', 'ToolResultContentType', 'CustomizationType', 'CustomizationLoadStatus', 'TerminalClaimKind', + 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationScope', 'ResourceChangeType', ]; @@ -710,6 +713,15 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: strin { name: 'RuleCustomization' }, { name: 'HookCustomization' }, { name: 'McpServerCustomization' }, + { name: 'McpServerCustomizationApps' }, + { name: 'AhpMcpUiHostCapabilities' }, + { name: 'McpServerStartingState' }, + { name: 'McpServerReadyState' }, + { name: 'McpServerAuthRequiredState' }, + { name: 'McpServerErrorState' }, + { name: 'McpServerStoppedState' }, + { name: 'ToolCallClientContributor' }, + { name: 'ToolCallMcpContributor' }, { name: 'FileEdit' }, { name: 'TerminalInfo' }, { name: 'TerminalClientClaim' }, @@ -851,10 +863,11 @@ const MESSAGE_ATTACHMENT_UNION: UnionConfig = { const CUSTOMIZATION_UNION: UnionConfig = { name: 'Customization', discriminantField: 'type', - doc: 'Customization is a top-level customization (plugin or directory).', + doc: 'Customization is a top-level customization (plugin, directory, or bare MCP server).', variants: [ { variantName: 'Plugin', innerType: 'PluginCustomization', wireValue: 'plugin' }, { variantName: 'Directory', innerType: 'DirectoryCustomization', wireValue: 'directory' }, + { variantName: 'McpServer', innerType: 'McpServerCustomization', wireValue: 'mcpServer' }, ], unknown: true, }; @@ -887,6 +900,31 @@ const CUSTOMIZATION_LOAD_STATE_UNION: UnionConfig = { unknown: true, }; +const MCP_SERVER_STATUS_UNION: UnionConfig = { + name: 'McpServerState', + discriminantField: 'kind', + doc: 'McpServerState is the discriminated lifecycle status of an MCP server customization.', + variants: [ + { variantName: 'Starting', innerType: 'McpServerStartingState', wireValue: 'starting' }, + { variantName: 'Ready', innerType: 'McpServerReadyState', wireValue: 'ready' }, + { variantName: 'AuthRequired', innerType: 'McpServerAuthRequiredState', wireValue: 'authRequired' }, + { variantName: 'Error', innerType: 'McpServerErrorState', wireValue: 'error' }, + { variantName: 'Stopped', innerType: 'McpServerStoppedState', wireValue: 'stopped' }, + ], + unknown: true, +}; + +const TOOL_CALL_CONTRIBUTOR_UNION: UnionConfig = { + name: 'ToolCallContributor', + discriminantField: 'kind', + doc: 'ToolCallContributor identifies the contributor (client or MCP server) of a tool call.', + variants: [ + { variantName: 'Client', innerType: 'ToolCallClientContributor', wireValue: 'client' }, + { variantName: 'Mcp', innerType: 'ToolCallMcpContributor', wireValue: 'mcp' }, + ], + unknown: true, +}; + function generateSnapshotState(): string { return `// SnapshotState is the state payload of a snapshot — root, session, // terminal, or changeset state. The active variant is chosen by which @@ -1014,6 +1052,10 @@ function generateStateFile(project: Project): string { lines.push(''); lines.push(generateDiscriminatedUnion(CUSTOMIZATION_LOAD_STATE_UNION)); lines.push(''); + lines.push(generateDiscriminatedUnion(MCP_SERVER_STATUS_UNION)); + lines.push(''); + lines.push(generateDiscriminatedUnion(TOOL_CALL_CONTRIBUTOR_UNION)); + lines.push(''); lines.push(generateSnapshotState()); lines.push(''); @@ -1066,6 +1108,7 @@ const ACTION_VARIANTS: { { type: 'session/customizationToggled', variantName: 'SessionCustomizationToggled', tsInterface: 'SessionCustomizationToggledAction' }, { type: 'session/customizationUpdated', variantName: 'SessionCustomizationUpdated', tsInterface: 'SessionCustomizationUpdatedAction' }, { type: 'session/customizationRemoved', variantName: 'SessionCustomizationRemoved', tsInterface: 'SessionCustomizationRemovedAction' }, + { type: 'session/mcpServerStateChanged', variantName: 'SessionMcpServerStateChanged', tsInterface: 'SessionMcpServerStateChangedAction' }, { type: 'session/truncated', variantName: 'SessionTruncated', tsInterface: 'SessionTruncatedAction' }, { type: 'session/configChanged', variantName: 'SessionConfigChanged', tsInterface: 'SessionConfigChangedAction' }, { type: 'session/metaChanged', variantName: 'SessionMetaChanged', tsInterface: 'SessionMetaChangedAction' }, @@ -1191,6 +1234,7 @@ const COMMAND_ENUMS = ['ReconnectResultType', 'ContentEncoding', 'CompletionItem const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: string }[] = [ { name: 'InitializeParams' }, { name: 'InitializeResult' }, + { name: 'ClientCapabilities' }, { name: 'ReconnectParams' }, { name: 'ReconnectReplayResult', omitDiscriminants: true }, { name: 'ReconnectSnapshotResult', omitDiscriminants: true }, @@ -1687,6 +1731,8 @@ function checkExhaustiveness(project: Project): void { 'ChildCustomization', 'ChildCustomizationType', 'CustomizationLoadState', + 'McpServerState', + 'ToolCallContributor', 'ReconnectResult', 'AuthRequiredErrorData', 'PermissionDeniedErrorData', diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index ea75e7a4..b7338c80 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -166,7 +166,13 @@ function mapType(tsType: string): string { // Record const recordMatch = tsType.match(/^Record$/); - if (recordMatch) return `Map`; + if (recordMatch) { + const inner = recordMatch[1].trim(); + // `Record` is the MCP-style marker for "empty object"; + // treat it like `Record` so the wire `{}` round-trips. + if (inner === 'never') return 'Map'; + return `Map`; + } // Partial const partialMatch = tsType.match(/^Partial<(\w+)>$/); @@ -740,7 +746,9 @@ const STATE_ENUMS = [ 'SessionInputResponseKind', 'TurnState', 'MessageKind', 'MessageAttachmentKind', 'ResponsePartKind', 'ToolCallStatus', 'ToolCallConfirmationReason', 'ToolCallCancellationReason', 'ConfirmationOptionKind', + 'ToolCallContributorKind', 'ToolResultContentType', 'CustomizationType', 'CustomizationLoadStatus', 'TerminalClaimKind', + 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationScope', 'ResourceChangeType', ]; @@ -775,6 +783,10 @@ const STATE_STRUCTS = [ 'PluginCustomization', 'ClientPluginCustomization', 'DirectoryCustomization', 'AgentCustomization', 'SkillCustomization', 'PromptCustomization', 'RuleCustomization', 'HookCustomization', 'McpServerCustomization', + 'McpServerCustomizationApps', 'AhpMcpUiHostCapabilities', + 'McpServerStartingState', 'McpServerReadyState', 'McpServerAuthRequiredState', + 'McpServerErrorState', 'McpServerStoppedState', + 'ToolCallClientContributor', 'ToolCallMcpContributor', 'FileEdit', 'TerminalInfo', 'TerminalClientClaim', 'TerminalSessionClaim', 'TerminalState', 'TerminalUnclassifiedPart', 'TerminalCommandPart', @@ -888,6 +900,7 @@ const CUSTOMIZATION_UNION: UnionConfig = { variants: [ { caseName: 'Plugin', structName: 'PluginCustomization', discriminantValue: 'plugin' }, { caseName: 'Directory', structName: 'DirectoryCustomization', discriminantValue: 'directory' }, + { caseName: 'McpServer', structName: 'McpServerCustomization', discriminantValue: 'mcpServer' }, ], unknown: true, }; @@ -918,6 +931,29 @@ const CUSTOMIZATION_LOAD_STATE_UNION: UnionConfig = { unknown: true, }; +const MCP_SERVER_STATUS_UNION: UnionConfig = { + name: 'McpServerState', + discriminantField: 'kind', + variants: [ + { caseName: 'Starting', structName: 'McpServerStartingState', discriminantValue: 'starting' }, + { caseName: 'Ready', structName: 'McpServerReadyState', discriminantValue: 'ready' }, + { caseName: 'AuthRequired', structName: 'McpServerAuthRequiredState', discriminantValue: 'authRequired' }, + { caseName: 'Error', structName: 'McpServerErrorState', discriminantValue: 'error' }, + { caseName: 'Stopped', structName: 'McpServerStoppedState', discriminantValue: 'stopped' }, + ], + unknown: true, +}; + +const TOOL_CALL_CONTRIBUTOR_UNION: UnionConfig = { + name: 'ToolCallContributor', + discriminantField: 'kind', + variants: [ + { caseName: 'Client', structName: 'ToolCallClientContributor', discriminantValue: 'client' }, + { caseName: 'Mcp', structName: 'ToolCallMcpContributor', discriminantValue: 'mcp' }, + ], + unknown: true, +}; + function generateStateFile(project: Project): string { const lines: string[] = [GENERATED_HEADER]; @@ -977,6 +1013,10 @@ function generateStateFile(project: Project): string { lines.push(''); lines.push(generateDiscriminatedUnion(CUSTOMIZATION_LOAD_STATE_UNION)); lines.push(''); + lines.push(generateDiscriminatedUnion(MCP_SERVER_STATUS_UNION)); + lines.push(''); + lines.push(generateDiscriminatedUnion(TOOL_CALL_CONTRIBUTOR_UNION)); + lines.push(''); lines.push(generateToolResultContentUnion()); lines.push(''); lines.push(generateSnapshotState()); @@ -1026,6 +1066,7 @@ const ACTION_VARIANTS: { type: string; caseName: string; tsInterface: string }[] { type: 'session/customizationToggled', caseName: 'SessionCustomizationToggled', tsInterface: 'SessionCustomizationToggledAction' }, { type: 'session/customizationUpdated', caseName: 'SessionCustomizationUpdated', tsInterface: 'SessionCustomizationUpdatedAction' }, { type: 'session/customizationRemoved', caseName: 'SessionCustomizationRemoved', tsInterface: 'SessionCustomizationRemovedAction' }, + { type: 'session/mcpServerStateChanged', caseName: 'SessionMcpServerStateChanged', tsInterface: 'SessionMcpServerStateChangedAction' }, { type: 'session/truncated', caseName: 'SessionTruncated', tsInterface: 'SessionTruncatedAction' }, { type: 'session/configChanged', caseName: 'SessionConfigChanged', tsInterface: 'SessionConfigChangedAction' }, { type: 'session/metaChanged', caseName: 'SessionMetaChanged', tsInterface: 'SessionMetaChangedAction' }, @@ -1188,6 +1229,7 @@ const COMMAND_ENUMS = ['ReconnectResultType', 'ContentEncoding', 'CompletionItem const COMMAND_STRUCTS = [ 'InitializeParams', 'InitializeResult', + 'ClientCapabilities', 'ReconnectParams', 'ReconnectReplayResult', 'ReconnectSnapshotResult', 'SubscribeParams', 'SubscribeResult', 'SessionForkSource', 'CreateSessionParams', 'DisposeSessionParams', @@ -1693,6 +1735,8 @@ function checkExhaustiveness(project: Project): void { 'MessageAttachmentBase', // base interface, flattened into the variant data classes via `extends` 'Customization', // CUSTOMIZATION_UNION discriminated union 'ChildCustomization', // CHILD_CUSTOMIZATION_UNION discriminated union + 'McpServerState', // MCP_SERVER_STATUS_UNION discriminated union + 'ToolCallContributor', // TOOL_CALL_CONTRIBUTOR_UNION discriminated union 'ChildCustomizationType', // TS subset alias of CustomizationType; consumers reuse CustomizationType 'CustomizationLoadState', // CUSTOMIZATION_LOAD_STATE_UNION discriminated union 'AuthRequiredErrorData', // emitted by generateErrorsFile() diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index 07af9143..f77f691b 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -165,7 +165,9 @@ function mapType(tsType: string, propName?: string, containerName?: string): str const recordMatch = tsType.match(/^Record$/); if (recordMatch) { const inner = recordMatch[1].trim(); - if (inner === 'unknown') return 'JsonObject'; + // `Record` is the MCP-style marker for "empty object"; + // treat it like `Record` so the wire `{}` round-trips. + if (inner === 'unknown' || inner === 'never') return 'JsonObject'; return `std::collections::HashMap`; } @@ -523,8 +525,9 @@ const STATE_ENUMS = [ 'SessionInputResponseKind', 'TurnState', 'MessageAttachmentKind', 'ResponsePartKind', 'ToolCallStatus', 'ToolCallConfirmationReason', 'ToolCallCancellationReason', - 'ConfirmationOptionKind', + 'ConfirmationOptionKind', 'ToolCallContributorKind', 'ToolResultContentType', 'CustomizationType', 'CustomizationLoadStatus', 'TerminalClaimKind', + 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationScope', 'ResourceChangeType', ]; @@ -610,6 +613,15 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: str { name: 'RuleCustomization', omitDiscriminants: true }, { name: 'HookCustomization', omitDiscriminants: true }, { name: 'McpServerCustomization', omitDiscriminants: true }, + { name: 'McpServerCustomizationApps' }, + { name: 'AhpMcpUiHostCapabilities' }, + { name: 'McpServerStartingState', omitDiscriminants: true }, + { name: 'McpServerReadyState', omitDiscriminants: true }, + { name: 'McpServerAuthRequiredState', omitDiscriminants: true }, + { name: 'McpServerErrorState', omitDiscriminants: true }, + { name: 'McpServerStoppedState', omitDiscriminants: true }, + { name: 'ToolCallClientContributor', omitDiscriminants: true }, + { name: 'ToolCallMcpContributor', omitDiscriminants: true }, { name: 'FileEdit' }, { name: 'TerminalInfo' }, { name: 'TerminalClientClaim', omitDiscriminants: true }, @@ -751,10 +763,11 @@ const MESSAGE_ATTACHMENT_UNION: UnionConfig = { const CUSTOMIZATION_UNION: UnionConfig = { name: 'Customization', discriminantField: 'type', - doc: 'A top-level customization (plugin or directory).', + doc: 'A top-level customization (plugin, directory, or bare MCP server).', variants: [ { variantName: 'Plugin', innerType: 'PluginCustomization', wireValue: 'plugin' }, { variantName: 'Directory', innerType: 'DirectoryCustomization', wireValue: 'directory' }, + { variantName: 'McpServer', innerType: 'McpServerCustomization', wireValue: 'mcpServer' }, ], unknown: true, }; @@ -787,6 +800,31 @@ const CUSTOMIZATION_LOAD_STATE_UNION: UnionConfig = { unknown: true, }; +const MCP_SERVER_STATUS_UNION: UnionConfig = { + name: 'McpServerState', + discriminantField: 'kind', + doc: 'Discriminated lifecycle status of an MCP server customization.', + variants: [ + { variantName: 'Starting', innerType: 'McpServerStartingState', wireValue: 'starting' }, + { variantName: 'Ready', innerType: 'McpServerReadyState', wireValue: 'ready' }, + { variantName: 'AuthRequired', innerType: 'McpServerAuthRequiredState', wireValue: 'authRequired' }, + { variantName: 'Error', innerType: 'McpServerErrorState', wireValue: 'error' }, + { variantName: 'Stopped', innerType: 'McpServerStoppedState', wireValue: 'stopped' }, + ], + unknown: true, +}; + +const TOOL_CALL_CONTRIBUTOR_UNION: UnionConfig = { + name: 'ToolCallContributor', + discriminantField: 'kind', + doc: 'Reference to the contributor of the tool being called.', + variants: [ + { variantName: 'Client', innerType: 'ToolCallClientContributor', wireValue: 'client' }, + { variantName: 'Mcp', innerType: 'ToolCallMcpContributor', wireValue: 'mcp' }, + ], + unknown: true, +}; + function generateSnapshotState(): string { return `/// The state payload of a snapshot — root, session, terminal, or /// changeset state. @@ -854,6 +892,10 @@ function generateStateFile(project: Project): string { lines.push(''); lines.push(generateDiscriminatedUnion(CUSTOMIZATION_LOAD_STATE_UNION)); lines.push(''); + lines.push(generateDiscriminatedUnion(MCP_SERVER_STATUS_UNION)); + lines.push(''); + lines.push(generateDiscriminatedUnion(TOOL_CALL_CONTRIBUTOR_UNION)); + lines.push(''); lines.push(generateSnapshotState()); lines.push(''); @@ -907,6 +949,7 @@ const ACTION_VARIANTS: { { type: 'session/customizationToggled', variantName: 'SessionCustomizationToggled', tsInterface: 'SessionCustomizationToggledAction' }, { type: 'session/customizationUpdated', variantName: 'SessionCustomizationUpdated', tsInterface: 'SessionCustomizationUpdatedAction' }, { type: 'session/customizationRemoved', variantName: 'SessionCustomizationRemoved', tsInterface: 'SessionCustomizationRemovedAction' }, + { type: 'session/mcpServerStateChanged', variantName: 'SessionMcpServerStateChanged', tsInterface: 'SessionMcpServerStateChangedAction' }, { type: 'session/truncated', variantName: 'SessionTruncated', tsInterface: 'SessionTruncatedAction' }, { type: 'session/configChanged', variantName: 'SessionConfigChanged', tsInterface: 'SessionConfigChangedAction' }, { type: 'session/metaChanged', variantName: 'SessionMetaChanged', tsInterface: 'SessionMetaChangedAction' }, @@ -966,7 +1009,7 @@ pub struct SessionToolCallConfirmedAction { function generateActionsFile(project: Project): string { const lines: string[] = [GENERATED_HEADER]; - lines.push('use crate::state::{AgentInfo, AgentSelection, ConfirmationOption, Customization, ErrorInfo, ModelSelection, ResponsePart, SessionActiveClient, SessionInputAnswer, SessionInputRequest, SessionInputResponseKind, TerminalClaim, TerminalInfo, ToolCallResult, ToolCallConfirmationReason, ToolCallCancellationReason, ToolDefinition, ToolResultContent, UsageInfo, Message, PendingMessageKind, ChangesetStatus, ChangesetFile, ChangesetOperation, ChangesetSummary};'); + lines.push('use crate::state::{AgentInfo, AgentSelection, ConfirmationOption, Customization, ErrorInfo, McpServerState, ModelSelection, ResponsePart, SessionActiveClient, SessionInputAnswer, SessionInputRequest, SessionInputResponseKind, TerminalClaim, TerminalInfo, ToolCallContributor, ToolCallResult, ToolCallConfirmationReason, ToolCallCancellationReason, ToolDefinition, ToolResultContent, UsageInfo, Message, PendingMessageKind, ChangesetStatus, ChangesetFile, ChangesetOperation, ChangesetSummary};'); lines.push(''); // ActionType enum @@ -1046,6 +1089,7 @@ const COMMAND_ENUMS = ['ReconnectResultType', 'ContentEncoding', 'CompletionItem const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: string }[] = [ { name: 'InitializeParams' }, { name: 'InitializeResult' }, + { name: 'ClientCapabilities' }, { name: 'ReconnectParams' }, { name: 'ReconnectReplayResult', omitDiscriminants: true }, { name: 'ReconnectSnapshotResult', omitDiscriminants: true }, @@ -1462,6 +1506,8 @@ function checkExhaustiveness(project: Project): void { 'ChildCustomization', // CHILD_CUSTOMIZATION_UNION discriminated union 'ChildCustomizationType', // TS subset alias of CustomizationType; Rust consumers reuse CustomizationType 'CustomizationLoadState', // CUSTOMIZATION_LOAD_STATE_UNION discriminated union + 'McpServerState', // MCP_SERVER_STATUS_UNION discriminated union + 'ToolCallContributor', // TOOL_CALL_CONTRIBUTOR_UNION discriminated union 'ReconnectResult', 'AuthRequiredErrorData', 'PermissionDeniedErrorData', diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index 630ca02f..09b1081c 100644 --- a/scripts/generate-swift.ts +++ b/scripts/generate-swift.ts @@ -126,7 +126,13 @@ function mapType(tsType: string, propName?: string, containerName?: string): str // Record const recordMatch = tsType.match(/^Record$/); - if (recordMatch) return `[String: ${mapType(recordMatch[1])}]`; + if (recordMatch) { + const inner = recordMatch[1].trim(); + // `Record` is the MCP-style marker for "empty object"; + // treat it like `Record` so the wire `{}` round-trips. + if (inner === 'never') return `[String: AnyCodable]`; + return `[String: ${mapType(inner)}]`; + } // Partial — Swift has no structural Partial; emit/ reuse a sibling // struct with every property optional. Tracked for later emission. @@ -496,7 +502,9 @@ const STATE_ENUMS = [ 'SessionInputResponseKind', 'TurnState', 'MessageAttachmentKind', 'ResponsePartKind', 'ToolCallStatus', 'ToolCallConfirmationReason', 'ToolCallCancellationReason', 'ConfirmationOptionKind', + 'ToolCallContributorKind', 'ToolResultContentType', 'CustomizationType', 'CustomizationLoadStatus', 'TerminalClaimKind', + 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationScope', 'ResourceChangeType', ]; @@ -531,7 +539,10 @@ const STATE_STRUCTS = [ 'PluginCustomization', 'ClientPluginCustomization', 'DirectoryCustomization', 'AgentCustomization', 'SkillCustomization', 'PromptCustomization', 'RuleCustomization', 'HookCustomization', - 'McpServerCustomization', + 'McpServerCustomization', 'McpServerCustomizationApps', 'AhpMcpUiHostCapabilities', + 'McpServerStartingState', 'McpServerReadyState', 'McpServerAuthRequiredState', + 'McpServerErrorState', 'McpServerStoppedState', + 'ToolCallClientContributor', 'ToolCallMcpContributor', 'FileEdit', 'TerminalInfo', 'TerminalClientClaim', 'TerminalSessionClaim', 'TerminalState', 'TerminalUnclassifiedPart', 'TerminalCommandPart', @@ -635,6 +646,7 @@ const CUSTOMIZATION_UNION: UnionConfig = { variants: [ { caseName: 'plugin', structName: 'PluginCustomization', discriminantValue: 'plugin' }, { caseName: 'directory', structName: 'DirectoryCustomization', discriminantValue: 'directory' }, + { caseName: 'mcpServer', structName: 'McpServerCustomization', discriminantValue: 'mcpServer' }, ], }; @@ -662,6 +674,27 @@ const CUSTOMIZATION_LOAD_STATE_UNION: UnionConfig = { ], }; +const MCP_SERVER_STATUS_UNION: UnionConfig = { + name: 'McpServerState', + discriminantField: 'kind', + variants: [ + { caseName: 'starting', structName: 'McpServerStartingState', discriminantValue: 'starting' }, + { caseName: 'ready', structName: 'McpServerReadyState', discriminantValue: 'ready' }, + { caseName: 'authRequired', structName: 'McpServerAuthRequiredState', discriminantValue: 'authRequired' }, + { caseName: 'error', structName: 'McpServerErrorState', discriminantValue: 'error' }, + { caseName: 'stopped', structName: 'McpServerStoppedState', discriminantValue: 'stopped' }, + ], +}; + +const TOOL_CALL_CONTRIBUTOR_UNION: UnionConfig = { + name: 'ToolCallContributor', + discriminantField: 'kind', + variants: [ + { caseName: 'client', structName: 'ToolCallClientContributor', discriminantValue: 'client' }, + { caseName: 'mcp', structName: 'ToolCallMcpContributor', discriminantValue: 'mcp' }, + ], +}; + function generateToolResultContentUnion(): string { return `public enum ToolResultContent: Codable, Sendable { case text(ToolResultTextContent) @@ -835,6 +868,10 @@ function generateStateFile(project: Project): string { lines.push(''); lines.push(generateDiscriminatedUnion(CUSTOMIZATION_LOAD_STATE_UNION)); lines.push(''); + lines.push(generateDiscriminatedUnion(MCP_SERVER_STATUS_UNION)); + lines.push(''); + lines.push(generateDiscriminatedUnion(TOOL_CALL_CONTRIBUTOR_UNION)); + lines.push(''); lines.push(generateToolResultContentUnion()); lines.push(''); lines.push(generateSnapshotState()); @@ -885,6 +922,7 @@ const ACTION_VARIANTS: { type: string; caseName: string; tsInterface: string }[] { type: 'session/customizationToggled', caseName: 'sessionCustomizationToggled', tsInterface: 'SessionCustomizationToggledAction' }, { type: 'session/customizationUpdated', caseName: 'sessionCustomizationUpdated', tsInterface: 'SessionCustomizationUpdatedAction' }, { type: 'session/customizationRemoved', caseName: 'sessionCustomizationRemoved', tsInterface: 'SessionCustomizationRemovedAction' }, + { type: 'session/mcpServerStateChanged', caseName: 'sessionMcpServerStatusChanged', tsInterface: 'SessionMcpServerStateChangedAction' }, { type: 'session/truncated', caseName: 'sessionTruncated', tsInterface: 'SessionTruncatedAction' }, { type: 'session/configChanged', caseName: 'sessionConfigChanged', tsInterface: 'SessionConfigChangedAction' }, { type: 'session/metaChanged', caseName: 'sessionMetaChanged', tsInterface: 'SessionMetaChangedAction' }, @@ -1052,7 +1090,7 @@ function generateActionsFile(project: Project): string { const COMMAND_ENUMS = ['ReconnectResultType', 'ContentEncoding', 'CompletionItemKind', 'ResourceType', 'ResourceWriteMode']; const COMMAND_STRUCTS = [ - 'InitializeParams', 'InitializeResult', + 'InitializeParams', 'InitializeResult', 'ClientCapabilities', 'ReconnectParams', 'ReconnectReplayResult', 'ReconnectSnapshotResult', 'SubscribeParams', 'SubscribeResult', 'SessionForkSource', 'CreateSessionParams', 'DisposeSessionParams', @@ -1604,6 +1642,8 @@ function checkExhaustiveness(project: Project): void { 'ChildCustomization', // CHILD_CUSTOMIZATION_UNION discriminated union 'ChildCustomizationType', // TS subset alias of CustomizationType; consumers reuse the CustomizationType Swift enum 'CustomizationLoadState', // CUSTOMIZATION_LOAD_STATE_UNION discriminated union + 'McpServerState', // MCP_SERVER_STATUS_UNION discriminated union + 'ToolCallContributor', // TOOL_CALL_CONTRIBUTOR_UNION discriminated union 'AuthRequiredErrorData', // emitted by generateErrorsFile() 'PermissionDeniedErrorData', // emitted by generateErrorsFile() 'UnsupportedProtocolVersionErrorData', // emitted by generateErrorsFile() diff --git a/types/action-origin.generated.ts b/types/action-origin.generated.ts index e1766791..f1a3f1de 100644 --- a/types/action-origin.generated.ts +++ b/types/action-origin.generated.ts @@ -40,7 +40,7 @@ import type { SessionCustomizationToggledAction, SessionCustomizationUpdatedAction, SessionCustomizationRemovedAction, - SessionMcpServerStatusChangedAction, + SessionMcpServerStateChangedAction, SessionTruncatedAction, SessionIsReadChangedAction, SessionIsArchivedChangedAction, @@ -126,7 +126,7 @@ export type SessionAction = | SessionCustomizationToggledAction | SessionCustomizationUpdatedAction | SessionCustomizationRemovedAction - | SessionMcpServerStatusChangedAction + | SessionMcpServerStateChangedAction | SessionTruncatedAction | SessionIsReadChangedAction | SessionIsArchivedChangedAction @@ -179,7 +179,7 @@ export type ServerSessionAction = | SessionCustomizationsChangedAction | SessionCustomizationUpdatedAction | SessionCustomizationRemovedAction - | SessionMcpServerStatusChangedAction + | SessionMcpServerStateChangedAction | SessionActivityChangedAction | SessionChangesetsChangedAction | SessionMetaChangedAction @@ -301,7 +301,7 @@ export const IS_CLIENT_DISPATCHABLE: { readonly [K in StateAction['type']]: bool [ActionType.SessionCustomizationToggled]: true, [ActionType.SessionCustomizationUpdated]: false, [ActionType.SessionCustomizationRemoved]: false, - [ActionType.SessionMcpServerStatusChanged]: false, + [ActionType.SessionMcpServerStateChanged]: false, [ActionType.SessionTruncated]: true, [ActionType.SessionIsReadChanged]: true, [ActionType.SessionIsArchivedChanged]: true, diff --git a/types/channels-session/actions.ts b/types/channels-session/actions.ts index 7f12126a..a6dc109f 100644 --- a/types/channels-session/actions.ts +++ b/types/channels-session/actions.ts @@ -14,7 +14,7 @@ import type { ToolDefinition, SessionActiveClient, Customization, - McpServerStatus, + McpServerState, SessionInputAnswer, SessionInputRequest, SessionInputResponseKind, @@ -646,7 +646,7 @@ export interface SessionCustomizationRemovedAction { * * Locates the target entry by `id`, searching both the top-level * customization list and the `children` array of every container. - * Replaces the entry's {@link McpServerCustomization.runtimeStatus | `runtimeStatus`} + * Replaces the entry's {@link McpServerCustomization.state | `state`} * and {@link McpServerCustomization.channel | `channel`} * (full-replacement semantics: omit `channel` to clear an existing * channel URI). Other fields of the customization are preserved. @@ -655,24 +655,24 @@ export interface SessionCustomizationRemovedAction { * update any other field (name, icons, `mcpApp` capabilities, etc.) use * {@link SessionCustomizationUpdatedAction} instead. * - * When the transition is to {@link McpServerStatusKind.AuthRequired} + * When the transition is to {@link McpServerStatus.AuthRequired} * because of a request issued mid-turn, the host SHOULD also raise * {@link SessionStatus.InputNeeded} on the session — see - * {@link McpServerStatusAuthRequired} for the rationale. + * {@link McpServerAuthRequiredState} for the rationale. * * @category Session Actions * @version 1 */ -export interface SessionMcpServerStatusChangedAction { - type: ActionType.SessionMcpServerStatusChanged; +export interface SessionMcpServerStateChangedAction { + type: ActionType.SessionMcpServerStateChanged; /** The id of the {@link McpServerCustomization} to update. */ id: string; - /** The new runtime status. */ - runtimeStatus: McpServerStatus; + /** The new lifecycle state. */ + state: McpServerState; /** * Updated `mcp://` side-channel URI. Full-replacement: omit to clear * an existing channel (typical when leaving - * {@link McpServerStatusKind.Ready | `Ready`}). + * {@link McpServerStatus.Ready | `Ready`}). */ channel?: URI; } diff --git a/types/channels-session/reducer.ts b/types/channels-session/reducer.ts index 35c0ea2b..b94b270d 100644 --- a/types/channels-session/reducer.ts +++ b/types/channels-session/reducer.ts @@ -679,7 +679,7 @@ export function sessionReducer(state: SessionState, action: SessionAction, log?: return { ...state, customizations: updated }; } - case ActionType.SessionMcpServerStatusChanged: { + case ActionType.SessionMcpServerStateChanged: { const list = state.customizations; if (!list) { return state; @@ -692,7 +692,7 @@ export function sessionReducer(state: SessionState, action: SessionAction, log?: } const updatedEntry: McpServerCustomization = { ...entry, - runtimeStatus: action.runtimeStatus, + state: action.state, channel: action.channel, }; const updated = list.slice(); @@ -719,7 +719,7 @@ export function sessionReducer(state: SessionState, action: SessionAction, log?: changed = true; const updatedChild: McpServerCustomization = { ...child, - runtimeStatus: action.runtimeStatus, + state: action.state, channel: action.channel, }; const newChildren = children.slice(); diff --git a/types/channels-session/state.ts b/types/channels-session/state.ts index 54b25dc1..320b0a84 100644 --- a/types/channels-session/state.ts +++ b/types/channels-session/state.ts @@ -1656,9 +1656,9 @@ export interface McpServerCustomization extends CustomizationBase { */ enabled: boolean; /** - * Current status of the MCP server. + * Current lifecycle state of the MCP server. */ - runtimeStatus: McpServerStatus; + state: McpServerState; /** * An `mcp://`-protocol channel the client uses to side-channel traffic * into the upstream MCP server itself. The channel is NOT a fresh raw MCP @@ -1673,7 +1673,7 @@ export interface McpServerCustomization extends CustomizationBase { * The channel URI SHOULD be stable across the server's lifetime, but * the agent host MAY change it (for example across a restart) and * MAY only expose it while the server is in - * {@link McpServerStatusKind.Ready | `Ready`}. Absence means no + * {@link McpServerStatus.Ready | `Ready`}. Absence means no * side-channel is currently available. */ channel?: URI; @@ -1795,11 +1795,11 @@ export type Customization = // ─── MCP Server State ──────────────────────────────────────────────────────── /** - * Discriminant for the {@link McpServerStatus} union. + * Discriminant for the {@link McpServerState} union. * * @category MCP Server State */ -export const enum McpServerStatusKind { +export const enum McpServerStatus { /** Server has been registered but is not yet running. */ Starting = 'starting', /** Server is running and serving requests. */ @@ -1819,7 +1819,7 @@ export const enum McpServerStatusKind { } /** - * Why an MCP server is currently in the {@link McpServerStatusKind.AuthRequired} + * Why an MCP server is currently in the {@link McpServerStatus.AuthRequired} * state. Mirrors the three failure modes defined by the * [MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization.md). * @@ -1839,7 +1839,7 @@ export const enum McpAuthRequiredReason { * before any tool work is in flight — `InsufficientScope` is almost * always triggered by an MCP request issued mid-turn (a `tools/call`, * `resources/read`, etc.). The host SHOULD pair the - * {@link McpServerStatusAuthRequired} transition with + * {@link McpServerAuthRequiredState} transition with * {@link SessionStatus.InputNeeded} on * {@link SessionSummary.status | the session} so the activity becomes * visible at the session-summary level, and clients SHOULD watch for @@ -1856,8 +1856,8 @@ export const enum McpAuthRequiredReason { * * @category MCP Server State */ -export interface McpServerStatusStarting { - kind: McpServerStatusKind.Starting; +export interface McpServerStartingState { + kind: McpServerStatus.Starting; } /** @@ -1865,8 +1865,8 @@ export interface McpServerStatusStarting { * * @category MCP Server State */ -export interface McpServerStatusReady { - kind: McpServerStatusKind.Ready; +export interface McpServerReadyState { + kind: McpServerStatus.Ready; } /** @@ -1894,8 +1894,8 @@ export interface McpServerStatusReady { * * @category MCP Server State */ -export interface McpServerStatusAuthRequired { - kind: McpServerStatusKind.AuthRequired; +export interface McpServerAuthRequiredState { + kind: McpServerStatus.AuthRequired; /** Why authentication is required. */ reason: McpAuthRequiredReason; /** @@ -1919,13 +1919,13 @@ export interface McpServerStatusAuthRequired { /** * Server failed to start, crashed, or otherwise transitioned to a - * non-recoverable error. Use {@link McpServerStatusKind.AuthRequired} + * non-recoverable error. Use {@link McpServerStatus.AuthRequired} * for authentication failures. * * @category MCP Server State */ -export interface McpServerStatusError { - kind: McpServerStatusKind.Error; +export interface McpServerErrorState { + kind: McpServerStatus.Error; /** Error details. */ error: ErrorInfo; } @@ -1936,18 +1936,19 @@ export interface McpServerStatusError { * * @category MCP Server State */ -export interface McpServerStatusStopped { - kind: McpServerStatusKind.Stopped; +export interface McpServerStoppedState { + kind: McpServerStatus.Stopped; } /** - * Discriminated union of all MCP server statuses. Discriminated by `kind`. + * Discriminated union of all MCP server lifecycle states. + * Discriminated by `kind` (a {@link McpServerStatus} value). * * @category MCP Server State */ -export type McpServerStatus = - | McpServerStatusStarting - | McpServerStatusReady - | McpServerStatusAuthRequired - | McpServerStatusError - | McpServerStatusStopped; +export type McpServerState = + | McpServerStartingState + | McpServerReadyState + | McpServerAuthRequiredState + | McpServerErrorState + | McpServerStoppedState; diff --git a/types/common/actions.ts b/types/common/actions.ts index 79d648f9..b71aa0b3 100644 --- a/types/common/actions.ts +++ b/types/common/actions.ts @@ -49,7 +49,7 @@ import type { SessionCustomizationToggledAction, SessionCustomizationUpdatedAction, SessionCustomizationRemovedAction, - SessionMcpServerStatusChangedAction, + SessionMcpServerStateChangedAction, SessionTruncatedAction, SessionIsReadChangedAction, SessionIsArchivedChangedAction, @@ -128,7 +128,7 @@ export const enum ActionType { SessionCustomizationToggled = 'session/customizationToggled', SessionCustomizationUpdated = 'session/customizationUpdated', SessionCustomizationRemoved = 'session/customizationRemoved', - SessionMcpServerStatusChanged = 'session/mcpServerStatusChanged', + SessionMcpServerStateChanged = 'session/mcpServerStateChanged', SessionTruncated = 'session/truncated', SessionIsReadChanged = 'session/isReadChanged', SessionIsArchivedChanged = 'session/isArchivedChanged', @@ -228,7 +228,7 @@ export type StateAction = | SessionCustomizationToggledAction | SessionCustomizationUpdatedAction | SessionCustomizationRemovedAction - | SessionMcpServerStatusChangedAction + | SessionMcpServerStateChangedAction | SessionTruncatedAction | SessionIsReadChangedAction | SessionIsArchivedChangedAction diff --git a/types/version/registry.ts b/types/version/registry.ts index 22fa43b1..a5ccd414 100644 --- a/types/version/registry.ts +++ b/types/version/registry.ts @@ -110,7 +110,7 @@ export const ACTION_INTRODUCED_IN: { readonly [K in StateAction['type']]: string [ActionType.SessionCustomizationToggled]: '0.1.0', [ActionType.SessionCustomizationUpdated]: '0.1.0', [ActionType.SessionCustomizationRemoved]: '0.2.0', - [ActionType.SessionMcpServerStatusChanged]: '0.3.0', + [ActionType.SessionMcpServerStateChanged]: '0.3.0', [ActionType.SessionTruncated]: '0.1.0', [ActionType.SessionIsReadChanged]: '0.1.0', [ActionType.SessionIsArchivedChanged]: '0.1.0', From 6d5f2f363bb1bb1c69110c327a89a8430a67f132 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 2 Jun 2026 22:14:04 -0700 Subject: [PATCH 3/4] fix: CI build failures on Rust + Kotlin clients - Rust: box McpServer customization variants and SessionMcpServerStateChanged action payload to satisfy clippy::large_enum_variant (ProtectedResourceMetadata payload pushes McpServerState past the 200-byte budget). - Kotlin: emitKDoc sanitizes nested block-comment delimiters (insert U+200B inside */) so doc strings containing 'tools/*' and 'resources/*' don't close the enclosing /** */ early. - Kotlin reducers: add CustomizationMcpServer branches to the four exhaustive 'when' helpers (id/children/withChildren/withEnabled). --- .../microsoft/agenthostprotocol/Reducers.kt | 40 +++++++++++-------- .../generated/Commands.generated.kt | 2 +- .../generated/State.generated.kt | 4 +- clients/rust/crates/ahp-types/src/actions.rs | 4 +- clients/rust/crates/ahp-types/src/state.rs | 6 +-- scripts/generate-kotlin.ts | 7 +++- scripts/generate-rust.ts | 18 ++++++--- 7 files changed, 50 insertions(+), 31 deletions(-) diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt index f7c009b1..a023bcb4 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt @@ -24,6 +24,7 @@ import com.microsoft.agenthostprotocol.generated.ChildCustomizationUnknown import com.microsoft.agenthostprotocol.generated.ConfirmationOption import com.microsoft.agenthostprotocol.generated.Customization import com.microsoft.agenthostprotocol.generated.CustomizationDirectory +import com.microsoft.agenthostprotocol.generated.CustomizationMcpServer import com.microsoft.agenthostprotocol.generated.CustomizationPlugin import com.microsoft.agenthostprotocol.generated.CustomizationUnknown import com.microsoft.agenthostprotocol.generated.ErrorInfo @@ -117,6 +118,7 @@ import com.microsoft.agenthostprotocol.generated.ToolCallCancellationReason import com.microsoft.agenthostprotocol.generated.ToolCallCancelledState import com.microsoft.agenthostprotocol.generated.ToolCallCompletedState import com.microsoft.agenthostprotocol.generated.ToolCallConfirmationReason +import com.microsoft.agenthostprotocol.generated.ToolCallContributor import com.microsoft.agenthostprotocol.generated.ToolCallPendingConfirmationState import com.microsoft.agenthostprotocol.generated.ToolCallPendingResultConfirmationState import com.microsoft.agenthostprotocol.generated.ToolCallResponsePart @@ -250,28 +252,28 @@ private data class ToolCallBase( val toolCallId: String, val toolName: String, val displayName: String, - val toolClientId: String?, + val contributor: ToolCallContributor?, val meta: Map?, ) private fun toolCallBase(tc: ToolCallState): ToolCallBase = when (tc) { is ToolCallStateStreaming -> tc.value.let { - ToolCallBase(it.toolCallId, it.toolName, it.displayName, it.toolClientId, it.meta) + ToolCallBase(it.toolCallId, it.toolName, it.displayName, it.contributor, it.meta) } is ToolCallStatePendingConfirmation -> tc.value.let { - ToolCallBase(it.toolCallId, it.toolName, it.displayName, it.toolClientId, it.meta) + ToolCallBase(it.toolCallId, it.toolName, it.displayName, it.contributor, it.meta) } is ToolCallStateRunning -> tc.value.let { - ToolCallBase(it.toolCallId, it.toolName, it.displayName, it.toolClientId, it.meta) + ToolCallBase(it.toolCallId, it.toolName, it.displayName, it.contributor, it.meta) } is ToolCallStatePendingResultConfirmation -> tc.value.let { - ToolCallBase(it.toolCallId, it.toolName, it.displayName, it.toolClientId, it.meta) + ToolCallBase(it.toolCallId, it.toolName, it.displayName, it.contributor, it.meta) } is ToolCallStateCompleted -> tc.value.let { - ToolCallBase(it.toolCallId, it.toolName, it.displayName, it.toolClientId, it.meta) + ToolCallBase(it.toolCallId, it.toolName, it.displayName, it.contributor, it.meta) } is ToolCallStateCancelled -> tc.value.let { - ToolCallBase(it.toolCallId, it.toolName, it.displayName, it.toolClientId, it.meta) + ToolCallBase(it.toolCallId, it.toolName, it.displayName, it.contributor, it.meta) } // Forward-compat: unknown lifecycle variants have no extractable base; mirror // Rust's `ToolCallState::Unknown(_) => (String::new(), ...)`. Combined with @@ -291,6 +293,7 @@ private fun toolCallIdOf(tc: ToolCallState): String = toolCallBase(tc).toolCallI private fun customizationId(c: Customization): String? = when (c) { is CustomizationPlugin -> c.value.id is CustomizationDirectory -> c.value.id + is CustomizationMcpServer -> c.value.id // Unknown variants carry an opaque `raw` JSON object — no id to expose. // Returning `null` mirrors Rust's `Customization::Unknown(_) => None`, so // an unknown container can never collide with a real id during lookups. @@ -300,6 +303,7 @@ private fun customizationId(c: Customization): String? = when (c) { private fun customizationChildren(c: Customization): List? = when (c) { is CustomizationPlugin -> c.value.children is CustomizationDirectory -> c.value.children + is CustomizationMcpServer -> null is CustomizationUnknown -> null } @@ -307,12 +311,14 @@ private fun withCustomizationChildren(c: Customization, children: List CustomizationPlugin(c.value.copy(children = children)) is CustomizationDirectory -> CustomizationDirectory(c.value.copy(children = children)) // Pass-through: we can't structurally edit a payload we don't understand. + is CustomizationMcpServer -> c is CustomizationUnknown -> c } private fun withCustomizationEnabled(c: Customization, enabled: Boolean): Customization = when (c) { is CustomizationPlugin -> CustomizationPlugin(c.value.copy(enabled = enabled)) is CustomizationDirectory -> CustomizationDirectory(c.value.copy(enabled = enabled)) + is CustomizationMcpServer -> CustomizationMcpServer(c.value.copy(enabled = enabled)) is CustomizationUnknown -> c } @@ -444,7 +450,7 @@ private fun endTurn( toolCallId = base.toolCallId, toolName = base.toolName, displayName = base.displayName, - toolClientId = base.toolClientId, + contributor = base.contributor, meta = base.meta, invocationMessage = invocationMessage, toolInput = toolInput, @@ -634,7 +640,7 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat toolCallId = a.toolCallId, toolName = a.toolName, displayName = a.displayName, - toolClientId = a.toolClientId, + contributor = a.contributor, meta = a.meta, status = ToolCallStatus.STREAMING, ), @@ -673,7 +679,7 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat toolCallId = base.toolCallId, toolName = base.toolName, displayName = base.displayName, - toolClientId = base.toolClientId, + contributor = base.contributor, meta = base.meta, invocationMessage = a.invocationMessage, toolInput = a.toolInput, @@ -687,7 +693,7 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat toolCallId = base.toolCallId, toolName = base.toolName, displayName = base.displayName, - toolClientId = base.toolClientId, + contributor = base.contributor, meta = base.meta, invocationMessage = a.invocationMessage, toolInput = a.toolInput, @@ -717,7 +723,7 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat toolCallId = base.toolCallId, toolName = base.toolName, displayName = base.displayName, - toolClientId = base.toolClientId, + contributor = base.contributor, meta = base.meta, invocationMessage = tc.value.invocationMessage, toolInput = a.editedToolInput ?: tc.value.toolInput, @@ -733,7 +739,7 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat toolCallId = base.toolCallId, toolName = base.toolName, displayName = base.displayName, - toolClientId = base.toolClientId, + contributor = base.contributor, meta = base.meta, invocationMessage = tc.value.invocationMessage, toolInput = tc.value.toolInput, @@ -777,7 +783,7 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat toolCallId = base.toolCallId, toolName = base.toolName, displayName = base.displayName, - toolClientId = base.toolClientId, + contributor = base.contributor, meta = base.meta, invocationMessage = invocationMessage, toolInput = toolInput, @@ -797,7 +803,7 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat toolCallId = base.toolCallId, toolName = base.toolName, displayName = base.displayName, - toolClientId = base.toolClientId, + contributor = base.contributor, meta = base.meta, invocationMessage = invocationMessage, toolInput = toolInput, @@ -828,7 +834,7 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat toolCallId = base.toolCallId, toolName = base.toolName, displayName = base.displayName, - toolClientId = base.toolClientId, + contributor = base.contributor, meta = base.meta, invocationMessage = tc.value.invocationMessage, toolInput = tc.value.toolInput, @@ -848,7 +854,7 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat toolCallId = base.toolCallId, toolName = base.toolName, displayName = base.displayName, - toolClientId = base.toolClientId, + contributor = base.contributor, meta = base.meta, invocationMessage = tc.value.invocationMessage, toolInput = tc.value.toolInput, diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt index 76b06987..96f9d7c5 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt @@ -185,7 +185,7 @@ data class ClientCapabilities( /** * Client can render * [MCP Apps](https://github.com/modelcontextprotocol/ext-apps) — i.e. - * it can host the View sandbox, run the `ui/*` protocol against it, + * it can host the View sandbox, run the `ui/​*` protocol against it, * and forward `mcp://`-channel traffic on the App's behalf. * * Hosts SHOULD only populate diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt index 919cb0ca..6a0aba8f 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt @@ -2812,11 +2812,11 @@ data class McpServerCustomizationApps( @Serializable data class AhpMcpUiHostCapabilities( /** - * Producer proxies the MCP `tools/*` methods to the upstream server. + * Producer proxies the MCP `tools/​*` methods to the upstream server. */ val serverTools: JsonElement? = null, /** - * Producer proxies the MCP `resources/*` methods to the upstream server. + * Producer proxies the MCP `resources/​*` methods to the upstream server. */ val serverResources: JsonElement? = null, /** diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index c0f7ef46..eda1cc25 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -1206,11 +1206,11 @@ pub enum StateAction { #[serde(rename = "session/customizationToggled")] SessionCustomizationToggled(SessionCustomizationToggledAction), #[serde(rename = "session/customizationUpdated")] - SessionCustomizationUpdated(SessionCustomizationUpdatedAction), + SessionCustomizationUpdated(Box), #[serde(rename = "session/customizationRemoved")] SessionCustomizationRemoved(SessionCustomizationRemovedAction), #[serde(rename = "session/mcpServerStateChanged")] - SessionMcpServerStateChanged(SessionMcpServerStateChangedAction), + SessionMcpServerStateChanged(Box), #[serde(rename = "session/truncated")] SessionTruncated(SessionTruncatedAction), #[serde(rename = "session/configChanged")] diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 38840234..b45a1be7 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -3061,7 +3061,7 @@ pub enum Customization { #[serde(rename = "directory")] Directory(DirectoryCustomization), #[serde(rename = "mcpServer")] - McpServer(McpServerCustomization), + McpServer(Box), /// Unknown or future variant — preserved as raw JSON for round-trip fidelity. /// Reducers treat this as a no-op. #[serde(untagged)] @@ -3083,7 +3083,7 @@ pub enum ChildCustomization { #[serde(rename = "hook")] Hook(HookCustomization), #[serde(rename = "mcpServer")] - McpServer(McpServerCustomization), + McpServer(Box), /// Unknown or future variant — preserved as raw JSON for round-trip fidelity. /// Reducers treat this as a no-op. #[serde(untagged)] @@ -3117,7 +3117,7 @@ pub enum McpServerState { #[serde(rename = "ready")] Ready(McpServerReadyState), #[serde(rename = "authRequired")] - AuthRequired(McpServerAuthRequiredState), + AuthRequired(Box), #[serde(rename = "error")] Error(McpServerErrorState), #[serde(rename = "stopped")] diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index 653a6a7e..05122bad 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -318,7 +318,12 @@ function emitKDoc(doc: string, indent = ''): string[] { const lines = doc.split('\n'); const out: string[] = [`${indent}/**`]; for (const line of lines) { - out.push(`${indent} * ${line.trim()}`); + // Kotlin's tokenizer treats `/*` and `*/` as block-comment delimiters even + // inside KDoc backtick spans, so `\`tools/*\`` would open a nested + // comment that never closes. Insert a zero-width space (U+200B) to break + // the token without changing the rendered output. + const safe = line.trim().replace(/\/\*/g, '/\u200B*').replace(/\*\//g, '*\u200B/'); + out.push(`${indent} * ${safe}`); } out.push(`${indent} */`); return out; diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index 34c3cb76..4debb72a 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -767,7 +767,9 @@ const CUSTOMIZATION_UNION: UnionConfig = { variants: [ { variantName: 'Plugin', innerType: 'PluginCustomization', wireValue: 'plugin' }, { variantName: 'Directory', innerType: 'DirectoryCustomization', wireValue: 'directory' }, - { variantName: 'McpServer', innerType: 'McpServerCustomization', wireValue: 'mcpServer' }, + // Boxed: `McpServerCustomization` is significantly larger than the + // other variants thanks to its transitive `ProtectedResourceMetadata`. + { variantName: 'McpServer', innerType: 'McpServerCustomization', wireValue: 'mcpServer', boxed: true }, ], unknown: true, }; @@ -782,7 +784,8 @@ const CHILD_CUSTOMIZATION_UNION: UnionConfig = { { variantName: 'Prompt', innerType: 'PromptCustomization', wireValue: 'prompt' }, { variantName: 'Rule', innerType: 'RuleCustomization', wireValue: 'rule' }, { variantName: 'Hook', innerType: 'HookCustomization', wireValue: 'hook' }, - { variantName: 'McpServer', innerType: 'McpServerCustomization', wireValue: 'mcpServer' }, + // Boxed: see comment on `Customization::McpServer`. + { variantName: 'McpServer', innerType: 'McpServerCustomization', wireValue: 'mcpServer', boxed: true }, ], unknown: true, }; @@ -807,7 +810,9 @@ const MCP_SERVER_STATUS_UNION: UnionConfig = { variants: [ { variantName: 'Starting', innerType: 'McpServerStartingState', wireValue: 'starting' }, { variantName: 'Ready', innerType: 'McpServerReadyState', wireValue: 'ready' }, - { variantName: 'AuthRequired', innerType: 'McpServerAuthRequiredState', wireValue: 'authRequired' }, + // Boxed: `McpServerAuthRequiredState` carries the large RFC 9728 + // `ProtectedResourceMetadata` payload. + { variantName: 'AuthRequired', innerType: 'McpServerAuthRequiredState', wireValue: 'authRequired', boxed: true }, { variantName: 'Error', innerType: 'McpServerErrorState', wireValue: 'error' }, { variantName: 'Stopped', innerType: 'McpServerStoppedState', wireValue: 'stopped' }, ], @@ -909,6 +914,8 @@ const ACTION_VARIANTS: { variantName: string; tsInterface: string; rustName?: string; + /** Box the variant payload in the `StateAction` enum (reduces enum size). */ + boxed?: boolean; }[] = [ { type: 'root/agentsChanged', variantName: 'RootAgentsChanged', tsInterface: 'RootAgentsChangedAction' }, { type: 'root/activeSessionsChanged', variantName: 'RootActiveSessionsChanged', tsInterface: 'RootActiveSessionsChangedAction' }, @@ -947,9 +954,9 @@ const ACTION_VARIANTS: { { type: 'session/inputCompleted', variantName: 'SessionInputCompleted', tsInterface: 'SessionInputCompletedAction' }, { type: 'session/customizationsChanged', variantName: 'SessionCustomizationsChanged', tsInterface: 'SessionCustomizationsChangedAction' }, { type: 'session/customizationToggled', variantName: 'SessionCustomizationToggled', tsInterface: 'SessionCustomizationToggledAction' }, - { type: 'session/customizationUpdated', variantName: 'SessionCustomizationUpdated', tsInterface: 'SessionCustomizationUpdatedAction' }, + { type: 'session/customizationUpdated', variantName: 'SessionCustomizationUpdated', tsInterface: 'SessionCustomizationUpdatedAction', boxed: true }, { type: 'session/customizationRemoved', variantName: 'SessionCustomizationRemoved', tsInterface: 'SessionCustomizationRemovedAction' }, - { type: 'session/mcpServerStateChanged', variantName: 'SessionMcpServerStateChanged', tsInterface: 'SessionMcpServerStateChangedAction' }, + { type: 'session/mcpServerStateChanged', variantName: 'SessionMcpServerStateChanged', tsInterface: 'SessionMcpServerStateChangedAction', boxed: true }, { type: 'session/truncated', variantName: 'SessionTruncated', tsInterface: 'SessionTruncatedAction' }, { type: 'session/configChanged', variantName: 'SessionConfigChanged', tsInterface: 'SessionConfigChangedAction' }, { type: 'session/metaChanged', variantName: 'SessionMetaChanged', tsInterface: 'SessionMetaChangedAction' }, @@ -1071,6 +1078,7 @@ pub struct ActionEnvelope { variantName: v.variantName, innerType: v.tsInterface === '_merged_' ? 'SessionToolCallConfirmedAction' : stripIPrefix(v.tsInterface), wireValue: v.type, + boxed: v.boxed, })); lines.push(generateDiscriminatedUnion({ name: 'StateAction', From 353c8f84ce3edafec7038f7b189a6f1edaf46a48 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 2 Jun 2026 22:21:03 -0700 Subject: [PATCH 4/4] fix(rust): add missing capabilities field in InitializeParams doctest --- clients/rust/crates/ahp-types/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/clients/rust/crates/ahp-types/src/lib.rs b/clients/rust/crates/ahp-types/src/lib.rs index 587771b6..74cff5ff 100644 --- a/clients/rust/crates/ahp-types/src/lib.rs +++ b/clients/rust/crates/ahp-types/src/lib.rs @@ -68,6 +68,7 @@ //! client_id: "my-host/1.0".into(), //! initial_subscriptions: Some(vec!["ahp-root://".into()]), //! locale: Some("en".into()), +//! capabilities: None, //! }; //! //! let req = JsonRpcMessage::Request(JsonRpcRequest {