Skip to content

MCP Servers

Model Context Protocol servers are surfaced in AHP as a 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 the client can use to talk to the upstream server when it needs to render an MCP App.

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:

  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)
  state: McpServerState // 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).

Clients can also ask the host to manage the server process without changing the customization's enabled intent:

  • session/mcpServerStartRequested asks the host to start or restart an existing MCP server customization. Reducers optimistically move the server to starting and clear any stale channel; the host remains authoritative and follows with session/mcpServerStateChanged.
  • session/mcpServerStopRequested asks the host to stop an existing MCP server customization. Reducers optimistically move the server to stopped and clear any stale channel. Stopping an authRequired server unblocks it from waiting on authentication; if the host raised session-level input solely for that server, it should remove that input-needed entry when accepting the stop.

Runtime status

state is a discriminated union on kind. It is the host's view of the server's lifecycle, separate from enabled (which is the user's intent).

KindMeaning
startingRegistered but not yet running. Tools/resources are not available.
readyRunning and serving requests. Tools/resources surface through the usual channels.
authRequiredReachable but blocked on authentication. Carries ProtectedResourceMetadata for the client to act on.
errorUnrecoverable failure. Carries an ErrorInfo. Use authRequired for auth-specific failures.
stoppedShut down. The host MAY remove the entry shortly after.

High-frequency lifecycle transitions go through the narrow session/mcpServerStateChanged action, which upserts just state (and optionally channel) on an existing entry. Client start/stop intent goes through session/mcpServerStartRequested and session/mcpServerStopRequested; use session/customizationUpdated for anything else (name, icons, mcpApp).

Authentication

AHP reuses the existing authenticate command for MCP server authentication. The flow is driven entirely by state — there is no MCP-specific notification.

McpServerAuthRequiredState carries:

  • reasonrequired, expired, or insufficientScope. Mirrors the MCP authorization spec failure modes.
  • resourceProtectedResourceMetadata per RFC 9728. 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 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 state.kind of any MCP server backing a running tool call (via 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 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.

The authenticate command's resource field accepts any protected-resource identifier the server has advertised — statically via AgentInfo.protectedResources, or dynamically via a live McpServerAuthRequiredState.resource (or, for the tool-call-level state below, ToolCallAuthRequiredState.auth.resource). Servers surfacing MCP auth this way don't need to mirror the resource into AgentInfo.protectedResources.

Tool-call-level authentication

The McpServerAuthRequiredState above describes the server's lifecycle — it can't serve any request while blocked. A distinct, first-class tool-call status, ToolCallStatus.AuthRequired, describes a specific in-flight tool call pausing on the same kind of challenge:

  • The server dispatches chat/toolCallAuthRequired with an auth: McpAuthRequirement object — the same { reason, resource, requiredScopes?, description? } shape as McpServerAuthRequiredState, minus kind, and factored into a shared McpAuthRequirement interface so both describe the same OAuth challenge with one vocabulary. It carries no token.
  • This is normally reached from, and returned to, running — it is not part of ToolCallConfirmationState (the parameter/result confirmation union), since it isn't resolved by a chat/toolCallConfirmed-style decision. It's resolved by the client obtaining a token for auth.resource and pushing it via authenticate; the host then dispatches chat/toolCallAuthResolved to resume the call. A client MAY instead dispatch chat/toolCallComplete with a failed result (e.g. error.code: 'cancelled') to cancel the invocation outright without ever authenticating; the reducer accepts this transition from auth-required the same way it does from running, but always moves straight to completedrequiresResultConfirmation is ignored for this path, since a cancelled auth challenge has no real result to review. A successful result dispatched from auth-required is invalid and the reducer ignores it as a no-op, leaving the tool call in auth-required; only a failed result can complete it.
  • It only ever applies to MCP-contributed tool calls: ToolCallAuthRequiredState.contributor is narrowed to the MCP variant of ToolCallContributor, so the invariant "auth-required implies MCP-contributed" is structurally enforced rather than merely documented.
  • The host SHOULD surface a session/inputNeededSet entry with kind: 'toolAuthentication' (see Aggregated Input Requests) correlating the blocked tool call to its chat and turn, so clients that only watch the session can discover and resolve it via authenticate without subscribing to the chat.

The server-level and tool-call-level states are dispatched independently and deliberately kept separate: "the MCP server needs auth" and "this specific invocation is waiting on that auth" are different facts that don't always coincide (a step-up challenge triggered by one tool call need not block the whole server).

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 chat/toolCallStart / chat/toolCallReady / chat/toolCallComplete.
  • The originating MCP server is identified by ToolCallContributor on the tool call: { kind: 'mcp', customizationId: <McpServerCustomization.id> }. 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 (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

  • 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 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 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 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 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 capabilityWhat the host promisesWhat the client passes to ui/initialize
serverToolstools/list and tools/call will be proxied via the mcp:// channel. listChanged controls whether notifications/tools/list_changed is forwarded.hostCapabilities.serverTools (mirroring listChanged)
serverResourcesresources/list, resources/templates/list, and resources/read will be proxied. listChanged controls notification forwarding.hostCapabilities.serverResources
loggingnotifications/message from the App will be forwarded to the server; logging/setLevel from the server will reach the App.hostCapabilities.logging ({})
samplingsampling/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. 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

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

Released under the MIT License.