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).

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. 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.

TIP

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

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 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.