From 4e8107113b6817289b82573b64b95000fad80ab0 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 24 Sep 2026 12:26:35 +0200 Subject: [PATCH 01/10] docs(rfd): audit MCP-over-ACP for stateless MCP --- docs/rfds/mcp-over-acp.mdx | 336 ++++++++++++++++++++++++++++--------- docs/rfds/proxy-chains.mdx | 10 +- 2 files changed, 264 insertions(+), 82 deletions(-) diff --git a/docs/rfds/mcp-over-acp.mdx b/docs/rfds/mcp-over-acp.mdx index 4242c5b6b..bfe0ffd1c 100644 --- a/docs/rfds/mcp-over-acp.mdx +++ b/docs/rfds/mcp-over-acp.mdx @@ -4,6 +4,8 @@ title: "MCP-over-ACP: MCP Transport via ACP Channels" Author(s): [nikomatsakis](https://github.com/nikomatsakis) +**Revised target: MCP 2026-07-28 only.** The [modernization audit](#modernization-audit-mcp-2026-07-28) below defines the work needed for the latest, stateless MCP specification. The connection-oriented proposal and implementation notes preceding that audit are retained as a **work-in-progress checkpoint**, not the intended stabilized transport. In particular, the initialization handshake, reverse JSON-RPC requests, and stateful HTTP sessions described there must not become compatibility requirements. This transport is intended to supply new tools to agents; support for older MCP revisions is out of scope. + ## Elevator pitch > What are you proposing to change? @@ -30,63 +32,67 @@ This enables patterns like: - A **client** that injects project-aware tools into every session and handles callbacks directly - An **[agent extension](./proxy-chains.mdx)** that adds context-aware tools based on the conversation state -- A **bridge** that translates ACP-transport MCP servers to stdio for agents that don't support native ACP transport +- A **bridge** that translates ACP-transport MCP servers to HTTP or stdio for agents that don't support native ACP transport ### How it works -When the client connects, the agent advertises MCP-over-ACP support via `mcpCapabilities.acp` in its `InitializeResponse`. If supported, the client can add MCP servers to a `session/new` request with `"type": "acp"` and an `id` that identifies the server: +When the client connects, the agent advertises MCP-over-ACP support in its `InitializeResponse` (see [Capability advertising](#capability-advertising) for the v1 and draft-v2 shapes). If supported, the client can add MCP servers to a `session/new` request with `"type": "acp"` and a `serverId` that identifies the server. For example, the session request parameters can include: ```json { - "tools": { - "mcpServers": [ - { - "type": "acp", - "name": "project-tools", - "id": "550e8400-e29b-41d4-a716-446655440000" - } - ] - } + "cwd": "/workspace/project", + "mcpServers": [ + { + "type": "acp", + "name": "project-tools", + "serverId": "550e8400-e29b-41d4-a716-446655440000" + } + ] } ``` -The `id` is generated by the component providing the MCP server. +The `serverId` is an opaque string generated by the component providing the MCP server; it need not be a UUID. The same declaration can be supplied to other session setup methods that accept `mcpServers`, such as `session/resume`. -When the agent connects to the MCP server, an `mcp/connect` message is sent with the MCP server's `id`. This returns a fresh `connectionId`. MCP messages are then sent back and forth using `mcp/message` requests and notifications. Finally, `mcp/disconnect` signals that the connection is closing. +When the agent connects to the MCP server, an `mcp/connect` request is sent with the MCP server's `serverId`. This returns a fresh `connectionId`. MCP messages are then sent back and forth using `mcp/message` requests and notifications. This includes the normal MCP initialization handshake: opening the transport does not initialize MCP itself. Finally, an `mcp/disconnect` request closes that connection. `mcp/connect` and `mcp/disconnect` are initiated by the side connecting to the ACP-transport MCP server. In the direct client-provided server case, that means the agent sends them to the client. Once connected, `mcp/message` is bidirectional: the agent can send MCP client-originated requests to the server, and the server can send MCP server-originated requests or notifications back to the agent. ### Bridging and compatibility -Existing agents don't support ACP transport for MCP servers. To bridge this gap, a wrapper component can translate between ACP-transport MCP servers and the stdio/HTTP transports that agents already support. The wrapper spawns shim processes or HTTP servers that the agent connects to normally, then relays messages to/from the ACP channel. +For agents that don't support ACP transport for MCP servers, a wrapper component can translate between ACP-transport MCP servers and the stdio/HTTP transports that those agents support. The wrapper spawns shim processes or HTTP servers that the agent connects to normally, then relays messages to/from the ACP channel. -We've implemented this bridging as part of the conductor described in the [Proxy Chains RFD](./proxy-chains). The conductor always advertises `mcpCapabilities.acp: true` to its clients, handling the translation transparently regardless of whether the downstream agent supports native ACP transport. +The Rust SDK implements HTTP adaptation as an explicit `McpOverAcpPolyfill` proxy from `agent-client-protocol-polyfill`, placed immediately before the final agent in a [proxy chain](./proxy-chains). It is not built into the conductor. The proxy advertises ACP MCP support upstream only when the downstream agent supports native ACP transport or the HTTP transport it can adapt to. Native support is passed through unchanged; a downstream agent supporting neither transport does not gain the capability. ### Message flow example +An agent may initialize MCP servers before returning the ACP session ID. Providers must therefore be ready to handle `mcp/connect` as soon as they publish the declaration, without waiting for `session/new` to finish. + ```mermaid sequenceDiagram participant Client participant Agent - Client->>Agent: session/new (with ACP-transport MCP server) + Client->>Agent: "session/new with an ACP-transport MCP server" + Agent->>Client: "mcp/connect with serverId" + Client-->>Agent: "connectionId" + Agent->>Client: "mcp/message wrapping MCP initialize" + Client-->>Agent: "MCP initialize result" + Agent->>Client: "mcp/message wrapping notifications/initialized" Agent-->>Client: session created - Client->>Agent: prompt ("analyze this codebase") + Client->>Agent: "session/prompt" Note over Agent: Agent decides to use the tool - Agent->>Client: mcp/connect (acpId: "") - Client-->>Agent: connectionId: "conn-1" - - Agent->>Client: mcp/message (list_files tool call) + Agent->>Client: "mcp/message wrapping tools/call" Client-->>Agent: file listing results - Client->>Agent: mcp/message (server callback or notification) - Agent-->>Client: callback result, if request + Client->>Agent: "mcp/message wrapping a server request" + Agent-->>Client: callback result Agent-->>Client: response using tool results - Agent->>Client: mcp/disconnect (connectionId: "conn-1") + Agent->>Client: "mcp/disconnect with connectionId" + Client-->>Agent: "Empty result after connection cleanup" ``` ## Shiny future @@ -111,29 +117,47 @@ For agents that don't natively support ACP transport, intermediaries can transpa ### Capability advertising -Agents advertise MCP-over-ACP support via the [`mcpCapabilities`](/protocol/v1/schema#mcpcapabilities) field in their `InitializeResponse`. We propose adding an `acp` field to this existing structure: +The shared schema exposes this draft transport under the `unstable_mcp_over_acp` feature. Capability placement depends on the negotiated ACP version; the `mcp/*` envelopes below are the same in both versions. + +In **v1**, agents advertise support with `agentCapabilities.mcpCapabilities.acp: true`. The relevant `InitializeResponse` fragment is: ```json { - "capabilities": { + "agentCapabilities": { "mcpCapabilities": { - "http": false, - "sse": false, "acp": true } } } ``` -When `mcpCapabilities.acp` is `true`, the agent can handle MCP servers declared with `"type": "acp"` natively. It will initiate `mcp/connect` and `mcp/disconnect` through the ACP channel, and both sides can exchange MCP payloads with `mcp/message`. +Omitting the v1 capability is equivalent to `false`. + +In **draft v2**, capabilities use optional objects. The relevant `InitializeResponse` fragment is: + +```json +{ + "capabilities": { + "session": { + "mcp": { + "acp": {} + } + } + } +} +``` + +The v2 `acp` field is optional and nullable: omission or `null` means support is not advertised, while `{}` advertises support. It is not a boolean. HTTP support follows the same optional-object convention in v2. + +Advertising support means the receiving component can consume MCP servers declared with `"type": "acp"`. It will initiate `mcp/connect` and `mcp/disconnect` through the ACP channel, and both sides can exchange MCP payloads with `mcp/message`. Clients don't need to advertise anything - they simply check the agent's capabilities to determine whether bridging is needed. -**Bridging intermediaries**: An intermediary that provides bridging can present `mcpCapabilities.acp: true` to its clients regardless of whether the downstream agent supports it, handling bridging transparently (see [Bridging](#bridging-for-agents-without-native-support) below). +**Bridging intermediaries**: An intermediary may advertise ACP MCP support if it can actually adapt to a transport supported by its downstream agent. That capability describes the chain as seen upstream, not necessarily native support in the final agent (see [Bridging](#bridging-for-agents-without-native-support) below). ### MCP transport schema extension -We extend the MCP server JSON schema to include ACP as a transport option: +We extend the MCP server JSON schema to include ACP as a transport option. `type`, `name`, and `serverId` are required and non-null. `_meta` is optional; omission and `null` both mean no additional metadata. ```json { @@ -146,94 +170,138 @@ We extend the MCP server JSON schema to include ACP as a transport option: "name": { "type": "string" }, - "id": { + "serverId": { "type": "string" }, "_meta": { "type": ["object", "null"] } }, - "required": ["type", "name", "id"] + "required": ["type", "name", "serverId"] } ``` ### Message reference -**Connection lifecycle:** +**Open a connection:** + +`mcp/connect` is a request, not a notification. Its required `serverId` selects the declared MCP server. ```json -// Establish MCP connection { + "jsonrpc": "2.0", + "id": 20, "method": "mcp/connect", "params": { - "acpId": "550e8400-e29b-41d4-a716-446655440000", - "_meta": { ... } + "serverId": "550e8400-e29b-41d4-a716-446655440000" } } -// Response result: +``` + +The response returns the required identifier for the new connection: + +```json { - "connectionId": "conn-123", - "_meta": { ... } + "jsonrpc": "2.0", + "id": 20, + "result": { + "connectionId": "conn-123" + } } +``` + +**Close a connection:** -// Close MCP connection +`mcp/disconnect` is also a request. Its required `connectionId` identifies the connection to close, not the server declaration. + +```json { + "jsonrpc": "2.0", + "id": 21, "method": "mcp/disconnect", "params": { - "connectionId": "conn-123", - "_meta": { ... } + "connectionId": "conn-123" } } -// Response result: +``` + +A successful response acknowledges connection cleanup: + +```json { - "_meta": { ... } + "jsonrpc": "2.0", + "id": 21, + "result": {} } ``` +These lifecycle request parameters and response results may also contain an optional `_meta` object. Omission and `null` are equivalent. Both identifier fields are non-null strings. + **MCP message exchange:** `mcp/message` is bidirectional. Either side can send the following request or notification shape on an established `connectionId`. ```json -// Send MCP request { + "jsonrpc": "2.0", "id": 123, "method": "mcp/message", "params": { "connectionId": "conn-123", - "method": "", - "params": { ... }, - "_meta": { ... } + "method": "tools/list", + "params": {} } } -// Response result: +``` + +The response carries the inner MCP result directly: + +```json { - ... inner MCP result payload ... + "jsonrpc": "2.0", + "id": 123, + "result": { + "tools": [] + } } +``` + +A notification has no outer request ID and receives no response: -// Send MCP notification +```json { + "jsonrpc": "2.0", "method": "mcp/message", "params": { "connectionId": "conn-123", - "method": "", - "params": { ... }, - "_meta": { ... } + "method": "notifications/tools/list_changed" } } ``` -The inner MCP message fields (`method`, `params`) are flattened into the params object. The `params` field is optional; if omitted or set to `null`, the inner MCP message has no params. Whether the wrapped message is a request or notification is determined by the presence of an `id` field in the outer JSON-RPC envelope, following JSON-RPC conventions. For requests, the ACP response result is the inner MCP result payload, and inner MCP errors are represented with the outer JSON-RPC error response. +The inner MCP message fields (`method`, `params`) are flattened into the outer params object alongside the required `connectionId`. The inner `method` is a required non-null string. The inner `params` field is optional and accepts an object or `null`, not positional arrays; omission and `null` both mean the inner MCP message has no params. + +Whether the wrapped message is a request or notification is determined by the presence of an `id` field in the outer JSON-RPC envelope. The envelope does not carry a second, nested MCP request ID. For requests, the ACP response result is the inner MCP result payload, and inner MCP errors use the outer JSON-RPC error response, preserving their code, message, and optional data. + +An optional `_meta` object alongside `connectionId` is ACP envelope metadata; omission and `null` are equivalent. It is separate from any MCP `_meta` inside the inner `params` or result. ### Routing by ID -The `acpId` in `mcp/connect` matches the `id` that was provided by the component when it declared the MCP server in `session/new`. The receiving side uses this `id` to route messages to the correct handler. +The `serverId` in `mcp/connect` matches the `serverId` supplied in the MCP server declaration. The receiving side uses it to route the connection request to the provider. -When a component provides multiple MCP servers in a single session, each gets a unique `id`, enabling proper message routing. +Providers must not reuse a server ID for different MCP servers visible on the same ACP connection, even across different ACP sessions. The same server may be offered to multiple sessions using the same server ID. A `connectionId` identifies one active connection to that server and is used for all subsequent messages. ### Connection multiplexing -Multiple connections to the same MCP server are supported - each `mcp/connect` returns a unique `connectionId`. This allows scenarios where an agent opens multiple concurrent connections to the same tool server. +Multiple connections to the same MCP server are supported: every successful `mcp/connect` returns a fresh `connectionId`. Each connection has its own MCP initialization and request state. Closing one connection must not close another connection to the same server or the containing ACP connection. + +### Connection lifetime + +A successful `mcp/connect` response means the provider is ready to route messages for that connection. Both sides must continue dispatching incoming ACP traffic while waiting for MCP responses, since the MCP server can issue a request back to the agent while handling an agent request. + +`mcp/disconnect` stops accepting new messages for that connection, stops its underlying server/relay work, and releases its connection-scoped resources before acknowledging success. Outstanding requests on the closed connection must complete or fail rather than remain pending indefinitely. Messages for an unknown or disconnected connection cannot be delivered: requests receive an error, and notifications do not receive a response. + +Closing the ACP transport releases all of its MCP connections. A disconnect exchange cannot be required after that transport is already gone. A failure in one MCP connection should be contained to that connection rather than terminating unrelated MCP or ACP work. ### Bridging for agents without native support @@ -241,51 +309,65 @@ Not all agents will support MCP-over-ACP natively. To maintain compatibility, it **Bridging approaches:** -- **Stdio shim**: Spawn a small shim process that the agent connects to via stdio. The shim relays MCP messages to/from the ACP channel. This is the most compatible approach since all MCP-capable agents support stdio. +- **Stdio shim**: Spawn a small shim process that the agent connects to via stdio. The shim relays MCP messages to/from the ACP channel. This works for agents that support stdio MCP servers. - **HTTP bridge**: Run a local HTTP server that the agent connects to. MCP messages are relayed to/from the ACP channel. This works for agents that prefer HTTP transport. **How bridging works:** -When a client provides an MCP server with `"type": "acp"`, and the agent doesn't advertise `mcpCapabilities.acp: true`, a bridge can: +When a client provides an MCP server with `"type": "acp"`, and the agent doesn't advertise native ACP MCP support, a bridge can: -1. Rewrite the MCP server declaration in `session/new` to use stdio or HTTP transport +1. Rewrite the MCP server declaration in a session setup request to use a transport supported by the agent 2. Spawn the appropriate shim process or HTTP server -3. Relay messages between the shim and the ACP channel +3. Open a native MCP connection when a logical MCP client session starts +4. Relay bidirectional requests and notifications between that client and the ACP channel +5. Disconnect that native connection when the logical MCP session ends, without affecting other sessions From the agent's perspective, it's talking to a normal stdio/HTTP MCP server. From the client's perspective, it's handling MCP-over-ACP messages. The bridge handles the translation transparently. +An HTTP bridge may reuse a listening endpoint for a `serverId`, but the listener is not itself an MCP connection. Each independent HTTP MCP session needs its own `mcp/connect` and `connectionId`, so initialization, request IDs, and callbacks cannot cross between clients. A stateful HTTP adapter can identify these sessions using `MCP-Session-Id` and translate HTTP DELETE into `mcp/disconnect`. Closing an individual POST response or GET event stream does not close the logical session. HTTP session management is an adapter concern, not an additional ACP wire method. + ```mermaid sequenceDiagram participant Client participant Bridge - participant Shim as Stdio Shim participant Agent - Note over Bridge: Agent doesn't support mcpCapabilities.acp - Client->>Bridge: session/new (MCP server with acp transport) - Bridge->>Agent: session/new (MCP server with stdio transport) - Note over Bridge: Spawns shim for bridging + Note over Bridge: "Agent supports HTTP but not native ACP MCP" + Client->>Bridge: "session/new with an ACP MCP declaration" + Bridge->>Agent: "session/new with an HTTP MCP endpoint" + + Agent->>Bridge: "HTTP POST MCP initialize" + Bridge->>Client: "mcp/connect with serverId" + Client-->>Bridge: "connectionId" + Bridge->>Client: "mcp/message wrapping MCP initialize" + Client-->>Bridge: MCP initialize result + Bridge-->>Agent: "MCP initialize result and MCP-Session-Id" - Agent->>Shim: MCP tool call (stdio) - Shim->>Bridge: relay - Bridge->>Client: mcp/message + Agent->>Bridge: "HTTP POST tools/call with MCP-Session-Id" + Bridge->>Client: "mcp/message with connectionId" Client-->>Bridge: tool result - Bridge-->>Shim: relay - Shim-->>Agent: MCP response (stdio) + Bridge-->>Agent: MCP response + + Agent->>Bridge: "HTTP DELETE with MCP-Session-Id" + Bridge->>Client: "mcp/disconnect with connectionId" + Client-->>Bridge: empty result + Bridge-->>Agent: session closed ``` -A first implementation of this bridging exists in the `sacp-conductor` crate, part of the proposed new version of the [ACP Rust SDK](https://github.com/anthropics/rust-sdk). +The [ACP Rust SDK](https://github.com/agentclientprotocol/rust-sdk) provides native MCP server attachment independently of proxy chains. Its `agent-client-protocol-polyfill` crate provides the explicit HTTP adapter described above; stdio adaptation remains a possible alternative, not a prerequisite for this proposal. ## Frequently asked questions > What questions have arisen over the course of authoring this document or during subsequent discussions? -### Why use a separate `id` instead of server names? +### Why use a separate `serverId` instead of server names? + +Server names in `mcpServers` are chosen by whoever adds them to the session, and could potentially collide if multiple components add servers. A provider-generated `serverId` lets each component choose a unique routing identifier independently of its display name. -Server names in `mcpServers` are chosen by whoever adds them to the session, and could potentially collide if multiple components add servers. A component-generated `id` provides guaranteed uniqueness and allows the providing component to correlate incoming messages back to the correct session context. +This also avoids a potential deadlock: some agents don't return the session ID until after MCP servers have been initialized. Using a provider-generated `serverId` avoids any dependency on agent-provided session identifiers. -This also avoids a potential deadlock: some agents don't return the session ID until after MCP servers have been initialized. Using a component-generated `id` avoids any dependency on agent-provided identifiers. +The same field name is used in the declaration and `mcp/connect`. Earlier versions of this draft used `id` and `acpId`, respectively; `serverId` matches the shared schema and distinguishes the server from both an active `connectionId` and the outer JSON-RPC request `id`. Those earlier names are not aliases in the current wire schema. ### How does this relate to proxy chains? @@ -301,6 +383,104 @@ See the [Bridging for agents without native support](#bridging-for-agents-withou MCP-over-ACP has the same trust model as regular MCP: you're allowing a component to handle tool invocations. The difference is transport, not trust. Components should only add MCP servers from sources they trust, same as with stdio or HTTP transport. +## Modernization audit: MCP 2026-07-28 + +### Target and non-goals + +The official MCP site identifies [2026-07-28](https://modelcontextprotocol.io/specification/2026-07-28) as the latest published specification, not a future draft. This proposal targets that revision only. Before stabilization, recheck the published revision and pin the chosen target explicitly rather than promising compatibility with an unbounded moving "latest." + +There is no requirement to support legacy initialization, stateful HTTP, the deprecated HTTP+SSE transport, or fallback to an older MCP revision. MCP version selection is separate from ACP version selection: the existing v1/v2 ACP capability shapes do not require supporting two eras of MCP. + +Stateless does not mean stateless tooling or an absence of open streams. The [base protocol](https://modelcontextprotocol.io/specification/2026-07-28/basic) forbids implicit request context inherited from a connection. Explicit tool arguments, opaque application handles, MRTR retry state, and state scoped to one long-lived request remain possible. + +### Recommended transport design + +Retain the provider-generated `serverId` in the declaration and route ordinary MCP requests directly to that server. Remove `mcp/connect`, `connectionId`, and `mcp/disconnect` from the proposed stabilized wire protocol unless a separate, demonstrated ACP routing need justifies them. A provider being reachable on an ACP connection is not an MCP session. + +The replacement transport needs: + +1. One independently valid MCP request, including its metadata, addressed to a declared `serverId`. +2. Request-scoped server notifications, followed by one final MCP result or error. +3. Explicit cancellation of one request or subscription without shutting down the server or other work. +4. Routing/cleanup scoped to the containing ACP connection and the lifetime of the server declaration, without carrying implicit MCP capabilities, identity, or authorization between requests. + +The exact ACP request/notification schema and request-correlation mechanism remain design work. Reusing an ACP request ID as an MCP ID is not automatically safe: SDK relays can renumber outer JSON-RPC IDs, while MCP cancellation and subscription metadata refer to those IDs inside payloads. Either preserve an explicit logical MCP request identity or specify a complete mapping through every hop. Do not repurpose a connection ID as an unbounded session merely to correlate notifications. + +This is a recommendation for the next wire-schema revision, not a claim that the current schema or SDK already implements it. + +### Protocol changes to incorporate + +1. **Initialization and discovery.** No `initialize` / `notifications/initialized`. Servers implement `server/discover`; clients may send ordinary requests without first discovering. Remove connection-opening handshakes and examples. Discovery is a tunneled MCP request, not a replacement ACP setup method. +2. **Per-request context.** Requests carry `io.modelcontextprotocol/protocolVersion` and `io.modelcontextprotocol/clientCapabilities` in their inner `params._meta`. Client identity is recommended, not authentication. Preserve these fields on every request and retry; never cache their meaning in a connection object or substitute outer ACP `_meta`. Return the modern unsupported-version error rather than falling back to legacy MCP. +3. **Results and input.** Results carry `resultType`. MRTR returns `input_required` and retries the original operation with `inputResponses` and any opaque `requestState`, using a fresh request ID. Replace examples of server-initiated RPC callbacks. Support MRTR for `tools/call`, `resources/read`, and `prompts/get`, not arbitrary methods. Preserve ordinary `complete` results and MCP error code/message/data. +4. **Subscriptions.** `subscriptions/listen` is a long-lived request. Its first message is `notifications/subscriptions/acknowledged`; notifications are filtered and tagged with `io.modelcontextprotocol/subscriptionId`. Define acknowledgement ordering, concurrent subscriptions, notification correlation, cancellation, and graceful completion on ACP. Do not implement a general unscoped server event channel. +5. **Request notifications.** Progress and any supported logging notifications belong to their originating request, not a subscription stream. Route them to the correct in-flight operation, stop them on completion/cancellation, and preserve progress tokens. Logging is deprecated and should not be a new dependency of the design. +6. **Cancellation and failure.** HTTP response-stream closure cancels that request. Stdio uses `notifications/cancelled`; broken HTTP streams are not resumable. Specify ACP cancellation rather than treating an entire ACP connection as the request stream. Settle pending work, suppress late messages, and use new request IDs for deliberate retries; do not silently replay side-effecting tool calls. +7. **Tool/resource/prompt catalogs.** Listings must not vary merely because a client uses a different connection. Cacheable results require `ttlMs` and `cacheScope`; deterministic tool order is recommended. Use explicit server identity and authorization scope for distinct offerings. Review registry filtering, list-change subscriptions, cache isolation, and cache-result constructors. Do not add per-connection tool catalogs. +8. **Tool schemas and output.** JSON Schema 2020-12 keywords and arbitrary JSON `structuredContent` are supported, with schema-reference and composition bounds. Preserve schema/output information in typed tool APIs. Opaque envelope forwarding alone is not sufficient evidence that the typed server helpers conform. +9. **Optional extensions.** Tasks are an opt-in `io.modelcontextprotocol/tasks` extension, not the old core task protocol. Preserve extension capability maps and payloads. Do not make a tasks implementation, MCP Apps, or other optional feature a prerequisite for this transport. +10. **Removed/deprecated features.** Removed features include `ping`, `logging/setLevel`, old resource subscription methods, and old completion notifications. Roots, Sampling, and Logging are deprecated. Do not build new transport APIs around these features. The modern elicitation/MRTR flow is the relevant interactive-tool example. + +Normative sources: [versioning and discovery](https://modelcontextprotocol.io/specification/2026-07-28/basic/lifecycle), [MRTR](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr), [subscriptions](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/subscriptions), [cancellation](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation), [tools](https://modelcontextprotocol.io/specification/2026-07-28/server/tools), and the [revision changelog](https://modelcontextprotocol.io/specification/2026-07-28/changelog). + +### HTTP adaptation is a separate conformance surface + +A latest-only [Streamable HTTP](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http) adapter must replace, not extend, the stateful checkpoint: + +- Accept one JSON-RPC request or notification per POST. Do not forward JSON-RPC batches or client-sent responses as valid modern MCP traffic, even if the underlying ACP JSON-RPC library supports them. +- Return either a single JSON response or request-scoped SSE. Keep subscription streams separate from unrelated progress or tool-call responses. +- Return 405 for GET and DELETE. Do not mint or echo `Mcp-Session-Id`; ignore obsolete session and `Last-Event-ID` headers rather than implementing session/resumption behavior. +- Validate `MCP-Protocol-Version`, `Mcp-Method`, and applicable `Mcp-Name` headers against the body, including the specified Base64 sentinel encoding. Missing/malformed/mismatched required headers produce HTTP 400 with `HeaderMismatch` (`-32020`). Unsupported protocol versions use `-32022`, and unimplemented methods use HTTP 404 with `-32601`. +- Account for recognized tool-parameter headers declared by `x-mcp-header`, their validation/encoding rules, and unknown-header forwarding rules at an HTTP intermediary. These are HTTP requirements, not fields that must be invented in native ACP envelopes. +- Validate `Origin`, bind local adapters to loopback, and define authentication/access control. A random local port is not an authorization boundary. +- Couple HTTP stream closure to cancellation of only the mapped ACP request. Test slow readers, bounded buffering, aborted requests, lost responses, and subscription shutdown. + +The existing polyfill's downstream `http` capability alone does not establish support for the new MCP revision. A latest-only adapter must document that its consuming agent needs a modern MCP HTTP client; it must not advertise compatibility with older agents merely because they advertise HTTP transport support. + +### SDK and schema work + +The reviewed Rust SDK requests `rmcp = "2.1.0"` and its lockfile resolves `rmcp 2.2.0`. That dependency recognizes a `2026-07-28` version constant, but its `ProtocolVersion::LATEST` is still `2025-11-25`; its service lifecycle is initialization-based, and the inspected model does not provide `server/discover`, `subscriptions/listen`, or `InputRequiredResult`. Selecting the newer version string is not a conformance implementation. + +Before choosing public APIs, establish a modern-capable MCP dependency or a deliberately scoped stateless implementation. Keep MCP-specific types out of the core ACP transport where possible. A raw JSON/byte transport can still carry modern MCP; neither byte streams nor an existing ACP connection inherently violate statelessness. The problems are hidden session semantics, old typed models, and missing request-stream/correlation behavior. + +Implementation work spans: + +- Shared ACP schema: replace the unstable connection-oriented MCP envelopes and side/method mappings; specify required/optional/null behavior, cancellation, and request-scoped notifications; regenerate schema and reference documentation. +- Core SDK: direct server routing, request-scoped provider/consumer APIs, transparent modern result/error/metadata handling, bounded notification routing, and cancellation/resource ownership. +- Tool helpers and `rmcp` integration: discovery, per-request capabilities, modern results and MRTR, cache metadata, and a request context that does not require an MCP connection ID. +- Conductor/proxies: route declared servers and logical requests without leaking identifiers or renumbering embedded correlation fields incorrectly. +- HTTP polyfill: replace the session engine with the modern POST/request-stream behavior above; do not retain a second legacy mode. +- Tests, examples, and documentation: replace initialization and reverse-RPC happy paths with modern discovery, direct tool calls, MRTR, subscriptions, and cancellation. + +### Security and resource lifetime + +The modern design needs more than the earlier statement that transport does not change trust: + +- Bind `serverId` ownership and visibility to the providing ACP component and authorized callers. Neither server IDs nor self-reported MCP `clientInfo` are credentials. +- Keep outer ACP metadata separate from inner MCP request metadata, and preserve tracing metadata without logging sensitive inputs or opaque retry state by default. +- Treat MRTR `requestState` as opaque in intermediaries and attacker-controlled at the server. Servers must integrity-protect it when it influences authorization or business logic, and address expiry, principal binding, replay, and single-use requirements where applicable. +- Define cancellation, backpressure, limits on outstanding requests/subscriptions, and provider/declaration removal. A request-scoped resource must not remain alive because the containing ACP transport is long-lived. +- Do not translate an input-required result into automatic user approval or an unbounded retry loop. The agent retains responsibility for capability checks, consent, and tool-execution policy. + +### What remains useful from the implementation checkpoint + +Provider-generated IDs, shared-schema naming, explicit capability propagation, error/metadata preservation, and the investigation into pending-request cleanup remain useful. Tests proving request isolation and cleanup should be recast around requests and subscriptions. + +The stateful HTTP engine, connect/disconnect wire lifecycle, per-MCP-connection context, and native consumer API that exposes that lifecycle are **not** compatibility commitments. They may be removed or replaced. The checkpoint is not ready for release: its aborted HTTP initialization can leave a session alive, and pending-work teardown coverage is incomplete. + +### Delivery order and acceptance criteria + +1. **Settle the transport contract:** direct server routing, logical MCP request identity, notification correlation, cancellation, and declaration lifetime. Decide the corresponding unstable schema changes before adding another consumer API. +2. **Prove the modern dependency path:** a real `server/discover` and `tools/call` with per-request metadata, no preceding handshake, and modern result shapes. Do not claim conformance based only on synthetic JSON echoes. +3. **Implement the native transport:** request-scoped tools, MRTR, subscriptions, errors, and cancellation; then add an example with a direct ACP client/agent pair. +4. **Implement optional HTTP adaptation:** full modern header, security, POST/SSE, and request-close behavior. It need not block a native-only first implementation. +5. **Run a conformance matrix:** two independent callers with overlapping local IDs; different per-request capabilities without inherited state; discovery without setup; exact MRTR state round-trips and new retry IDs; concurrent filtered subscriptions and acknowledgement ordering; request-specific progress; cancellation during a pending tool call and subscription; provider loss; late-message rejection; opaque metadata/errors/results; catalog/cache isolation; HTTP malformed headers, forbidden origins, unsupported methods, batch rejection, and broken streams. + +The detailed cancellation and subscription pages contain wording that needs care when specifying server-initiated subscription termination (successful completion versus a cancellation notification). Resolve that mapping explicitly in this transport rather than copying an example of arbitrary reverse requests. The pinned schema and detailed normative rules should take precedence over overview prose that still mentions initialization. + ## Revision history -Split from proxy-chains RFD to enable independent use of MCP-over-ACP transport by any ACP component, not just proxies. +- Split from proxy-chains RFD to enable independent use of MCP-over-ACP transport by any ACP component, not just proxies. +- Aligned declaration and connect identifiers with the shared schema's `serverId`, corrected session setup and v1/v2 capability examples, and documented the explicit HTTP polyfill architecture. +- Clarified readiness during session setup, independent connection lifetimes, disconnect acknowledgement, and the distinction between a shared HTTP listener and independent MCP sessions. +- Set the stabilization target to MCP 2026-07-28 only and added a modernization audit. Retained the earlier connection-oriented design as a draft implementation checkpoint, not a backwards-compatibility requirement. diff --git a/docs/rfds/proxy-chains.mdx b/docs/rfds/proxy-chains.mdx index eb89a05c1..ea53b469e 100644 --- a/docs/rfds/proxy-chains.mdx +++ b/docs/rfds/proxy-chains.mdx @@ -253,9 +253,11 @@ Note: A conductor can be configured to run in either terminal mode (expecting `i ### MCP-over-ACP support -Proxies that provide MCP servers use the [MCP-over-ACP transport](./mcp-over-acp) mechanism. The conductor always advertises `mcpCapabilities.acp: true` to proxies and handles bridging for agents that don't support native ACP transport. +The [MCP-over-ACP modernization audit](./mcp-over-acp#modernization-audit-mcp-2026-07-28) targets stateless MCP 2026-07-28 only. The connection-oriented implementation described here is a draft checkpoint; its connect/disconnect lifecycle is not a compatibility requirement for the stabilized transport. -All proxies MUST respond to `proxy/initialize` with the MCP-over-ACP capability enabled. When the conductor sends `proxy/initialize`, proxies should be prepared to handle `mcp/connect`, `mcp/message`, and `mcp/disconnect` messages for any MCP servers they provide. +Proxies that provide MCP servers use the [MCP-over-ACP transport](./mcp-over-acp) mechanism. Capability advertising reflects what the downstream chain can consume; the conductor does not unconditionally add MCP-over-ACP support. In the Rust SDK, an explicit `McpOverAcpPolyfill` proxy can be placed immediately before an HTTP-capable agent that lacks native ACP MCP support. + +A forwarding proxy preserves downstream MCP capabilities. A bridging proxy may advertise ACP MCP support only when it can adapt to a transport its successor supports. Proxies that publish MCP servers should be prepared to handle `mcp/connect`, `mcp/message`, and `mcp/disconnect` for those servers as soon as their declarations are forwarded, including while session setup is still in progress. See the transport RFD for the v1 and draft-v2 capability shapes. ### Message reference @@ -409,9 +411,9 @@ The key advantage is that proxy-based extensions work with any ACP-compatible ag Proxies can provide MCP servers via [MCP-over-ACP transport](./mcp-over-acp), enabling a single proxy to add context, provide tools, and handle callbacks with full awareness of the conversation state. -The conductor always advertises `mcpCapabilities.acp: true` to proxies, regardless of whether the downstream agent supports it natively. When the agent doesn't support ACP transport, the conductor handles bridging transparently - spawning stdio shims or HTTP servers that the agent connects to normally, then relaying messages to/from the proxy's ACP channel. +When the agent supports native ACP MCP transport, no adapter is needed. Otherwise, the chain can include an explicit adapter to a transport the agent does support. The Rust SDK's HTTP polyfill rewrites MCP declarations to local HTTP endpoints and relays messages to and from the providing proxy's ACP channel. Each logical MCP session retains its own native connection, even when sessions share a listening endpoint. -This means proxy authors don't need to worry about agent compatibility - they implement MCP-over-ACP, and the conductor handles the rest. +Tool-providing proxies implement MCP-over-ACP without managing those alternative transports themselves. The chain's owner chooses an appropriate adapter, and the resulting advertised capability tells providers whether ACP MCP servers can be consumed. ```mermaid sequenceDiagram From fc84e615309ad0e69eeec67f3e576f2f60424bfc Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 24 Sep 2026 13:13:52 +0200 Subject: [PATCH 02/10] docs(rfd): use rmcp 3 upgrade as modernization prerequisite --- docs/rfds/mcp-over-acp.mdx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/rfds/mcp-over-acp.mdx b/docs/rfds/mcp-over-acp.mdx index bfe0ffd1c..aa8df9075 100644 --- a/docs/rfds/mcp-over-acp.mdx +++ b/docs/rfds/mcp-over-acp.mdx @@ -439,9 +439,11 @@ The existing polyfill's downstream `http` capability alone does not establish su ### SDK and schema work -The reviewed Rust SDK requests `rmcp = "2.1.0"` and its lockfile resolves `rmcp 2.2.0`. That dependency recognizes a `2026-07-28` version constant, but its `ProtocolVersion::LATEST` is still `2025-11-25`; its service lifecycle is initialization-based, and the inspected model does not provide `server/discover`, `subscriptions/listen`, or `InputRequiredResult`. Selecting the newer version string is not a conformance implementation. +The initial audit examined the Rust SDK's pinned `rmcp 2.2.0`, which lacks the modern service model. The available upgrade is tracked in [Rust SDK PR #372](https://github.com/agentclientprotocol/rust-sdk/pull/372): `rmcp 3.4.0` provides discovery, per-request metadata, MRTR, and subscription APIs. Land that dependency migration before building the replacement ACP transport, rather than introducing a temporary MCP implementation. -Before choosing public APIs, establish a modern-capable MCP dependency or a deliberately scoped stateless implementation. Keep MCP-specific types out of the core ACP transport where possible. A raw JSON/byte transport can still carry modern MCP; neither byte streams nor an existing ACP connection inherently violate statelessness. The problems are hidden session semantics, old typed models, and missing request-stream/correlation behavior. +The upgrade includes adapter tests for real MCP 2026-07-28 tool calls without initialization, discovery, per-request version validation, required cache metadata, and MRTR elicitation with nonempty input responses and fresh metadata. This is evidence for the dependency path, not proof of complete MCP conformance or of a redesigned ACP binding. The public rmcp major-version change also requires the integration crate's next release to be 4.x; the core ACP SDK remains on 2.x. + +`rmcp 3.4.0` still defaults `ProtocolVersion::LATEST` to `2025-11-25`. The new transport's callers must select 2026-07-28 explicitly and carry its required metadata on every request. Keep MCP-specific types out of the core ACP transport where possible. A raw JSON/byte transport can still carry modern MCP; neither byte streams nor an existing ACP connection inherently violate statelessness. The remaining transport work is request routing, correlation, streaming, cancellation, and removal of implicit session semantics. Implementation work spans: @@ -470,8 +472,8 @@ The stateful HTTP engine, connect/disconnect wire lifecycle, per-MCP-connection ### Delivery order and acceptance criteria -1. **Settle the transport contract:** direct server routing, logical MCP request identity, notification correlation, cancellation, and declaration lifetime. Decide the corresponding unstable schema changes before adding another consumer API. -2. **Prove the modern dependency path:** a real `server/discover` and `tools/call` with per-request metadata, no preceding handshake, and modern result shapes. Do not claim conformance based only on synthetic JSON echoes. +1. **Land the modern dependency path:** [Rust SDK PR #372](https://github.com/agentclientprotocol/rust-sdk/pull/372) upgrades rmcp and proves real discovery, tool calls without initialization, per-request metadata, and MRTR through the adapter. Keep its tests as the baseline, not a claim of full transport conformance. +2. **Settle the transport contract:** direct server routing, logical MCP request identity, notification correlation, cancellation, and declaration lifetime. Decide the corresponding unstable schema changes before adding another consumer API. 3. **Implement the native transport:** request-scoped tools, MRTR, subscriptions, errors, and cancellation; then add an example with a direct ACP client/agent pair. 4. **Implement optional HTTP adaptation:** full modern header, security, POST/SSE, and request-close behavior. It need not block a native-only first implementation. 5. **Run a conformance matrix:** two independent callers with overlapping local IDs; different per-request capabilities without inherited state; discovery without setup; exact MRTR state round-trips and new retry IDs; concurrent filtered subscriptions and acknowledgement ordering; request-specific progress; cancellation during a pending tool call and subscription; provider loss; late-message rejection; opaque metadata/errors/results; catalog/cache isolation; HTTP malformed headers, forbidden origins, unsupported methods, batch rejection, and broken streams. From 1ae7f09519fa0ba43289365da42bd589468e135f Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 24 Sep 2026 13:42:06 +0200 Subject: [PATCH 03/10] feat(unstable): make MCP-over-ACP request-scoped --- .../src/serde_util.rs | 8 - agent-client-protocol-schema/src/v1/agent.rs | 52 +-- agent-client-protocol-schema/src/v1/client.rs | 124 ++----- agent-client-protocol-schema/src/v1/mcp.rs | 250 ++------------ agent-client-protocol-schema/src/v2/agent.rs | 55 ++- agent-client-protocol-schema/src/v2/client.rs | 124 ++----- agent-client-protocol-schema/src/v2/mcp.rs | 321 +++--------------- docs/protocol/v1/draft/schema.mdx | 222 ++---------- docs/protocol/v2/draft/schema.mdx | 298 +++------------- schema-generator/src/main.rs | 4 +- schema/v1/meta.unstable.json | 2 - schema/v1/schema.unstable.json | 245 ++++--------- schema/v2/meta.unstable.json | 2 - schema/v2/schema.unstable.json | 276 ++++----------- 14 files changed, 382 insertions(+), 1601 deletions(-) diff --git a/agent-client-protocol-schema/src/serde_util.rs b/agent-client-protocol-schema/src/serde_util.rs index c8cb74fd7..407a85607 100644 --- a/agent-client-protocol-schema/src/serde_util.rs +++ b/agent-client-protocol-schema/src/serde_util.rs @@ -135,10 +135,6 @@ mod default_on_null_tests { $check::(); $check::(); } - #[cfg(feature = "unstable_mcp_over_acp")] - { - $check::(); - } #[cfg(feature = "unstable_protocol_v2")] { @@ -163,10 +159,6 @@ mod default_on_null_tests { $check::(); $check::(); } - #[cfg(feature = "unstable_mcp_over_acp")] - { - $check::(); - } } }; } diff --git a/agent-client-protocol-schema/src/v1/agent.rs b/agent-client-protocol-schema/src/v1/agent.rs index ccc984b6e..a311b3d77 100644 --- a/agent-client-protocol-schema/src/v1/agent.rs +++ b/agent-client-protocol-schema/src/v1/agent.rs @@ -18,9 +18,7 @@ use super::{ }; #[cfg(feature = "unstable_mcp_over_acp")] -use super::mcp::{ - MCP_MESSAGE_METHOD_NAME, MessageMcpNotification, MessageMcpRequest, MessageMcpResponse, -}; +use super::mcp::{MCP_MESSAGE_METHOD_NAME, MessageMcpNotification}; #[cfg(feature = "unstable_nes")] use super::{ @@ -2796,8 +2794,7 @@ impl McpServerSse { /// Unique identifier for an MCP server using the ACP transport. /// /// The value is opaque and generated by the ACP component providing the MCP server. It is -/// used by `mcp/connect` to route connection requests back to the component that declared the -/// server. +/// used by `mcp/message` to route requests to the component that declared the server. #[cfg(feature = "unstable_mcp_over_acp")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)] @@ -2822,7 +2819,7 @@ impl McpServerAcpId { /// ACP transport configuration for MCP. /// /// The MCP server is provided by an ACP component and communicates over the ACP channel -/// using `mcp/connect`, `mcp/message`, and `mcp/disconnect`. +/// using `mcp/message`. #[serde_as] #[skip_serializing_none] #[cfg(feature = "unstable_mcp_over_acp")] @@ -4962,13 +4959,6 @@ pub enum ClientRequest { /// The agent must cancel any ongoing work and then free up any resources /// associated with the NES session. CloseNesRequest(CloseNesRequest), - /// **UNSTABLE** - /// - /// This capability is not part of the spec yet, and may be removed or changed at any point. - /// - /// Exchanges an MCP-over-ACP message. - #[cfg(feature = "unstable_mcp_over_acp")] - MessageMcpRequest(MessageMcpRequest), /// Handles extension method requests from the client. /// /// Extension methods provide a way to add custom functionality while maintaining @@ -5009,8 +4999,6 @@ impl ClientRequest { Self::SuggestNesRequest(_) => AGENT_METHOD_NAMES.nes_suggest, #[cfg(feature = "unstable_nes")] Self::CloseNesRequest(_) => AGENT_METHOD_NAMES.nes_close, - #[cfg(feature = "unstable_mcp_over_acp")] - Self::MessageMcpRequest(_) => AGENT_METHOD_NAMES.mcp_message, Self::ExtMethodRequest(ext_request) => &ext_request.method, } } @@ -5076,9 +5064,6 @@ pub enum AgentResponse { CloseNesResponse(#[serde(default)] CloseNesResponse), /// Successful result returned by an extension method outside the core ACP method set. ExtMethodResponse(ExtResponse), - /// Successful result returned by an MCP-over-ACP `mcp/message` request. - #[cfg(feature = "unstable_mcp_over_acp")] - MessageMcpResponse(MessageMcpResponse), } /// All possible notifications that a client can send to an agent. @@ -5420,21 +5405,36 @@ mod test_serialization { #[cfg(feature = "unstable_mcp_over_acp")] #[test] fn test_client_mcp_message_method_names() { + use serde_json::json; + assert_eq!(AGENT_METHOD_NAMES.mcp_message, "mcp/message"); + let notification = + MessageMcpNotification::new("server-1", "req-1", "notifications/progress"); assert_eq!( - ClientRequest::MessageMcpRequest(MessageMcpRequest::new("conn-1", "tools/list")) - .method(), + ClientNotification::MessageMcpNotification(notification.clone()).method(), "mcp/message" ); assert_eq!( - ClientNotification::MessageMcpNotification(MessageMcpNotification::new( - "conn-1", - "notifications/progress" - )) - .method(), - "mcp/message" + serde_json::to_value(notification).unwrap(), + json!({ + "serverId": "server-1", + "requestId": "req-1", + "method": "notifications/progress" + }) ); + let notification: MessageMcpNotification = serde_json::from_value(json!({ + "serverId": "server-1", "requestId": "req-1", "method": "notifications/progress", + "params": null, "_meta": null + })) + .unwrap(); + assert_eq!(notification.params, None); + assert_eq!(notification.meta, None); + for key in ["serverId", "requestId", "method"] { + let mut value = json!({"serverId":"server-1", "requestId":"req-1", "method":"notifications/progress"}); + value.as_object_mut().unwrap().remove(key); + assert!(serde_json::from_value::(value).is_err()); + } } #[cfg(all(feature = "unstable_mcp_over_acp", feature = "schemars"))] diff --git a/agent-client-protocol-schema/src/v1/client.rs b/agent-client-protocol-schema/src/v1/client.rs index d06808b30..7b116bb9d 100644 --- a/agent-client-protocol-schema/src/v1/client.rs +++ b/agent-client-protocol-schema/src/v1/client.rs @@ -23,11 +23,7 @@ use super::{ use super::{PlanCapabilities, PlanRemoved, PlanUpdate}; #[cfg(feature = "unstable_mcp_over_acp")] -use super::mcp::{ - ConnectMcpRequest, ConnectMcpResponse, DisconnectMcpRequest, DisconnectMcpResponse, - MCP_CONNECT_METHOD_NAME, MCP_DISCONNECT_METHOD_NAME, MCP_MESSAGE_METHOD_NAME, - MessageMcpNotification, MessageMcpRequest, MessageMcpResponse, -}; +use super::mcp::{MCP_MESSAGE_METHOD_NAME, MessageMcpRequest, MessageMcpResponse}; #[cfg(feature = "unstable_nes")] use super::{ClientNesCapabilities, PositionEncodingKind}; @@ -2649,15 +2645,9 @@ pub struct ClientMethodNames { pub terminal_wait_for_exit: &'static str, /// Method for killing a terminal. pub terminal_kill: &'static str, - /// Method for opening an MCP-over-ACP connection. - #[cfg(feature = "unstable_mcp_over_acp")] - pub mcp_connect: &'static str, /// Method for exchanging MCP-over-ACP messages. #[cfg(feature = "unstable_mcp_over_acp")] pub mcp_message: &'static str, - /// Method for closing an MCP-over-ACP connection. - #[cfg(feature = "unstable_mcp_over_acp")] - pub mcp_disconnect: &'static str, /// Method for elicitation. pub elicitation_create: &'static str, /// Notification for elicitation completion. @@ -2676,11 +2666,7 @@ pub const CLIENT_METHOD_NAMES: ClientMethodNames = ClientMethodNames { terminal_wait_for_exit: TERMINAL_WAIT_FOR_EXIT_METHOD_NAME, terminal_kill: TERMINAL_KILL_METHOD_NAME, #[cfg(feature = "unstable_mcp_over_acp")] - mcp_connect: MCP_CONNECT_METHOD_NAME, - #[cfg(feature = "unstable_mcp_over_acp")] mcp_message: MCP_MESSAGE_METHOD_NAME, - #[cfg(feature = "unstable_mcp_over_acp")] - mcp_disconnect: MCP_DISCONNECT_METHOD_NAME, elicitation_create: ELICITATION_CREATE_METHOD_NAME, elicitation_complete: ELICITATION_COMPLETE_NOTIFICATION, }; @@ -2806,23 +2792,9 @@ pub enum AgentRequest { /// /// This capability is not part of the spec yet, and may be removed or changed at any point. /// - /// Opens an MCP-over-ACP connection. - #[cfg(feature = "unstable_mcp_over_acp")] - ConnectMcpRequest(ConnectMcpRequest), - /// **UNSTABLE** - /// - /// This capability is not part of the spec yet, and may be removed or changed at any point. - /// /// Exchanges an MCP-over-ACP message. #[cfg(feature = "unstable_mcp_over_acp")] MessageMcpRequest(MessageMcpRequest), - /// **UNSTABLE** - /// - /// This capability is not part of the spec yet, and may be removed or changed at any point. - /// - /// Closes an MCP-over-ACP connection. - #[cfg(feature = "unstable_mcp_over_acp")] - DisconnectMcpRequest(DisconnectMcpRequest), /// Handles extension method requests from the agent. /// /// Allows the Agent to send an arbitrary request that is not part of the ACP spec. @@ -2848,11 +2820,7 @@ impl AgentRequest { Self::KillTerminalRequest(_) => CLIENT_METHOD_NAMES.terminal_kill, Self::CreateElicitationRequest(_) => CLIENT_METHOD_NAMES.elicitation_create, #[cfg(feature = "unstable_mcp_over_acp")] - Self::ConnectMcpRequest(_) => CLIENT_METHOD_NAMES.mcp_connect, - #[cfg(feature = "unstable_mcp_over_acp")] Self::MessageMcpRequest(_) => CLIENT_METHOD_NAMES.mcp_message, - #[cfg(feature = "unstable_mcp_over_acp")] - Self::DisconnectMcpRequest(_) => CLIENT_METHOD_NAMES.mcp_disconnect, Self::ExtMethodRequest(ext_request) => &ext_request.method, } } @@ -2888,12 +2856,6 @@ pub enum ClientResponse { KillTerminalResponse(#[serde(default)] KillTerminalResponse), /// Successful result returned for a `elicitation/create` request. CreateElicitationResponse(CreateElicitationResponse), - /// Successful result returned for a `mcp/connect` request. - #[cfg(feature = "unstable_mcp_over_acp")] - ConnectMcpResponse(ConnectMcpResponse), - /// Successful result returned for a `mcp/disconnect` request. - #[cfg(feature = "unstable_mcp_over_acp")] - DisconnectMcpResponse(#[serde(default)] DisconnectMcpResponse), /// Successful result returned by an MCP-over-ACP `mcp/message` request. #[cfg(feature = "unstable_mcp_over_acp")] MessageMcpResponse(MessageMcpResponse), @@ -2930,13 +2892,6 @@ pub enum AgentNotification { /// /// See protocol docs: [Elicitation](https://agentclientprotocol.com/protocol/elicitation#url-completion) CompleteElicitationNotification(CompleteElicitationNotification), - /// **UNSTABLE** - /// - /// This capability is not part of the spec yet, and may be removed or changed at any point. - /// - /// Receives an MCP-over-ACP notification. - #[cfg(feature = "unstable_mcp_over_acp")] - MessageMcpNotification(MessageMcpNotification), /// Handles extension notifications from the agent. /// /// Allows the Agent to send an arbitrary notification that is not part of the ACP spec. @@ -2954,8 +2909,6 @@ impl AgentNotification { match self { Self::SessionNotification(_) => CLIENT_METHOD_NAMES.session_update, Self::CompleteElicitationNotification(_) => CLIENT_METHOD_NAMES.elicitation_complete, - #[cfg(feature = "unstable_mcp_over_acp")] - Self::MessageMcpNotification(_) => CLIENT_METHOD_NAMES.mcp_message, Self::ExtNotification(ext_notification) => &ext_notification.method, } } @@ -3613,72 +3566,51 @@ mod tests { let params: serde_json::Map = [("cursor".to_string(), json!("abc"))].into_iter().collect(); - assert_eq!(CLIENT_METHOD_NAMES.mcp_connect, "mcp/connect"); assert_eq!(CLIENT_METHOD_NAMES.mcp_message, "mcp/message"); - assert_eq!(CLIENT_METHOD_NAMES.mcp_disconnect, "mcp/disconnect"); - assert_eq!( - AgentRequest::ConnectMcpRequest(ConnectMcpRequest::new("server-1")).method(), - "mcp/connect" - ); - assert_eq!( - AgentRequest::MessageMcpRequest(MessageMcpRequest::new("conn-1", "tools/list")) - .method(), - "mcp/message" - ); - assert_eq!( - AgentRequest::DisconnectMcpRequest(DisconnectMcpRequest::new("conn-1")).method(), - "mcp/disconnect" - ); - assert_eq!( - AgentNotification::MessageMcpNotification(MessageMcpNotification::new( - "conn-1", - "notifications/progress" + AgentRequest::MessageMcpRequest(MessageMcpRequest::new( + "server-1", + "req-1", + "tools/list" )) .method(), "mcp/message" ); - assert_eq!( - serde_json::to_value(ConnectMcpRequest::new("server-1")).unwrap(), - json!({ "serverId": "server-1" }) - ); - assert_eq!( - serde_json::to_value(ConnectMcpResponse::new("conn-1")).unwrap(), - json!({ "connectionId": "conn-1" }) - ); - assert_eq!( - serde_json::to_value(MessageMcpRequest::new("conn-1", "tools/list").params(params)) - .unwrap(), + serde_json::to_value( + MessageMcpRequest::new("server-1", "req-1", "tools/list").params(params) + ) + .unwrap(), json!({ - "connectionId": "conn-1", + "serverId": "server-1", + "requestId": "req-1", "method": "tools/list", "params": { "cursor": "abc" } }) ); - assert_eq!( - serde_json::to_value(DisconnectMcpRequest::new("conn-1")).unwrap(), - json!({ "connectionId": "conn-1" }) - ); - assert_eq!( - serde_json::to_value(MessageMcpNotification::new( - "conn-1", - "notifications/progress" - )) - .unwrap(), - json!({ - "connectionId": "conn-1", - "method": "notifications/progress" - }) - ); let request_with_null_params: MessageMcpRequest = serde_json::from_value(json!({ - "connectionId": "conn-1", + "serverId": "server-1", + "requestId": "req-1", "method": "tools/list", - "params": null + "params": null, + "_meta": null })) .unwrap(); assert_eq!(request_with_null_params.params, None); + assert_eq!(request_with_null_params.meta, None); + for key in ["serverId", "requestId", "method"] { + let mut value = + json!({"serverId":"server-1", "requestId":"req-1", "method":"tools/list"}); + value.as_object_mut().unwrap().remove(key); + assert!(serde_json::from_value::(value).is_err()); + } + for key in ["serverId", "requestId", "method"] { + let mut value = + json!({"serverId":"server-1", "requestId":"req-1", "method":"tools/list"}); + value[key] = serde_json::Value::Null; + assert!(serde_json::from_value::(value).is_err()); + } } #[test] diff --git a/agent-client-protocol-schema/src/v1/mcp.rs b/agent-client-protocol-schema/src/v1/mcp.rs index 0430748aa..c4db25076 100644 --- a/agent-client-protocol-schema/src/v1/mcp.rs +++ b/agent-client-protocol-schema/src/v1/mcp.rs @@ -15,120 +15,25 @@ use super::{McpServerAcpId, Meta}; /// /// This capability is not part of the spec yet, and may be removed or changed at any point. /// -/// A unique identifier for an active MCP-over-ACP connection. +/// Identifies an inner MCP request active against a server on this ACP connection. +/// +/// Generated by the caller and preserved unchanged by proxies. This is distinct +/// from the outer ACP JSON-RPC request ID. #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)] #[serde(transparent)] #[from(Arc, String, &'static str)] #[non_exhaustive] -pub struct McpConnectionId(pub Arc); +pub struct McpRequestId(pub Arc); -impl McpConnectionId { - /// Wraps a protocol string as a typed [`McpConnectionId`]. +impl McpRequestId { + /// Wraps a protocol string as a typed [`McpRequestId`]. #[must_use] pub fn new(id: impl Into>) -> Self { Self(id.into()) } } -/// **UNSTABLE** -/// -/// This capability is not part of the spec yet, and may be removed or changed at any point. -/// -/// Request parameters for `mcp/connect`. -#[serde_as] -#[skip_serializing_none] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = MCP_CONNECT_METHOD_NAME)))] -#[non_exhaustive] -pub struct ConnectMcpRequest { - /// The ACP MCP server ID that was provided by the component declaring the MCP server. - pub server_id: McpServerAcpId, - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - #[serde_as(deserialize_as = "DefaultOnError")] - #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] - #[serde(default)] - #[serde(rename = "_meta")] - pub meta: Option, -} - -impl ConnectMcpRequest { - /// Builds [`ConnectMcpRequest`] with the required request fields set; optional fields start unset or empty. - #[must_use] - pub fn new(server_id: impl Into) -> Self { - Self { - server_id: server_id.into(), - meta: None, - } - } - - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - #[must_use] - pub fn meta(mut self, meta: impl IntoOption) -> Self { - self.meta = meta.into_option(); - self - } -} - -/// **UNSTABLE** -/// -/// This capability is not part of the spec yet, and may be removed or changed at any point. -/// -/// Response to `mcp/connect`. -#[serde_as] -#[skip_serializing_none] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = MCP_CONNECT_METHOD_NAME)))] -#[non_exhaustive] -pub struct ConnectMcpResponse { - /// The unique identifier for this MCP-over-ACP connection. - pub connection_id: McpConnectionId, - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - #[serde_as(deserialize_as = "DefaultOnError")] - #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] - #[serde(default)] - #[serde(rename = "_meta")] - pub meta: Option, -} - -impl ConnectMcpResponse { - /// Builds [`ConnectMcpResponse`] with the required response fields set; optional fields start unset or empty. - #[must_use] - pub fn new(connection_id: impl Into) -> Self { - Self { - connection_id: connection_id.into(), - meta: None, - } - } - - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - #[must_use] - pub fn meta(mut self, meta: impl IntoOption) -> Self { - self.meta = meta.into_option(); - self - } -} - /// **UNSTABLE** /// /// This capability is not part of the spec yet, and may be removed or changed at any point. @@ -139,11 +44,13 @@ impl ConnectMcpResponse { #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "both", "x-method" = MCP_MESSAGE_METHOD_NAME)))] +#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = MCP_MESSAGE_METHOD_NAME)))] #[non_exhaustive] pub struct MessageMcpRequest { - /// The MCP-over-ACP connection this message is sent on. - pub connection_id: McpConnectionId, + /// The declared ACP MCP server receiving this request. + pub server_id: McpServerAcpId, + /// The caller-generated identifier for the inner MCP request. + pub request_id: McpRequestId, /// The inner MCP method name. pub method: String, /// Optional inner MCP params. @@ -166,9 +73,14 @@ pub struct MessageMcpRequest { impl MessageMcpRequest { /// Builds [`MessageMcpRequest`] with the required request fields set; optional fields start unset or empty. #[must_use] - pub fn new(connection_id: impl Into, method: impl Into) -> Self { + pub fn new( + server_id: impl Into, + request_id: impl Into, + method: impl Into, + ) -> Self { Self { - connection_id: connection_id.into(), + server_id: server_id.into(), + request_id: request_id.into(), method: method.into(), params: None, meta: None, @@ -205,25 +117,25 @@ impl MessageMcpRequest { /// /// Notification parameters for `mcp/message`. /// -/// This is used when the wrapped MCP message is a notification and the outer JSON-RPC -/// envelope has no `id`. +/// Sent by the provider to the consumer for an active request (including +/// subscription acknowledgements and updates); the outer envelope has no `id`. #[serde_as] #[skip_serializing_none] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "both", "x-method" = MCP_MESSAGE_METHOD_NAME)))] +#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "agent", "x-method" = MCP_MESSAGE_METHOD_NAME)))] #[non_exhaustive] pub struct MessageMcpNotification { - /// The MCP-over-ACP connection this message is sent on. - pub connection_id: McpConnectionId, + /// The declared ACP MCP server handling the associated request. + pub server_id: McpServerAcpId, + /// The identifier of the active inner MCP request. + pub request_id: McpRequestId, /// The inner MCP method name. pub method: String, /// Optional inner MCP params. /// /// If omitted or set to `null`, the inner MCP message has no params. - #[serde_as(deserialize_as = "DefaultOnError")] - #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] #[serde(default)] pub params: Option>, /// The _meta property is reserved by ACP to allow clients and agents to attach additional @@ -241,9 +153,14 @@ pub struct MessageMcpNotification { impl MessageMcpNotification { /// Builds [`MessageMcpNotification`] with the required notification fields set; optional fields start unset or empty. #[must_use] - pub fn new(connection_id: impl Into, method: impl Into) -> Self { + pub fn new( + server_id: impl Into, + request_id: impl Into, + method: impl Into, + ) -> Self { Self { - connection_id: connection_id.into(), + server_id: server_id.into(), + request_id: request_id.into(), method: method.into(), params: None, meta: None, @@ -284,7 +201,7 @@ impl MessageMcpNotification { #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Debug, Clone, Serialize, Deserialize, From)] #[serde(transparent)] -#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "both", "x-method" = MCP_MESSAGE_METHOD_NAME)))] +#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = MCP_MESSAGE_METHOD_NAME)))] #[non_exhaustive] pub struct MessageMcpResponse( #[cfg_attr(feature = "schemars", schemars(with = "serde_json::Value"))] pub Arc, @@ -298,104 +215,5 @@ impl MessageMcpResponse { } } -/// **UNSTABLE** -/// -/// This capability is not part of the spec yet, and may be removed or changed at any point. -/// -/// Request parameters for `mcp/disconnect`. -#[serde_as] -#[skip_serializing_none] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = MCP_DISCONNECT_METHOD_NAME)))] -#[non_exhaustive] -pub struct DisconnectMcpRequest { - /// The MCP-over-ACP connection to close. - pub connection_id: McpConnectionId, - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - #[serde_as(deserialize_as = "DefaultOnError")] - #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] - #[serde(default)] - #[serde(rename = "_meta")] - pub meta: Option, -} - -impl DisconnectMcpRequest { - /// Builds [`DisconnectMcpRequest`] with the required request fields set; optional fields start unset or empty. - #[must_use] - pub fn new(connection_id: impl Into) -> Self { - Self { - connection_id: connection_id.into(), - meta: None, - } - } - - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - #[must_use] - pub fn meta(mut self, meta: impl IntoOption) -> Self { - self.meta = meta.into_option(); - self - } -} - -crate::serde_util::default_on_null! { - /// **UNSTABLE** - /// - /// This capability is not part of the spec yet, and may be removed or changed at any point. - /// - /// Response to `mcp/disconnect`. - #[serde_as] - #[skip_serializing_none] - #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] - #[derive(Default, Debug, Clone, Serialize, PartialEq, Eq)] - #[serde(rename_all = "camelCase")] - #[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = MCP_DISCONNECT_METHOD_NAME)))] - #[non_exhaustive] - pub struct DisconnectMcpResponse { - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - #[serde_as(deserialize_as = "DefaultOnError")] - #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] - #[serde(default)] - #[serde(rename = "_meta")] - pub meta: Option, - } -} - -impl DisconnectMcpResponse { - /// Builds [`DisconnectMcpResponse`] with the required response fields set; optional fields start unset or empty. - #[must_use] - pub fn new() -> Self { - Self::default() - } - - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - #[must_use] - pub fn meta(mut self, meta: impl IntoOption) -> Self { - self.meta = meta.into_option(); - self - } -} - -/// Method name for opening an MCP-over-ACP connection. -pub(crate) const MCP_CONNECT_METHOD_NAME: &str = "mcp/connect"; /// Method name for exchanging MCP-over-ACP messages. pub(crate) const MCP_MESSAGE_METHOD_NAME: &str = "mcp/message"; -/// Method name for closing an MCP-over-ACP connection. -pub(crate) const MCP_DISCONNECT_METHOD_NAME: &str = "mcp/disconnect"; diff --git a/agent-client-protocol-schema/src/v2/agent.rs b/agent-client-protocol-schema/src/v2/agent.rs index 1e6b9688b..25a4534ae 100644 --- a/agent-client-protocol-schema/src/v2/agent.rs +++ b/agent-client-protocol-schema/src/v2/agent.rs @@ -21,9 +21,7 @@ use super::{ use crate::{IntoOption, ProtocolVersion, SkipListener}; #[cfg(feature = "unstable_mcp_over_acp")] -use super::mcp::{ - MCP_MESSAGE_METHOD_NAME, MessageMcpNotification, MessageMcpRequest, MessageMcpResponse, -}; +use super::mcp::{MCP_MESSAGE_METHOD_NAME, MessageMcpNotification}; #[cfg(feature = "unstable_nes")] use super::{ @@ -2929,8 +2927,7 @@ impl McpServerHttp { /// Unique identifier for an MCP server using the ACP transport. /// /// The value is opaque and generated by the ACP component providing the MCP server. It is -/// used by `mcp/connect` to route connection requests back to the component that declared the -/// server. +/// used by `mcp/message` to route requests to the component that declared the server. #[cfg(feature = "unstable_mcp_over_acp")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)] @@ -2955,7 +2952,7 @@ impl McpServerAcpId { /// ACP transport configuration for MCP. /// /// The MCP server is provided by an ACP component and communicates over the ACP channel -/// using `mcp/connect`, `mcp/message`, and `mcp/disconnect`. +/// using `mcp/message`. #[serde_as] #[skip_serializing_none] #[cfg(feature = "unstable_mcp_over_acp")] @@ -5240,13 +5237,6 @@ pub enum ClientRequest { /// The agent must cancel any ongoing work and then free up any resources /// associated with the NES session. CloseNesRequest(Box), - /// **UNSTABLE** - /// - /// This capability is not part of the spec yet, and may be removed or changed at any point. - /// - /// Exchanges an MCP-over-ACP message. - #[cfg(feature = "unstable_mcp_over_acp")] - MessageMcpRequest(Box), /// Handles extension method requests from the client. /// /// Extension methods provide a way to add custom functionality while maintaining @@ -5285,8 +5275,6 @@ impl ClientRequest { Self::SuggestNesRequest(_) => AGENT_METHOD_NAMES.nes_suggest, #[cfg(feature = "unstable_nes")] Self::CloseNesRequest(_) => AGENT_METHOD_NAMES.nes_close, - #[cfg(feature = "unstable_mcp_over_acp")] - Self::MessageMcpRequest(_) => AGENT_METHOD_NAMES.mcp_message, Self::ExtMethodRequest(ext_request) => &ext_request.method, } } @@ -5347,9 +5335,6 @@ pub enum AgentResponse { CloseNesResponse(#[serde(default)] Box), /// Successful result returned by an extension method outside the core ACP method set. ExtMethodResponse(Box), - /// Successful result returned by an MCP-over-ACP `mcp/message` request. - #[cfg(feature = "unstable_mcp_over_acp")] - MessageMcpResponse(Box), } /// All possible notifications that a client can send to an agent. @@ -5780,24 +5765,36 @@ mod test_serialization { #[cfg(feature = "unstable_mcp_over_acp")] #[test] fn test_client_mcp_message_method_names() { + use serde_json::json; + assert_eq!(AGENT_METHOD_NAMES.mcp_message, "mcp/message"); + let notification = + MessageMcpNotification::new("server-1", "req-1", "notifications/progress"); assert_eq!( - ClientRequest::MessageMcpRequest(Box::new(MessageMcpRequest::new( - "conn-1", - "tools/list" - ))) - .method(), + ClientNotification::MessageMcpNotification(Box::new(notification.clone())).method(), "mcp/message" ); assert_eq!( - ClientNotification::MessageMcpNotification(Box::new(MessageMcpNotification::new( - "conn-1", - "notifications/progress" - ))) - .method(), - "mcp/message" + serde_json::to_value(notification).unwrap(), + json!({ + "serverId": "server-1", + "requestId": "req-1", + "method": "notifications/progress" + }) ); + let notification: MessageMcpNotification = serde_json::from_value(json!({ + "serverId": "server-1", "requestId": "req-1", "method": "notifications/progress", + "params": null, "_meta": null + })) + .unwrap(); + assert_eq!(notification.params, None); + assert_eq!(notification.meta, None); + for key in ["serverId", "requestId", "method"] { + let mut value = json!({"serverId":"server-1", "requestId":"req-1", "method":"notifications/progress"}); + value.as_object_mut().unwrap().remove(key); + assert!(serde_json::from_value::(value).is_err()); + } } #[test] diff --git a/agent-client-protocol-schema/src/v2/client.rs b/agent-client-protocol-schema/src/v2/client.rs index b30006e1e..5943e62fb 100644 --- a/agent-client-protocol-schema/src/v2/client.rs +++ b/agent-client-protocol-schema/src/v2/client.rs @@ -27,11 +27,7 @@ use super::{ use crate::{IntoMaybeUndefined, IntoOption, MaybeUndefined, SkipListener}; #[cfg(feature = "unstable_mcp_over_acp")] -use super::mcp::{ - ConnectMcpRequest, ConnectMcpResponse, DisconnectMcpRequest, DisconnectMcpResponse, - MCP_CONNECT_METHOD_NAME, MCP_DISCONNECT_METHOD_NAME, MCP_MESSAGE_METHOD_NAME, - MessageMcpNotification, MessageMcpRequest, MessageMcpResponse, -}; +use super::mcp::{MCP_MESSAGE_METHOD_NAME, MessageMcpRequest, MessageMcpResponse}; #[cfg(feature = "unstable_nes")] use super::{ClientNesCapabilities, PositionEncodingKind}; @@ -2424,15 +2420,9 @@ pub struct ClientMethodNames { pub session_request_permission: &'static str, /// Notification for session updates. pub session_update: &'static str, - /// Method for opening an MCP-over-ACP connection. - #[cfg(feature = "unstable_mcp_over_acp")] - pub mcp_connect: &'static str, /// Method for exchanging MCP-over-ACP messages. #[cfg(feature = "unstable_mcp_over_acp")] pub mcp_message: &'static str, - /// Method for closing an MCP-over-ACP connection. - #[cfg(feature = "unstable_mcp_over_acp")] - pub mcp_disconnect: &'static str, /// Method for elicitation. pub elicitation_create: &'static str, /// Notification for elicitation completion. @@ -2444,11 +2434,7 @@ pub const CLIENT_METHOD_NAMES: ClientMethodNames = ClientMethodNames { session_update: SESSION_UPDATE_NOTIFICATION, session_request_permission: SESSION_REQUEST_PERMISSION_METHOD_NAME, #[cfg(feature = "unstable_mcp_over_acp")] - mcp_connect: MCP_CONNECT_METHOD_NAME, - #[cfg(feature = "unstable_mcp_over_acp")] mcp_message: MCP_MESSAGE_METHOD_NAME, - #[cfg(feature = "unstable_mcp_over_acp")] - mcp_disconnect: MCP_DISCONNECT_METHOD_NAME, elicitation_create: ELICITATION_CREATE_METHOD_NAME, elicitation_complete: ELICITATION_COMPLETE_NOTIFICATION, }; @@ -2493,23 +2479,9 @@ pub enum AgentRequest { /// /// This capability is not part of the spec yet, and may be removed or changed at any point. /// - /// Opens an MCP-over-ACP connection. - #[cfg(feature = "unstable_mcp_over_acp")] - ConnectMcpRequest(Box), - /// **UNSTABLE** - /// - /// This capability is not part of the spec yet, and may be removed or changed at any point. - /// /// Exchanges an MCP-over-ACP message. #[cfg(feature = "unstable_mcp_over_acp")] MessageMcpRequest(Box), - /// **UNSTABLE** - /// - /// This capability is not part of the spec yet, and may be removed or changed at any point. - /// - /// Closes an MCP-over-ACP connection. - #[cfg(feature = "unstable_mcp_over_acp")] - DisconnectMcpRequest(Box), /// Handles extension method requests from the agent. /// /// Allows the Agent to send an arbitrary request that is not part of the ACP spec. @@ -2528,11 +2500,7 @@ impl AgentRequest { Self::RequestPermissionRequest(_) => CLIENT_METHOD_NAMES.session_request_permission, Self::CreateElicitationRequest(_) => CLIENT_METHOD_NAMES.elicitation_create, #[cfg(feature = "unstable_mcp_over_acp")] - Self::ConnectMcpRequest(_) => CLIENT_METHOD_NAMES.mcp_connect, - #[cfg(feature = "unstable_mcp_over_acp")] Self::MessageMcpRequest(_) => CLIENT_METHOD_NAMES.mcp_message, - #[cfg(feature = "unstable_mcp_over_acp")] - Self::DisconnectMcpRequest(_) => CLIENT_METHOD_NAMES.mcp_disconnect, Self::ExtMethodRequest(ext_request) => &ext_request.method, } } @@ -2554,12 +2522,6 @@ pub enum ClientResponse { RequestPermissionResponse(Box), /// Successful result returned for a `elicitation/create` request. CreateElicitationResponse(Box), - /// Successful result returned for a `mcp/connect` request. - #[cfg(feature = "unstable_mcp_over_acp")] - ConnectMcpResponse(Box), - /// Successful result returned for a `mcp/disconnect` request. - #[cfg(feature = "unstable_mcp_over_acp")] - DisconnectMcpResponse(#[serde(default)] Box), /// Successful result returned by an MCP-over-ACP `mcp/message` request. #[cfg(feature = "unstable_mcp_over_acp")] MessageMcpResponse(Box), @@ -2596,13 +2558,6 @@ pub enum AgentNotification { /// /// See protocol docs: [Elicitation](https://agentclientprotocol.com/protocol/elicitation#url-completion) CompleteElicitationNotification(Box), - /// **UNSTABLE** - /// - /// This capability is not part of the spec yet, and may be removed or changed at any point. - /// - /// Receives an MCP-over-ACP notification. - #[cfg(feature = "unstable_mcp_over_acp")] - MessageMcpNotification(Box), /// Handles extension notifications from the agent. /// /// Allows the Agent to send an arbitrary notification that is not part of the ACP spec. @@ -2620,8 +2575,6 @@ impl AgentNotification { match self { Self::UpdateSessionNotification(_) => CLIENT_METHOD_NAMES.session_update, Self::CompleteElicitationNotification(_) => CLIENT_METHOD_NAMES.elicitation_complete, - #[cfg(feature = "unstable_mcp_over_acp")] - Self::MessageMcpNotification(_) => CLIENT_METHOD_NAMES.mcp_message, Self::ExtNotification(ext_notification) => &ext_notification.method, } } @@ -3822,76 +3775,51 @@ mod tests { let params: serde_json::Map = [("cursor".to_string(), json!("abc"))].into_iter().collect(); - assert_eq!(CLIENT_METHOD_NAMES.mcp_connect, "mcp/connect"); assert_eq!(CLIENT_METHOD_NAMES.mcp_message, "mcp/message"); - assert_eq!(CLIENT_METHOD_NAMES.mcp_disconnect, "mcp/disconnect"); - - assert_eq!( - AgentRequest::ConnectMcpRequest(Box::new(ConnectMcpRequest::new("server-1"))).method(), - "mcp/connect" - ); assert_eq!( AgentRequest::MessageMcpRequest(Box::new(MessageMcpRequest::new( - "conn-1", + "server-1", + "req-1", "tools/list" ))) .method(), "mcp/message" ); assert_eq!( - AgentRequest::DisconnectMcpRequest(Box::new(DisconnectMcpRequest::new("conn-1"))) - .method(), - "mcp/disconnect" - ); - assert_eq!( - AgentNotification::MessageMcpNotification(Box::new(MessageMcpNotification::new( - "conn-1", - "notifications/progress" - ))) - .method(), - "mcp/message" - ); - - assert_eq!( - serde_json::to_value(ConnectMcpRequest::new("server-1")).unwrap(), - json!({ "serverId": "server-1" }) - ); - assert_eq!( - serde_json::to_value(ConnectMcpResponse::new("conn-1")).unwrap(), - json!({ "connectionId": "conn-1" }) - ); - assert_eq!( - serde_json::to_value(MessageMcpRequest::new("conn-1", "tools/list").params(params)) - .unwrap(), + serde_json::to_value( + MessageMcpRequest::new("server-1", "req-1", "tools/list").params(params) + ) + .unwrap(), json!({ - "connectionId": "conn-1", + "serverId": "server-1", + "requestId": "req-1", "method": "tools/list", "params": { "cursor": "abc" } }) ); - assert_eq!( - serde_json::to_value(DisconnectMcpRequest::new("conn-1")).unwrap(), - json!({ "connectionId": "conn-1" }) - ); - assert_eq!( - serde_json::to_value(MessageMcpNotification::new( - "conn-1", - "notifications/progress" - )) - .unwrap(), - json!({ - "connectionId": "conn-1", - "method": "notifications/progress" - }) - ); let request_with_null_params: MessageMcpRequest = serde_json::from_value(json!({ - "connectionId": "conn-1", + "serverId": "server-1", + "requestId": "req-1", "method": "tools/list", - "params": null + "params": null, + "_meta": null })) .unwrap(); assert_eq!(request_with_null_params.params, None); + assert_eq!(request_with_null_params.meta, None); + for key in ["serverId", "requestId", "method"] { + let mut value = + json!({"serverId":"server-1", "requestId":"req-1", "method":"tools/list"}); + value.as_object_mut().unwrap().remove(key); + assert!(serde_json::from_value::(value).is_err()); + } + for key in ["serverId", "requestId", "method"] { + let mut value = + json!({"serverId":"server-1", "requestId":"req-1", "method":"tools/list"}); + value[key] = serde_json::Value::Null; + assert!(serde_json::from_value::(value).is_err()); + } } #[test] diff --git a/agent-client-protocol-schema/src/v2/mcp.rs b/agent-client-protocol-schema/src/v2/mcp.rs index 7386af965..0a8f703cc 100644 --- a/agent-client-protocol-schema/src/v2/mcp.rs +++ b/agent-client-protocol-schema/src/v2/mcp.rs @@ -7,154 +7,51 @@ use serde::{Deserialize, Serialize}; use serde_json::value::RawValue; use serde_with::{DefaultOnError, serde_as, skip_serializing_none}; -use super::{McpServerAcpId, Meta}; use crate::IntoOption; +use super::{McpServerAcpId, Meta}; + /// **UNSTABLE** /// -/// This capability is not part of the spec yet, and may be removed or changed at any point. -/// -/// A unique identifier for an active MCP-over-ACP connection. +/// Identifies an inner MCP request active against a server on this ACP connection. +/// Generated by the caller and preserved unchanged by proxies, independently of +/// the outer ACP JSON-RPC request ID. #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)] #[serde(transparent)] -#[from(forward)] -#[non_exhaustive] -pub struct McpConnectionId(pub Arc); - -impl McpConnectionId { - /// Wraps a protocol string as a typed [`McpConnectionId`]. - #[must_use] - pub fn new(id: impl Into) -> Self { - id.into() - } -} - -/// **UNSTABLE** -/// -/// This capability is not part of the spec yet, and may be removed or changed at any point. -/// -/// Request parameters for `mcp/connect`. -#[serde_as] -#[skip_serializing_none] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = MCP_CONNECT_METHOD_NAME)))] -#[non_exhaustive] -pub struct ConnectMcpRequest { - /// The ACP MCP server ID that was provided by the component declaring the MCP server. - pub server_id: McpServerAcpId, - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - #[serde_as(deserialize_as = "DefaultOnError")] - #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] - #[serde(default)] - #[serde(rename = "_meta")] - pub meta: Option, -} - -impl ConnectMcpRequest { - /// Builds [`ConnectMcpRequest`] with the required request fields set; optional fields start unset or empty. - #[must_use] - pub fn new(server_id: impl Into) -> Self { - Self { - server_id: server_id.into(), - meta: None, - } - } - - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - #[must_use] - pub fn meta(mut self, meta: impl IntoOption) -> Self { - self.meta = meta.into_option(); - self - } -} - -/// **UNSTABLE** -/// -/// This capability is not part of the spec yet, and may be removed or changed at any point. -/// -/// Response to `mcp/connect`. -#[serde_as] -#[skip_serializing_none] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = MCP_CONNECT_METHOD_NAME)))] +#[from(Arc, String, &'static str)] #[non_exhaustive] -pub struct ConnectMcpResponse { - /// The unique identifier for this MCP-over-ACP connection. - pub connection_id: McpConnectionId, - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - #[serde_as(deserialize_as = "DefaultOnError")] - #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] - #[serde(default)] - #[serde(rename = "_meta")] - pub meta: Option, -} - -impl ConnectMcpResponse { - /// Builds [`ConnectMcpResponse`] with the required response fields set; optional fields start unset or empty. - #[must_use] - pub fn new(connection_id: impl Into) -> Self { - Self { - connection_id: connection_id.into(), - meta: None, - } - } +pub struct McpRequestId(pub Arc); - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) +impl McpRequestId { + /// Wraps a protocol string as a typed [`McpRequestId`]. #[must_use] - pub fn meta(mut self, meta: impl IntoOption) -> Self { - self.meta = meta.into_option(); - self + pub fn new(id: impl Into>) -> Self { + Self(id.into()) } } /// **UNSTABLE** /// -/// This capability is not part of the spec yet, and may be removed or changed at any point. -/// -/// Request parameters for `mcp/message`. +/// Request parameters for `mcp/message`, sent from consumer to provider. #[serde_as] #[skip_serializing_none] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "both", "x-method" = MCP_MESSAGE_METHOD_NAME)))] +#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = MCP_MESSAGE_METHOD_NAME)))] #[non_exhaustive] pub struct MessageMcpRequest { - /// The MCP-over-ACP connection this message is sent on. - pub connection_id: McpConnectionId, + /// The declared ACP MCP server receiving this request. + pub server_id: McpServerAcpId, + /// The caller-generated identifier for the inner MCP request. + pub request_id: McpRequestId, /// The inner MCP method name. pub method: String, - /// Optional inner MCP params. - /// - /// If omitted or set to `null`, the inner MCP message has no params. + /// Optional inner MCP params; null is equivalent to omission. #[serde(default)] pub params: Option>, - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + /// ACP extension metadata (not inner MCP params._meta); null is equivalent to omission. #[serde_as(deserialize_as = "DefaultOnError")] #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] #[serde(default)] @@ -163,20 +60,23 @@ pub struct MessageMcpRequest { } impl MessageMcpRequest { - /// Builds [`MessageMcpRequest`] with the required request fields set; optional fields start unset or empty. + /// Builds [`MessageMcpRequest`] with required fields set. #[must_use] - pub fn new(connection_id: impl Into, method: impl Into) -> Self { + pub fn new( + server_id: impl Into, + request_id: impl Into, + method: impl Into, + ) -> Self { Self { - connection_id: connection_id.into(), + server_id: server_id.into(), + request_id: request_id.into(), method: method.into(), params: None, meta: None, } } - /// Optional inner MCP params. - /// - /// If omitted or set to `null`, the inner MCP message has no params. + /// Sets optional inner MCP params. #[must_use] pub fn params( mut self, @@ -186,11 +86,7 @@ impl MessageMcpRequest { self } - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + /// Sets optional ACP extension metadata. #[must_use] pub fn meta(mut self, meta: impl IntoOption) -> Self { self.meta = meta.into_option(); @@ -200,36 +96,26 @@ impl MessageMcpRequest { /// **UNSTABLE** /// -/// This capability is not part of the spec yet, and may be removed or changed at any point. -/// -/// Notification parameters for `mcp/message`. -/// -/// This is used when the wrapped MCP message is a notification and the outer JSON-RPC -/// envelope has no `id`. +/// Notification for an active request, sent from provider to consumer. +/// Includes subscription acknowledgements and updates. #[serde_as] #[skip_serializing_none] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "both", "x-method" = MCP_MESSAGE_METHOD_NAME)))] +#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "agent", "x-method" = MCP_MESSAGE_METHOD_NAME)))] #[non_exhaustive] pub struct MessageMcpNotification { - /// The MCP-over-ACP connection this message is sent on. - pub connection_id: McpConnectionId, + /// The declared ACP MCP server handling the associated request. + pub server_id: McpServerAcpId, + /// The identifier of the active inner MCP request. + pub request_id: McpRequestId, /// The inner MCP method name. pub method: String, - /// Optional inner MCP params. - /// - /// If omitted or set to `null`, the inner MCP message has no params. - #[serde_as(deserialize_as = "DefaultOnError")] - #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] + /// Optional inner MCP params; null is equivalent to omission. #[serde(default)] pub params: Option>, - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + /// ACP extension metadata (not inner MCP params._meta); null is equivalent to omission. #[serde_as(deserialize_as = "DefaultOnError")] #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] #[serde(default)] @@ -238,20 +124,23 @@ pub struct MessageMcpNotification { } impl MessageMcpNotification { - /// Builds [`MessageMcpNotification`] with the required notification fields set; optional fields start unset or empty. + /// Builds [`MessageMcpNotification`] with required fields set. #[must_use] - pub fn new(connection_id: impl Into, method: impl Into) -> Self { + pub fn new( + server_id: impl Into, + request_id: impl Into, + method: impl Into, + ) -> Self { Self { - connection_id: connection_id.into(), + server_id: server_id.into(), + request_id: request_id.into(), method: method.into(), params: None, meta: None, } } - /// Optional inner MCP params. - /// - /// If omitted or set to `null`, the inner MCP message has no params. + /// Sets optional inner MCP params. #[must_use] pub fn params( mut self, @@ -261,11 +150,7 @@ impl MessageMcpNotification { self } - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + /// Sets optional ACP extension metadata. #[must_use] pub fn meta(mut self, meta: impl IntoOption) -> Self { self.meta = meta.into_option(); @@ -275,126 +160,24 @@ impl MessageMcpNotification { /// **UNSTABLE** /// -/// This capability is not part of the spec yet, and may be removed or changed at any point. -/// -/// Response to `mcp/message`. -/// -/// This is the inner MCP response result payload. Any JSON value is valid. +/// Response to `mcp/message`: transparent inner MCP result. MCP errors use +/// the outer ACP JSON-RPC error envelope. #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Debug, Clone, Serialize, Deserialize, From)] #[serde(transparent)] -#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "both", "x-method" = MCP_MESSAGE_METHOD_NAME)))] +#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = MCP_MESSAGE_METHOD_NAME)))] #[non_exhaustive] pub struct MessageMcpResponse( #[cfg_attr(feature = "schemars", schemars(with = "serde_json::Value"))] pub Arc, ); impl MessageMcpResponse { - /// Builds [`MessageMcpResponse`] with the required response fields set; optional fields start unset or empty. + /// Builds [`MessageMcpResponse`] with the result payload. #[must_use] pub fn new(result: Arc) -> Self { Self(result) } } -/// **UNSTABLE** -/// -/// This capability is not part of the spec yet, and may be removed or changed at any point. -/// -/// Request parameters for `mcp/disconnect`. -#[serde_as] -#[skip_serializing_none] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = MCP_DISCONNECT_METHOD_NAME)))] -#[non_exhaustive] -pub struct DisconnectMcpRequest { - /// The MCP-over-ACP connection to close. - pub connection_id: McpConnectionId, - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - #[serde_as(deserialize_as = "DefaultOnError")] - #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] - #[serde(default)] - #[serde(rename = "_meta")] - pub meta: Option, -} - -impl DisconnectMcpRequest { - /// Builds [`DisconnectMcpRequest`] with the required request fields set; optional fields start unset or empty. - #[must_use] - pub fn new(connection_id: impl Into) -> Self { - Self { - connection_id: connection_id.into(), - meta: None, - } - } - - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - #[must_use] - pub fn meta(mut self, meta: impl IntoOption) -> Self { - self.meta = meta.into_option(); - self - } -} - -crate::serde_util::default_on_null! { - /// **UNSTABLE** - /// - /// This capability is not part of the spec yet, and may be removed or changed at any point. - /// - /// Response to `mcp/disconnect`. - #[serde_as] - #[skip_serializing_none] - #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] - #[derive(Default, Debug, Clone, Serialize, PartialEq, Eq)] - #[serde(rename_all = "camelCase")] - #[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = MCP_DISCONNECT_METHOD_NAME)))] - #[non_exhaustive] - pub struct DisconnectMcpResponse { - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - #[serde_as(deserialize_as = "DefaultOnError")] - #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] - #[serde(default)] - #[serde(rename = "_meta")] - pub meta: Option, - } -} - -impl DisconnectMcpResponse { - /// Builds [`DisconnectMcpResponse`] with the required response fields set; optional fields start unset or empty. - #[must_use] - pub fn new() -> Self { - Self::default() - } - - /// The _meta property is reserved by ACP to allow clients and agents to attach additional - /// metadata to their interactions. Implementations MUST NOT make assumptions about values at - /// these keys. - /// - /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - #[must_use] - pub fn meta(mut self, meta: impl IntoOption) -> Self { - self.meta = meta.into_option(); - self - } -} - -/// Method name for opening an MCP-over-ACP connection. -pub(crate) const MCP_CONNECT_METHOD_NAME: &str = "mcp/connect"; /// Method name for exchanging MCP-over-ACP messages. pub(crate) const MCP_MESSAGE_METHOD_NAME: &str = "mcp/message"; -/// Method name for closing an MCP-over-ACP connection. -pub(crate) const MCP_DISCONNECT_METHOD_NAME: &str = "mcp/disconnect"; diff --git a/docs/protocol/v1/draft/schema.mdx b/docs/protocol/v1/draft/schema.mdx index 007b66cd7..d86600e76 100644 --- a/docs/protocol/v1/draft/schema.mdx +++ b/docs/protocol/v1/draft/schema.mdx @@ -388,7 +388,7 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v1/d This capability is not part of the spec yet, and may be removed or changed at any point. -Exchanges an MCP-over-ACP message. +Sends an MCP-over-ACP notification. #### MessageMcpNotification @@ -398,8 +398,8 @@ This capability is not part of the spec yet, and may be removed or changed at an Notification parameters for `mcp/message`. -This is used when the wrapped MCP message is a notification and the outer JSON-RPC -envelope has no `id`. +Sent by the provider to the consumer for an active request (including +subscription acknowledgements and updates); the outer envelope has no `id`. **Type:** Object @@ -412,9 +412,6 @@ these keys. See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v1/draft/extensibility) - -McpConnectionId} required> - The MCP-over-ACP connection this message is sent on. The inner MCP method name. @@ -425,50 +422,13 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v1/d If omitted or set to `null`, the inner MCP message has no params. - -#### MessageMcpRequest - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Request parameters for `mcp/message`. - -**Type:** Object - -**Properties:** - - - The _meta property is reserved by ACP to allow clients and agents to attach additional -metadata to their interactions. Implementations MUST NOT make assumptions about values at -these keys. - -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v1/draft/extensibility) - +McpRequestId} required> + The identifier of the active inner MCP request. -McpConnectionId} required> - The MCP-over-ACP connection this message is sent on. - - - The inner MCP method name. - - - Optional inner MCP params. - -If omitted or set to `null`, the inner MCP message has no params. - +McpServerAcpId} required> + The declared ACP MCP server handling the associated request. -#### MessageMcpResponse - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Response to `mcp/message`. - -This is the inner MCP response result payload. Any JSON value is valid. - ### nes/accept @@ -2095,117 +2055,6 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v1/d - -### mcp/connect - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Opens an MCP-over-ACP connection. - -#### ConnectMcpRequest - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Request parameters for `mcp/connect`. - -**Type:** Object - -**Properties:** - - - The _meta property is reserved by ACP to allow clients and agents to attach additional -metadata to their interactions. Implementations MUST NOT make assumptions about values at -these keys. - -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v1/draft/extensibility) - - -McpServerAcpId} required> - The ACP MCP server ID that was provided by the component declaring the MCP server. - - -#### ConnectMcpResponse - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Response to `mcp/connect`. - -**Type:** Object - -**Properties:** - - - The _meta property is reserved by ACP to allow clients and agents to attach additional -metadata to their interactions. Implementations MUST NOT make assumptions about values at -these keys. - -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v1/draft/extensibility) - - -McpConnectionId} required> - The unique identifier for this MCP-over-ACP connection. - - - -### mcp/disconnect - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Closes an MCP-over-ACP connection. - -#### DisconnectMcpRequest - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Request parameters for `mcp/disconnect`. - -**Type:** Object - -**Properties:** - - - The _meta property is reserved by ACP to allow clients and agents to attach additional -metadata to their interactions. Implementations MUST NOT make assumptions about values at -these keys. - -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v1/draft/extensibility) - - -McpConnectionId} required> - The MCP-over-ACP connection to close. - - -#### DisconnectMcpResponse - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Response to `mcp/disconnect`. - -**Type:** Object - -**Properties:** - - - The _meta property is reserved by ACP to allow clients and agents to attach additional -metadata to their interactions. Implementations MUST NOT make assumptions about values at -these keys. - -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v1/draft/extensibility) - - - ### mcp/message @@ -2215,42 +2064,6 @@ This capability is not part of the spec yet, and may be removed or changed at an Exchanges an MCP-over-ACP message. -#### MessageMcpNotification - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Notification parameters for `mcp/message`. - -This is used when the wrapped MCP message is a notification and the outer JSON-RPC -envelope has no `id`. - -**Type:** Object - -**Properties:** - - - The _meta property is reserved by ACP to allow clients and agents to attach additional -metadata to their interactions. Implementations MUST NOT make assumptions about values at -these keys. - -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v1/draft/extensibility) - - -McpConnectionId} required> - The MCP-over-ACP connection this message is sent on. - - - The inner MCP method name. - - - Optional inner MCP params. - -If omitted or set to `null`, the inner MCP message has no params. - - - #### MessageMcpRequest **UNSTABLE** @@ -2270,9 +2083,6 @@ these keys. See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v1/draft/extensibility) - -McpConnectionId} required> - The MCP-over-ACP connection this message is sent on. The inner MCP method name. @@ -2283,6 +2093,12 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v1/d If omitted or set to `null`, the inner MCP message has no params. +McpRequestId} required> + The caller-generated identifier for the inner MCP request. + +McpServerAcpId} required> + The declared ACP MCP server receiving this request. + #### MessageMcpResponse @@ -5021,13 +4837,16 @@ Agent supports `McpServer::Acp`. -## McpConnectionId +## McpRequestId **UNSTABLE** This capability is not part of the spec yet, and may be removed or changed at any point. -A unique identifier for an active MCP-over-ACP connection. +Identifies an inner MCP request active against a server on this ACP connection. + +Generated by the caller and preserved unchanged by proxies. This is distinct +from the outer ACP JSON-RPC request ID. **Type:** `string` @@ -5181,7 +5000,7 @@ This capability is not part of the spec yet, and may be removed or changed at an ACP transport configuration for MCP. The MCP server is provided by an ACP component and communicates over the ACP channel -using `mcp/connect`, `mcp/message`, and `mcp/disconnect`. +using `mcp/message`. **Type:** Object @@ -5215,8 +5034,7 @@ This capability is not part of the spec yet, and may be removed or changed at an Unique identifier for an MCP server using the ACP transport. The value is opaque and generated by the ACP component providing the MCP server. It is -used by `mcp/connect` to route connection requests back to the component that declared the -server. +used by `mcp/message` to route requests to the component that declared the server. **Type:** `string` diff --git a/docs/protocol/v2/draft/schema.mdx b/docs/protocol/v2/draft/schema.mdx index 22a21b0f2..8e19c60fa 100644 --- a/docs/protocol/v2/draft/schema.mdx +++ b/docs/protocol/v2/draft/schema.mdx @@ -419,87 +419,44 @@ The client should disconnect, if it doesn't support this version. This capability is not part of the spec yet, and may be removed or changed at any point. -Exchanges an MCP-over-ACP message. +Sends an MCP-over-ACP notification. #### MessageMcpNotification **UNSTABLE** -This capability is not part of the spec yet, and may be removed or changed at any point. - -Notification parameters for `mcp/message`. - -This is used when the wrapped MCP message is a notification and the outer JSON-RPC -envelope has no `id`. +Notification for an active request, sent from provider to consumer. +Includes subscription acknowledgements and updates. **Type:** Object **Properties:** - - The _meta property is reserved by ACP to allow clients and agents to attach additional -metadata to their interactions. Implementations MUST NOT make assumptions about values at -these keys. - -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility) - - -McpConnectionId} required> - The MCP-over-ACP connection this message is sent on. + + ACP extension metadata (not inner MCP params._meta); null is equivalent to + omission. The inner MCP method name. - - Optional inner MCP params. - -If omitted or set to `null`, the inner MCP message has no params. - + + Optional inner MCP params; null is equivalent to omission. - -#### MessageMcpRequest - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Request parameters for `mcp/message`. - -**Type:** Object - -**Properties:** - - - The _meta property is reserved by ACP to allow clients and agents to attach additional -metadata to their interactions. Implementations MUST NOT make assumptions about values at -these keys. - -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility) - - -McpConnectionId} required> - The MCP-over-ACP connection this message is sent on. - - - The inner MCP method name. +McpRequestId} + required +> + The identifier of the active inner MCP request. - - Optional inner MCP params. - -If omitted or set to `null`, the inner MCP message has no params. - +McpServerAcpId} + required +> + The declared ACP MCP server handling the associated request. -#### MessageMcpResponse - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Response to `mcp/message`. - -This is the inner MCP response result payload. Any JSON value is valid. - ### nes/accept @@ -1885,117 +1842,6 @@ future ACP variants. - -### mcp/connect - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Opens an MCP-over-ACP connection. - -#### ConnectMcpRequest - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Request parameters for `mcp/connect`. - -**Type:** Object - -**Properties:** - - - The _meta property is reserved by ACP to allow clients and agents to attach additional -metadata to their interactions. Implementations MUST NOT make assumptions about values at -these keys. - -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility) - - -McpServerAcpId} required> - The ACP MCP server ID that was provided by the component declaring the MCP server. - - -#### ConnectMcpResponse - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Response to `mcp/connect`. - -**Type:** Object - -**Properties:** - - - The _meta property is reserved by ACP to allow clients and agents to attach additional -metadata to their interactions. Implementations MUST NOT make assumptions about values at -these keys. - -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility) - - -McpConnectionId} required> - The unique identifier for this MCP-over-ACP connection. - - - -### mcp/disconnect - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Closes an MCP-over-ACP connection. - -#### DisconnectMcpRequest - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Request parameters for `mcp/disconnect`. - -**Type:** Object - -**Properties:** - - - The _meta property is reserved by ACP to allow clients and agents to attach additional -metadata to their interactions. Implementations MUST NOT make assumptions about values at -these keys. - -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility) - - -McpConnectionId} required> - The MCP-over-ACP connection to close. - - -#### DisconnectMcpResponse - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Response to `mcp/disconnect`. - -**Type:** Object - -**Properties:** - - - The _meta property is reserved by ACP to allow clients and agents to attach additional -metadata to their interactions. Implementations MUST NOT make assumptions about values at -these keys. - -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility) - - - ### mcp/message @@ -2005,84 +1851,47 @@ This capability is not part of the spec yet, and may be removed or changed at an Exchanges an MCP-over-ACP message. -#### MessageMcpNotification +#### MessageMcpRequest **UNSTABLE** -This capability is not part of the spec yet, and may be removed or changed at any point. - -Notification parameters for `mcp/message`. - -This is used when the wrapped MCP message is a notification and the outer JSON-RPC -envelope has no `id`. +Request parameters for `mcp/message`, sent from consumer to provider. **Type:** Object **Properties:** - - The _meta property is reserved by ACP to allow clients and agents to attach additional -metadata to their interactions. Implementations MUST NOT make assumptions about values at -these keys. - -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility) - - -McpConnectionId} required> - The MCP-over-ACP connection this message is sent on. + + ACP extension metadata (not inner MCP params._meta); null is equivalent to + omission. The inner MCP method name. - - Optional inner MCP params. - -If omitted or set to `null`, the inner MCP message has no params. - + + Optional inner MCP params; null is equivalent to omission. - -#### MessageMcpRequest - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -Request parameters for `mcp/message`. - -**Type:** Object - -**Properties:** - - - The _meta property is reserved by ACP to allow clients and agents to attach additional -metadata to their interactions. Implementations MUST NOT make assumptions about values at -these keys. - -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility) - - -McpConnectionId} required> - The MCP-over-ACP connection this message is sent on. - - - The inner MCP method name. +McpRequestId} + required +> + The caller-generated identifier for the inner MCP request. - - Optional inner MCP params. - -If omitted or set to `null`, the inner MCP message has no params. - +McpServerAcpId} + required +> + The declared ACP MCP server receiving this request. #### MessageMcpResponse **UNSTABLE** -This capability is not part of the spec yet, and may be removed or changed at any point. - -Response to `mcp/message`. - -This is the inner MCP response result payload. Any JSON value is valid. +Response to `mcp/message`: transparent inner MCP result. MCP errors use +the outer ACP JSON-RPC error envelope. ### session/request_permission @@ -4995,16 +4804,6 @@ Supplying `\{\}` means the agent supports stdio MCP server transports. -## McpConnectionId - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. - -A unique identifier for an active MCP-over-ACP connection. - -**Type:** `string` - ## McpHttpCapabilities Capabilities for HTTP MCP server transports. @@ -5024,6 +4823,16 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/d +## McpRequestId + +**UNSTABLE** + +Identifies an inner MCP request active against a server on this ACP connection. +Generated by the caller and preserved unchanged by proxies, independently of +the outer ACP JSON-RPC request ID. + +**Type:** `string` + ## McpServer Configuration for connecting to an MCP (Model Context Protocol) server. @@ -5174,7 +4983,7 @@ This capability is not part of the spec yet, and may be removed or changed at an ACP transport configuration for MCP. The MCP server is provided by an ACP component and communicates over the ACP channel -using `mcp/connect`, `mcp/message`, and `mcp/disconnect`. +using `mcp/message`. **Type:** Object @@ -5208,8 +5017,7 @@ This capability is not part of the spec yet, and may be removed or changed at an Unique identifier for an MCP server using the ACP transport. The value is opaque and generated by the ACP component providing the MCP server. It is -used by `mcp/connect` to route connection requests back to the component that declared the -server. +used by `mcp/message` to route requests to the component that declared the server. **Type:** `string` diff --git a/schema-generator/src/main.rs b/schema-generator/src/main.rs index b5690d4e0..aaa4c4385 100644 --- a/schema-generator/src/main.rs +++ b/schema-generator/src/main.rs @@ -1931,7 +1931,7 @@ starting with '$/' it is free to ignore the notification." "document/didClose" => self.agent.get("DidCloseDocumentNotification").unwrap(), "document/didSave" => self.agent.get("DidSaveDocumentNotification").unwrap(), "document/didFocus" => self.agent.get("DidFocusDocumentNotification").unwrap(), - "mcp/message" => self.agent.get("MessageMcpRequest").unwrap(), + "mcp/message" => self.agent.get("MessageMcpNotification").unwrap(), _ => panic!("Introduced a method? Add it here :)"), } } @@ -1957,9 +1957,7 @@ starting with '$/' it is free to ignore the notification." "elicitation/complete" => { self.client.get("CompleteElicitationNotification").unwrap() } - "mcp/connect" => self.client.get("ConnectMcpRequest").unwrap(), "mcp/message" => self.client.get("MessageMcpRequest").unwrap(), - "mcp/disconnect" => self.client.get("DisconnectMcpRequest").unwrap(), _ => panic!("Introduced a method? Add it here :)"), } } diff --git a/schema/v1/meta.unstable.json b/schema/v1/meta.unstable.json index d8937f384..956449fc0 100644 --- a/schema/v1/meta.unstable.json +++ b/schema/v1/meta.unstable.json @@ -40,9 +40,7 @@ "terminal_release": "terminal/release", "terminal_wait_for_exit": "terminal/wait_for_exit", "terminal_kill": "terminal/kill", - "mcp_connect": "mcp/connect", "mcp_message": "mcp/message", - "mcp_disconnect": "mcp/disconnect", "elicitation_create": "elicitation/create", "elicitation_complete": "elicitation/complete" }, diff --git a/schema/v1/schema.unstable.json b/schema/v1/schema.unstable.json index dafc52359..e6493ac1e 100644 --- a/schema/v1/schema.unstable.json +++ b/schema/v1/schema.unstable.json @@ -222,15 +222,6 @@ } ] }, - { - "title": "ConnectMcpRequest", - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nOpens an MCP-over-ACP connection.", - "allOf": [ - { - "$ref": "#/$defs/ConnectMcpRequest" - } - ] - }, { "title": "MessageMcpRequest", "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nExchanges an MCP-over-ACP message.", @@ -240,15 +231,6 @@ } ] }, - { - "title": "DisconnectMcpRequest", - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nCloses an MCP-over-ACP connection.", - "allOf": [ - { - "$ref": "#/$defs/DisconnectMcpRequest" - } - ] - }, { "title": "ExtMethodRequest", "description": "Handles extension method requests from the agent.\n\nAllows the Agent to send an arbitrary request that is not part of the ACP spec.\nExtension methods provide a way to add custom functionality while maintaining\nprotocol compatibility.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", @@ -2193,42 +2175,23 @@ ], "required": ["elicitationId", "url"] }, - "ConnectMcpRequest": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nRequest parameters for `mcp/connect`.", + "MessageMcpRequest": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nRequest parameters for `mcp/message`.", "type": "object", "properties": { "serverId": { - "description": "The ACP MCP server ID that was provided by the component declaring the MCP server.", + "description": "The declared ACP MCP server receiving this request.", "allOf": [ { "$ref": "#/$defs/McpServerAcpId" } ] }, - "_meta": { - "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", - "type": ["object", "null"], - "x-deserialize-default-on-error": true, - "additionalProperties": true - } - }, - "required": ["serverId"], - "x-side": "client", - "x-method": "mcp/connect" - }, - "McpServerAcpId": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nUnique identifier for an MCP server using the ACP transport.\n\nThe value is opaque and generated by the ACP component providing the MCP server. It is\nused by `mcp/connect` to route connection requests back to the component that declared the\nserver.", - "type": "string" - }, - "MessageMcpRequest": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nRequest parameters for `mcp/message`.", - "type": "object", - "properties": { - "connectionId": { - "description": "The MCP-over-ACP connection this message is sent on.", + "requestId": { + "description": "The caller-generated identifier for the inner MCP request.", "allOf": [ { - "$ref": "#/$defs/McpConnectionId" + "$ref": "#/$defs/McpRequestId" } ] }, @@ -2248,36 +2211,17 @@ "additionalProperties": true } }, - "required": ["connectionId", "method"], - "x-side": "both", + "required": ["serverId", "requestId", "method"], + "x-side": "client", "x-method": "mcp/message" }, - "McpConnectionId": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nA unique identifier for an active MCP-over-ACP connection.", + "McpServerAcpId": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nUnique identifier for an MCP server using the ACP transport.\n\nThe value is opaque and generated by the ACP component providing the MCP server. It is\nused by `mcp/message` to route requests to the component that declared the server.", "type": "string" }, - "DisconnectMcpRequest": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nRequest parameters for `mcp/disconnect`.", - "type": "object", - "properties": { - "connectionId": { - "description": "The MCP-over-ACP connection to close.", - "allOf": [ - { - "$ref": "#/$defs/McpConnectionId" - } - ] - }, - "_meta": { - "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", - "type": ["object", "null"], - "x-deserialize-default-on-error": true, - "additionalProperties": true - } - }, - "required": ["connectionId"], - "x-side": "client", - "x-method": "mcp/disconnect" + "McpRequestId": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nIdentifies an inner MCP request active against a server on this ACP connection.\n\nGenerated by the caller and preserved unchanged by proxies. This is distinct\nfrom the outer ACP JSON-RPC request ID.", + "type": "string" }, "ExtRequest": { "description": "Allows for sending an arbitrary request that is not part of the ACP spec.\nExtension methods provide a way to add custom functionality while maintaining\nprotocol compatibility.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)" @@ -2480,15 +2424,6 @@ "$ref": "#/$defs/ExtResponse" } ] - }, - { - "title": "MessageMcpResponse", - "description": "Successful result returned by an MCP-over-ACP `mcp/message` request.", - "allOf": [ - { - "$ref": "#/$defs/MessageMcpResponse" - } - ] } ] } @@ -4787,11 +4722,6 @@ "ExtResponse": { "description": "Allows for sending an arbitrary response to an [`ExtRequest`] that is not part of the ACP spec.\nExtension methods provide a way to add custom functionality while maintaining\nprotocol compatibility.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)" }, - "MessageMcpResponse": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nResponse to `mcp/message`.\n\nThis is the inner MCP response result payload. Any JSON value is valid.", - "x-side": "both", - "x-method": "mcp/message" - }, "Error": { "description": "JSON-RPC error object.\n\nRepresents an error that occurred during method execution, following the\nJSON-RPC 2.0 error object specification with optional additional data.\n\nSee protocol docs: [JSON-RPC Error Object](https://www.jsonrpc.org/specification#error_object)", "type": "object", @@ -4914,15 +4844,6 @@ } ] }, - { - "title": "MessageMcpNotification", - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nReceives an MCP-over-ACP notification.", - "allOf": [ - { - "$ref": "#/$defs/MessageMcpNotification" - } - ] - }, { "title": "ExtNotification", "description": "Handles extension notifications from the agent.\n\nAllows the Agent to send an arbitrary notification that is not part of the ACP spec.\nExtension notifications provide a way to send one-way messages for custom functionality\nwhile maintaining protocol compatibility.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", @@ -6021,39 +5942,6 @@ "x-side": "client", "x-method": "elicitation/complete" }, - "MessageMcpNotification": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nNotification parameters for `mcp/message`.\n\nThis is used when the wrapped MCP message is a notification and the outer JSON-RPC\nenvelope has no `id`.", - "type": "object", - "properties": { - "connectionId": { - "description": "The MCP-over-ACP connection this message is sent on.", - "allOf": [ - { - "$ref": "#/$defs/McpConnectionId" - } - ] - }, - "method": { - "description": "The inner MCP method name.", - "type": "string" - }, - "params": { - "description": "Optional inner MCP params.\n\nIf omitted or set to `null`, the inner MCP message has no params.", - "type": ["object", "null"], - "x-deserialize-default-on-error": true, - "additionalProperties": true - }, - "_meta": { - "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", - "type": ["object", "null"], - "x-deserialize-default-on-error": true, - "additionalProperties": true - } - }, - "required": ["connectionId", "method"], - "x-side": "both", - "x-method": "mcp/message" - }, "ExtNotification": { "description": "Allows the Agent to send an arbitrary notification that is not part of the ACP spec.\nExtension notifications provide a way to send one-way messages for custom functionality\nwhile maintaining protocol compatibility.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)" }, @@ -6250,15 +6138,6 @@ } ] }, - { - "title": "MessageMcpRequest", - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nExchanges an MCP-over-ACP message.", - "allOf": [ - { - "$ref": "#/$defs/MessageMcpRequest" - } - ] - }, { "title": "ExtMethodRequest", "description": "Handles extension method requests from the client.\n\nExtension methods provide a way to add custom functionality while maintaining\nprotocol compatibility.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", @@ -7016,7 +6895,7 @@ "required": ["name", "url", "headers"] }, "McpServerAcp": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nACP transport configuration for MCP.\n\nThe MCP server is provided by an ACP component and communicates over the ACP channel\nusing `mcp/connect`, `mcp/message`, and `mcp/disconnect`.", + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nACP transport configuration for MCP.\n\nThe MCP server is provided by an ACP component and communicates over the ACP channel\nusing `mcp/message`.", "type": "object", "properties": { "name": { @@ -7990,24 +7869,6 @@ } ] }, - { - "title": "ConnectMcpResponse", - "description": "Successful result returned for a `mcp/connect` request.", - "allOf": [ - { - "$ref": "#/$defs/ConnectMcpResponse" - } - ] - }, - { - "title": "DisconnectMcpResponse", - "description": "Successful result returned for a `mcp/disconnect` request.", - "allOf": [ - { - "$ref": "#/$defs/DisconnectMcpResponse" - } - ] - }, { "title": "MessageMcpResponse", "description": "Successful result returned by an MCP-over-ACP `mcp/message` request.", @@ -8455,42 +8316,10 @@ } } }, - "ConnectMcpResponse": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nResponse to `mcp/connect`.", - "type": "object", - "properties": { - "connectionId": { - "description": "The unique identifier for this MCP-over-ACP connection.", - "allOf": [ - { - "$ref": "#/$defs/McpConnectionId" - } - ] - }, - "_meta": { - "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", - "type": ["object", "null"], - "x-deserialize-default-on-error": true, - "additionalProperties": true - } - }, - "required": ["connectionId"], - "x-side": "client", - "x-method": "mcp/connect" - }, - "DisconnectMcpResponse": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nResponse to `mcp/disconnect`.", - "type": "object", - "properties": { - "_meta": { - "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", - "type": ["object", "null"], - "x-deserialize-default-on-error": true, - "additionalProperties": true - } - }, + "MessageMcpResponse": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nResponse to `mcp/message`.\n\nThis is the inner MCP response result payload. Any JSON value is valid.", "x-side": "client", - "x-method": "mcp/disconnect" + "x-method": "mcp/message" }, "ClientNotification": { "description": "A JSON-RPC notification object.", @@ -8940,6 +8769,46 @@ } ] }, + "MessageMcpNotification": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nNotification parameters for `mcp/message`.\n\nSent by the provider to the consumer for an active request (including\nsubscription acknowledgements and updates); the outer envelope has no `id`.", + "type": "object", + "properties": { + "serverId": { + "description": "The declared ACP MCP server handling the associated request.", + "allOf": [ + { + "$ref": "#/$defs/McpServerAcpId" + } + ] + }, + "requestId": { + "description": "The identifier of the active inner MCP request.", + "allOf": [ + { + "$ref": "#/$defs/McpRequestId" + } + ] + }, + "method": { + "description": "The inner MCP method name.", + "type": "string" + }, + "params": { + "description": "Optional inner MCP params.\n\nIf omitted or set to `null`, the inner MCP message has no params.", + "type": ["object", "null"], + "additionalProperties": true + }, + "_meta": { + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", + "type": ["object", "null"], + "x-deserialize-default-on-error": true, + "additionalProperties": true + } + }, + "required": ["serverId", "requestId", "method"], + "x-side": "agent", + "x-method": "mcp/message" + }, "CancelRequestNotification": { "description": "Notification to cancel an ongoing request.\n\nSee protocol docs: [Cancellation](https://agentclientprotocol.com/protocol/cancellation)", "type": "object", diff --git a/schema/v2/meta.unstable.json b/schema/v2/meta.unstable.json index 47d7973cb..1912c7914 100644 --- a/schema/v2/meta.unstable.json +++ b/schema/v2/meta.unstable.json @@ -31,9 +31,7 @@ "clientMethods": { "session_request_permission": "session/request_permission", "session_update": "session/update", - "mcp_connect": "mcp/connect", "mcp_message": "mcp/message", - "mcp_disconnect": "mcp/disconnect", "elicitation_create": "elicitation/create", "elicitation_complete": "elicitation/complete" }, diff --git a/schema/v2/schema.unstable.json b/schema/v2/schema.unstable.json index 03c0e762e..18dffc072 100644 --- a/schema/v2/schema.unstable.json +++ b/schema/v2/schema.unstable.json @@ -313,15 +313,6 @@ "$ref": "#/$defs/ExtResponse" } ] - }, - { - "title": "MessageMcpResponse", - "description": "Successful result returned by an MCP-over-ACP `mcp/message` request.", - "allOf": [ - { - "$ref": "#/$defs/MessageMcpResponse" - } - ] } ] } @@ -449,24 +440,6 @@ } ] }, - { - "title": "ConnectMcpResponse", - "description": "Successful result returned for a `mcp/connect` request.", - "allOf": [ - { - "$ref": "#/$defs/ConnectMcpResponse" - } - ] - }, - { - "title": "DisconnectMcpResponse", - "description": "Successful result returned for a `mcp/disconnect` request.", - "allOf": [ - { - "$ref": "#/$defs/DisconnectMcpResponse" - } - ] - }, { "title": "MessageMcpResponse", "description": "Successful result returned by an MCP-over-ACP `mcp/message` request.", @@ -600,15 +573,6 @@ } ] }, - { - "title": "ConnectMcpRequest", - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nOpens an MCP-over-ACP connection.", - "allOf": [ - { - "$ref": "#/$defs/ConnectMcpRequest" - } - ] - }, { "title": "MessageMcpRequest", "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nExchanges an MCP-over-ACP message.", @@ -618,15 +582,6 @@ } ] }, - { - "title": "DisconnectMcpRequest", - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nCloses an MCP-over-ACP connection.", - "allOf": [ - { - "$ref": "#/$defs/DisconnectMcpRequest" - } - ] - }, { "title": "ExtMethodRequest", "description": "Handles extension method requests from the agent.\n\nAllows the Agent to send an arbitrary request that is not part of the ACP spec.\nExtension methods provide a way to add custom functionality while maintaining\nprotocol compatibility.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility)", @@ -3010,42 +2965,23 @@ ], "required": ["elicitationId", "url"] }, - "ConnectMcpRequest": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nRequest parameters for `mcp/connect`.", + "MessageMcpRequest": { + "description": "**UNSTABLE**\n\nRequest parameters for `mcp/message`, sent from consumer to provider.", "type": "object", "properties": { "serverId": { - "description": "The ACP MCP server ID that was provided by the component declaring the MCP server.", + "description": "The declared ACP MCP server receiving this request.", "allOf": [ { "$ref": "#/$defs/McpServerAcpId" } ] }, - "_meta": { - "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility)", - "type": ["object", "null"], - "x-deserialize-default-on-error": true, - "additionalProperties": true - } - }, - "required": ["serverId"], - "x-side": "client", - "x-method": "mcp/connect" - }, - "McpServerAcpId": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nUnique identifier for an MCP server using the ACP transport.\n\nThe value is opaque and generated by the ACP component providing the MCP server. It is\nused by `mcp/connect` to route connection requests back to the component that declared the\nserver.", - "type": "string" - }, - "MessageMcpRequest": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nRequest parameters for `mcp/message`.", - "type": "object", - "properties": { - "connectionId": { - "description": "The MCP-over-ACP connection this message is sent on.", + "requestId": { + "description": "The caller-generated identifier for the inner MCP request.", "allOf": [ { - "$ref": "#/$defs/McpConnectionId" + "$ref": "#/$defs/McpRequestId" } ] }, @@ -3054,47 +2990,28 @@ "type": "string" }, "params": { - "description": "Optional inner MCP params.\n\nIf omitted or set to `null`, the inner MCP message has no params.", + "description": "Optional inner MCP params; null is equivalent to omission.", "type": ["object", "null"], "additionalProperties": true }, "_meta": { - "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility)", + "description": "ACP extension metadata (not inner MCP params._meta); null is equivalent to omission.", "type": ["object", "null"], "x-deserialize-default-on-error": true, "additionalProperties": true } }, - "required": ["connectionId", "method"], - "x-side": "both", + "required": ["serverId", "requestId", "method"], + "x-side": "client", "x-method": "mcp/message" }, - "McpConnectionId": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nA unique identifier for an active MCP-over-ACP connection.", + "McpServerAcpId": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nUnique identifier for an MCP server using the ACP transport.\n\nThe value is opaque and generated by the ACP component providing the MCP server. It is\nused by `mcp/message` to route requests to the component that declared the server.", "type": "string" }, - "DisconnectMcpRequest": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nRequest parameters for `mcp/disconnect`.", - "type": "object", - "properties": { - "connectionId": { - "description": "The MCP-over-ACP connection to close.", - "allOf": [ - { - "$ref": "#/$defs/McpConnectionId" - } - ] - }, - "_meta": { - "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility)", - "type": ["object", "null"], - "x-deserialize-default-on-error": true, - "additionalProperties": true - } - }, - "required": ["connectionId"], - "x-side": "client", - "x-method": "mcp/disconnect" + "McpRequestId": { + "description": "**UNSTABLE**\n\nIdentifies an inner MCP request active against a server on this ACP connection.\nGenerated by the caller and preserved unchanged by proxies, independently of\nthe outer ACP JSON-RPC request ID.", + "type": "string" }, "ExtRequest": { "description": "Allows for sending an arbitrary request that is not part of the ACP spec.\nExtension methods provide a way to add custom functionality while maintaining\nprotocol compatibility.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility)" @@ -3279,15 +3196,6 @@ "$ref": "#/$defs/ExtResponse" } ] - }, - { - "title": "MessageMcpResponse", - "description": "Successful result returned by an MCP-over-ACP `mcp/message` request.", - "allOf": [ - { - "$ref": "#/$defs/MessageMcpResponse" - } - ] } ] } @@ -5541,11 +5449,6 @@ "ExtResponse": { "description": "Allows for sending an arbitrary response to an [`ExtRequest`] that is not part of the ACP spec.\nExtension methods provide a way to add custom functionality while maintaining\nprotocol compatibility.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility)" }, - "MessageMcpResponse": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nResponse to `mcp/message`.\n\nThis is the inner MCP response result payload. Any JSON value is valid.", - "x-side": "both", - "x-method": "mcp/message" - }, "Error": { "description": "JSON-RPC error object.\n\nRepresents an error that occurred during method execution, following the\nJSON-RPC 2.0 error object specification with optional additional data.\n\nSee protocol docs: [JSON-RPC Error Object](https://www.jsonrpc.org/specification#error_object)", "type": "object", @@ -5668,15 +5571,6 @@ } ] }, - { - "title": "MessageMcpNotification", - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nReceives an MCP-over-ACP notification.", - "allOf": [ - { - "$ref": "#/$defs/MessageMcpNotification" - } - ] - }, { "title": "ExtNotification", "description": "Handles extension notifications from the agent.\n\nAllows the Agent to send an arbitrary notification that is not part of the ACP spec.\nExtension notifications provide a way to send one-way messages for custom functionality\nwhile maintaining protocol compatibility.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility)", @@ -7528,39 +7422,6 @@ "x-side": "client", "x-method": "elicitation/complete" }, - "MessageMcpNotification": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nNotification parameters for `mcp/message`.\n\nThis is used when the wrapped MCP message is a notification and the outer JSON-RPC\nenvelope has no `id`.", - "type": "object", - "properties": { - "connectionId": { - "description": "The MCP-over-ACP connection this message is sent on.", - "allOf": [ - { - "$ref": "#/$defs/McpConnectionId" - } - ] - }, - "method": { - "description": "The inner MCP method name.", - "type": "string" - }, - "params": { - "description": "Optional inner MCP params.\n\nIf omitted or set to `null`, the inner MCP message has no params.", - "type": ["object", "null"], - "x-deserialize-default-on-error": true, - "additionalProperties": true - }, - "_meta": { - "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility)", - "type": ["object", "null"], - "x-deserialize-default-on-error": true, - "additionalProperties": true - } - }, - "required": ["connectionId", "method"], - "x-side": "both", - "x-method": "mcp/message" - }, "ExtNotification": { "description": "Allows the Agent to send an arbitrary notification that is not part of the ACP spec.\nExtension notifications provide a way to send one-way messages for custom functionality\nwhile maintaining protocol compatibility.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility)" }, @@ -7739,15 +7600,6 @@ } ] }, - { - "title": "MessageMcpRequest", - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nExchanges an MCP-over-ACP message.", - "allOf": [ - { - "$ref": "#/$defs/MessageMcpRequest" - } - ] - }, { "title": "ExtMethodRequest", "description": "Handles extension method requests from the client.\n\nExtension methods provide a way to add custom functionality while maintaining\nprotocol compatibility.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility)", @@ -8355,7 +8207,7 @@ "required": ["name", "url"] }, "McpServerAcp": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nACP transport configuration for MCP.\n\nThe MCP server is provided by an ACP component and communicates over the ACP channel\nusing `mcp/connect`, `mcp/message`, and `mcp/disconnect`.", + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nACP transport configuration for MCP.\n\nThe MCP server is provided by an ACP component and communicates over the ACP channel\nusing `mcp/message`.", "type": "object", "properties": { "name": { @@ -9356,24 +9208,6 @@ } ] }, - { - "title": "ConnectMcpResponse", - "description": "Successful result returned for a `mcp/connect` request.", - "allOf": [ - { - "$ref": "#/$defs/ConnectMcpResponse" - } - ] - }, - { - "title": "DisconnectMcpResponse", - "description": "Successful result returned for a `mcp/disconnect` request.", - "allOf": [ - { - "$ref": "#/$defs/DisconnectMcpResponse" - } - ] - }, { "title": "MessageMcpResponse", "description": "Successful result returned by an MCP-over-ACP `mcp/message` request.", @@ -9686,42 +9520,10 @@ } } }, - "ConnectMcpResponse": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nResponse to `mcp/connect`.", - "type": "object", - "properties": { - "connectionId": { - "description": "The unique identifier for this MCP-over-ACP connection.", - "allOf": [ - { - "$ref": "#/$defs/McpConnectionId" - } - ] - }, - "_meta": { - "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility)", - "type": ["object", "null"], - "x-deserialize-default-on-error": true, - "additionalProperties": true - } - }, - "required": ["connectionId"], - "x-side": "client", - "x-method": "mcp/connect" - }, - "DisconnectMcpResponse": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nResponse to `mcp/disconnect`.", - "type": "object", - "properties": { - "_meta": { - "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility)", - "type": ["object", "null"], - "x-deserialize-default-on-error": true, - "additionalProperties": true - } - }, + "MessageMcpResponse": { + "description": "**UNSTABLE**\n\nResponse to `mcp/message`: transparent inner MCP result. MCP errors use\nthe outer ACP JSON-RPC error envelope.", "x-side": "client", - "x-method": "mcp/disconnect" + "x-method": "mcp/message" }, "ClientNotification": { "description": "A JSON-RPC notification object.", @@ -10181,6 +9983,46 @@ } ] }, + "MessageMcpNotification": { + "description": "**UNSTABLE**\n\nNotification for an active request, sent from provider to consumer.\nIncludes subscription acknowledgements and updates.", + "type": "object", + "properties": { + "serverId": { + "description": "The declared ACP MCP server handling the associated request.", + "allOf": [ + { + "$ref": "#/$defs/McpServerAcpId" + } + ] + }, + "requestId": { + "description": "The identifier of the active inner MCP request.", + "allOf": [ + { + "$ref": "#/$defs/McpRequestId" + } + ] + }, + "method": { + "description": "The inner MCP method name.", + "type": "string" + }, + "params": { + "description": "Optional inner MCP params; null is equivalent to omission.", + "type": ["object", "null"], + "additionalProperties": true + }, + "_meta": { + "description": "ACP extension metadata (not inner MCP params._meta); null is equivalent to omission.", + "type": ["object", "null"], + "x-deserialize-default-on-error": true, + "additionalProperties": true + } + }, + "required": ["serverId", "requestId", "method"], + "x-side": "agent", + "x-method": "mcp/message" + }, "ProtocolLevelNotification": { "description": "A JSON-RPC notification object.", "type": "object", From eac57c9ebe44db1035d1ec6f38617c7d514d602d Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 24 Sep 2026 15:22:45 +0200 Subject: [PATCH 04/10] docs(rfd): describe stateless MCP transport and verified limits --- docs/rfds/mcp-over-acp.mdx | 478 +++++++++++-------------------------- docs/rfds/proxy-chains.mdx | 6 +- 2 files changed, 137 insertions(+), 347 deletions(-) diff --git a/docs/rfds/mcp-over-acp.mdx b/docs/rfds/mcp-over-acp.mdx index aa8df9075..cddab64c4 100644 --- a/docs/rfds/mcp-over-acp.mdx +++ b/docs/rfds/mcp-over-acp.mdx @@ -1,42 +1,41 @@ --- -title: "MCP-over-ACP: MCP Transport via ACP Channels" +title: "MCP-over-ACP: Stateless MCP Requests over ACP" --- Author(s): [nikomatsakis](https://github.com/nikomatsakis) -**Revised target: MCP 2026-07-28 only.** The [modernization audit](#modernization-audit-mcp-2026-07-28) below defines the work needed for the latest, stateless MCP specification. The connection-oriented proposal and implementation notes preceding that audit are retained as a **work-in-progress checkpoint**, not the intended stabilized transport. In particular, the initialization handshake, reverse JSON-RPC requests, and stateful HTTP sessions described there must not become compatibility requirements. This transport is intended to supply new tools to agents; support for older MCP revisions is out of scope. - ## Elevator pitch -> What are you proposing to change? +Let an ACP client or proxy supply tools to an agent using the ACP connection it already has. Declare an MCP server with `"type": "acp"`, then send independent MCP requests to its `serverId`. Results, request-scoped notifications, and cancellation travel over ACP without a second process or network endpoint. -Add support for MCP servers that communicate over ACP channels instead of stdio or HTTP. This enables any ACP component to provide MCP tools and handle callbacks through the existing ACP connection, without spawning separate processes or managing additional transports. +This draft targets **MCP 2026-07-28 only**. It does not emulate older MCP revisions. ACP initialization and sessions remain unchanged, but there is no MCP initialization handshake, MCP connection object, or connect/disconnect exchange. -## Status quo +## Motivation -> How do things work today and what problems does this cause? Why would we change things? +ACP manages an agent's conversation while MCP provides tools behind it. A client often needs to do both: provide project-aware tools, ask the agent to use them, and execute them inside the client's process or sandbox. -ACP and MCP each solve different halves of the problem of interacting with an agent. ACP stands in "front" of the agent, managing sessions, sending prompts, and receiving responses. MCP stands "behind" the agent, providing tools that the agent can use to do its work. +Requiring a separate HTTP listener or subprocess for those tools creates an extra communication path and makes isolation harder. Native MCP-over-ACP keeps the interaction on the authorized ACP channel. It works directly between a client and an agent; proxy chains are useful but not required. -Many applications would benefit from being able to be both "in front" of the agent and "behind" it. This would allow a client, for example, to create custom MCP tools that are tailored to a specific request and which live in the client's address space. +## Protocol model -The only way to combine ACP and MCP today is to use some sort of "backdoor", such as opening an HTTP port for the agent to connect to or providing a binary that communicates with IPC. This is inconvenient to implement but also means that clients cannot be properly abstracted and sandboxed, as some of the communication with the agent is going through side channels. Imagine trying to host an ACP component (client, agent, or [agent extension](./proxy-chains.mdx)) that runs in a WASM sandbox or even on another machine: for that to work, the ACP protocol has to encompass all of the relevant interactions so that messages can be transmitted properly. +There are three identities, with different purposes: -## What we propose to do about it +- **`serverId`** names a server declared by its provider. It selects a tool/resource/prompt offering, not an MCP session. +- **`requestId`** is a caller-generated opaque string identifying one logical MCP request. It is used as the inner MCP JSON-RPC ID and is preserved across ACP proxies. +- **Outer JSON-RPC `id`** identifies the ACP request on one hop. A proxy may renumber it when forwarding. -> What are you proposing to improve the situation? +Every operation carries its own MCP version, client capabilities, and other request context. Nothing about a previous request establishes that context for the next one. A subscription may keep one request alive; that is not a session for unrelated calls. -We propose adding `"acp"` as a new MCP transport type. When an ACP component (client or proxy) adds an MCP server with ACP transport to a session, tool invocations for that server are routed back through the ACP channel to the component that provided it. +The binding uses one method name, `mcp/message`, in two directions: -This enables patterns like: +- An **agent-to-provider request** invokes one MCP operation and eventually receives one result or error. +- A **provider-to-agent notification** carries an MCP notification belonging to that active operation. -- A **client** that injects project-aware tools into every session and handles callbacks directly -- An **[agent extension](./proxy-chains.mdx)** that adds context-aware tools based on the conversation state -- A **bridge** that translates ACP-transport MCP servers to HTTP or stdio for agents that don't support native ACP transport +There are no provider-originated MCP requests. Interactive tools use MCP's multi round-trip request pattern (MRTR). -### How it works +## Declaring a server -When the client connects, the agent advertises MCP-over-ACP support in its `InitializeResponse` (see [Capability advertising](#capability-advertising) for the v1 and draft-v2 shapes). If supported, the client can add MCP servers to a `session/new` request with `"type": "acp"` and a `serverId` that identifies the server. For example, the session request parameters can include: +The client checks the agent's ACP MCP capability, then includes a declaration in a session setup request. For example, `session/new` parameters can include: ```json { @@ -45,81 +44,19 @@ When the client connects, the agent advertises MCP-over-ACP support in its `Init { "type": "acp", "name": "project-tools", - "serverId": "550e8400-e29b-41d4-a716-446655440000" + "serverId": "project-tools:7a72" } ] } ``` -The `serverId` is an opaque string generated by the component providing the MCP server; it need not be a UUID. The same declaration can be supplied to other session setup methods that accept `mcpServers`, such as `session/resume`. - -When the agent connects to the MCP server, an `mcp/connect` request is sent with the MCP server's `serverId`. This returns a fresh `connectionId`. MCP messages are then sent back and forth using `mcp/message` requests and notifications. This includes the normal MCP initialization handshake: opening the transport does not initialize MCP itself. Finally, an `mcp/disconnect` request closes that connection. - -`mcp/connect` and `mcp/disconnect` are initiated by the side connecting to the ACP-transport MCP server. In the direct client-provided server case, that means the agent sends them to the client. Once connected, `mcp/message` is bidirectional: the agent can send MCP client-originated requests to the server, and the server can send MCP server-originated requests or notifications back to the agent. - -### Bridging and compatibility - -For agents that don't support ACP transport for MCP servers, a wrapper component can translate between ACP-transport MCP servers and the stdio/HTTP transports that those agents support. The wrapper spawns shim processes or HTTP servers that the agent connects to normally, then relays messages to/from the ACP channel. - -The Rust SDK implements HTTP adaptation as an explicit `McpOverAcpPolyfill` proxy from `agent-client-protocol-polyfill`, placed immediately before the final agent in a [proxy chain](./proxy-chains). It is not built into the conductor. The proxy advertises ACP MCP support upstream only when the downstream agent supports native ACP transport or the HTTP transport it can adapt to. Native support is passed through unchanged; a downstream agent supporting neither transport does not gain the capability. - -### Message flow example - -An agent may initialize MCP servers before returning the ACP session ID. Providers must therefore be ready to handle `mcp/connect` as soon as they publish the declaration, without waiting for `session/new` to finish. +The provider generates the opaque `serverId`. Different servers visible on the same ACP connection must have different IDs. The same server may be offered to multiple ACP sessions; distinct offerings should use distinct server IDs rather than hidden per-MCP-connection catalogs. -```mermaid -sequenceDiagram - participant Client - participant Agent - - Client->>Agent: "session/new with an ACP-transport MCP server" - Agent->>Client: "mcp/connect with serverId" - Client-->>Agent: "connectionId" - Agent->>Client: "mcp/message wrapping MCP initialize" - Client-->>Agent: "MCP initialize result" - Agent->>Client: "mcp/message wrapping notifications/initialized" - Agent-->>Client: session created - - Client->>Agent: "session/prompt" - - Note over Agent: Agent decides to use the tool - Agent->>Client: "mcp/message wrapping tools/call" - Client-->>Agent: file listing results - - Client->>Agent: "mcp/message wrapping a server request" - Agent-->>Client: callback result - - Agent-->>Client: response using tool results - - Agent->>Client: "mcp/disconnect with connectionId" - Client-->>Agent: "Empty result after connection cleanup" -``` - -## Shiny future - -> How will things play out once this feature exists? - -### Seamless tool injection - -Components can provide tools without any process management. A Rust development environment could inject cargo-aware tools, a cloud IDE could inject deployment tools, and a security scanner could inject vulnerability checking - all through the same ACP connection they're already using. - -### WebAssembly-based tooling - -Components running in sandboxed environments (like WASM) can provide MCP tools without needing filesystem or process spawning capabilities. The ACP channel is their only interface, and that's sufficient. - -### Transparent bridging - -For agents that don't natively support ACP transport, intermediaries can transparently bridge: accepting MCP-over-ACP from clients and spawning stdio- or HTTP-based MCP servers that the agent can use normally. This provides backwards compatibility while allowing the ecosystem to adopt ACP transport incrementally. - -## Implementation details and plan - -> Tell me more about your implementation. What is your detailed implementation plan? +The provider must be ready to serve requests when it publishes the declaration. An agent may invoke tools or discover the server before returning the ACP session ID. ### Capability advertising -The shared schema exposes this draft transport under the `unstable_mcp_over_acp` feature. Capability placement depends on the negotiated ACP version; the `mcp/*` envelopes below are the same in both versions. - -In **v1**, agents advertise support with `agentCapabilities.mcpCapabilities.acp: true`. The relevant `InitializeResponse` fragment is: +In **ACP v1**, the relevant `InitializeResponse` fragment is: ```json { @@ -131,9 +68,9 @@ In **v1**, agents advertise support with `agentCapabilities.mcpCapabilities.acp: } ``` -Omitting the v1 capability is equivalent to `false`. +Omission is equivalent to `false`. -In **draft v2**, capabilities use optional objects. The relevant `InitializeResponse` fragment is: +In **draft ACP v2**, the fragment is: ```json { @@ -147,342 +84,195 @@ In **draft v2**, capabilities use optional objects. The relevant `InitializeResp } ``` -The v2 `acp` field is optional and nullable: omission or `null` means support is not advertised, while `{}` advertises support. It is not a boolean. HTTP support follows the same optional-object convention in v2. +The v2 field is an optional object: omission or `null` means support is not advertised, and `{}` advertises support. These are ACP capabilities, separate from the MCP client capabilities carried on each request. -Advertising support means the receiving component can consume MCP servers declared with `"type": "acp"`. It will initiate `mcp/connect` and `mcp/disconnect` through the ACP channel, and both sides can exchange MCP payloads with `mcp/message`. +An intermediary only advertises support when its downstream chain can consume this transport. The conductor does not unconditionally add the capability. A proxy that provides no adaptation preserves its successor's capabilities. -Clients don't need to advertise anything - they simply check the agent's capabilities to determine whether bridging is needed. +## Requests and results -**Bridging intermediaries**: An intermediary may advertise ACP MCP support if it can actually adapt to a transport supported by its downstream agent. That capability describes the chain as seen upstream, not necessarily native support in the final agent (see [Bridging](#bridging-for-agents-without-native-support) below). - -### MCP transport schema extension - -We extend the MCP server JSON schema to include ACP as a transport option. `type`, `name`, and `serverId` are required and non-null. `_meta` is optional; omission and `null` both mean no additional metadata. - -```json -{ - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "acp" - }, - "name": { - "type": "string" - }, - "serverId": { - "type": "string" - }, - "_meta": { - "type": ["object", "null"] - } - }, - "required": ["type", "name", "serverId"] -} -``` - -### Message reference - -**Open a connection:** - -`mcp/connect` is a request, not a notification. Its required `serverId` selects the declared MCP server. +The agent sends an ACP request with a server ID, a fresh logical MCP request ID, and flattened MCP method/parameters: ```json { "jsonrpc": "2.0", "id": 20, - "method": "mcp/connect", + "method": "mcp/message", "params": { - "serverId": "550e8400-e29b-41d4-a716-446655440000" + "serverId": "project-tools:7a72", + "requestId": "mcp-request:a11f", + "method": "tools/call", + "params": { + "name": "echo", + "arguments": { + "message": "hello" + }, + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {}, + "io.modelcontextprotocol/clientInfo": { + "name": "example-agent", + "version": "1" + }, + "progressToken": "progress-1" + } + } } } ``` -The response returns the required identifier for the new connection: +The provider executes the inner request using `"mcp-request:a11f"` as its MCP JSON-RPC ID. It does not use the outer ACP ID `20`, which may differ on another hop. + +The ACP response carries the inner MCP result directly: ```json { "jsonrpc": "2.0", "id": 20, "result": { - "connectionId": "conn-123" + "resultType": "complete", + "content": [ + { + "type": "text", + "text": "hello" + } + ], + "isError": false } } ``` -**Close a connection:** - -`mcp/disconnect` is also a request. Its required `connectionId` identifies the connection to close, not the server declaration. +An inner MCP protocol error uses the outer ACP error response, preserving its code, message, and optional data. An MCP tool-execution error remains a tool result with `isError`, not an ACP transport error. -```json -{ - "jsonrpc": "2.0", - "id": 21, - "method": "mcp/disconnect", - "params": { - "connectionId": "conn-123" - } -} -``` +`server/discover` is an ordinary inner MCP request. It is supported but is not a prerequisite for calling a tool. The transport does not silently perform an MCP handshake or substitute ACP capability discovery for MCP server discovery. -A successful response acknowledges connection cleanup: +Discovery's `supportedVersions` describes the revisions available through this binding. The SDK restricts it to 2026-07-28, even when the hosted backend supports additional revisions on other transports; it does not invent support that the backend lacks. Other discovery capabilities and metadata are preserved. -```json -{ - "jsonrpc": "2.0", - "id": 21, - "result": {} -} -``` - -These lifecycle request parameters and response results may also contain an optional `_meta` object. Omission and `null` are equivalent. Both identifier fields are non-null strings. +### Fields and metadata -**MCP message exchange:** +`serverId`, `requestId`, and `method` are required non-null strings on both requests and notifications. A caller uses a fresh `requestId` for each operation, including an MRTR retry, and must not reuse an active ID for the same server on the same ACP connection. -`mcp/message` is bidirectional. Either side can send the following request or notification shape on an established `connectionId`. +The inner `params` field accepts an object or `null` and is optional at the envelope level. Omission and `null` both mean no inner parameters; positional arrays are invalid. This does not relax MCP's requirements: a valid 2026-07-28 request must include its required metadata in `params._meta`. -```json -{ - "jsonrpc": "2.0", - "id": 123, - "method": "mcp/message", - "params": { - "connectionId": "conn-123", - "method": "tools/list", - "params": {} - } -} -``` +An optional outer `_meta` object alongside `serverId` is ACP envelope metadata. Omission and `null` are equivalent. It is distinct from MCP metadata inside the inner parameters or result, which must be preserved. -The response carries the inner MCP result directly: +Providers validate the per-request MCP version and capabilities. Missing or malformed required metadata is an invalid-parameters error. An unsupported version is MCP's `UnsupportedProtocolVersion` error (`-32022`), with the supported and requested versions. There is no fallback to `initialize` or an earlier protocol revision. -```json -{ - "jsonrpc": "2.0", - "id": 123, - "result": { - "tools": [] - } -} -``` +## Request-scoped notifications -A notification has no outer request ID and receives no response: +While a request is active, its provider can send a notification with the same server and logical request IDs: ```json { "jsonrpc": "2.0", "method": "mcp/message", "params": { - "connectionId": "conn-123", - "method": "notifications/tools/list_changed" + "serverId": "project-tools:7a72", + "requestId": "mcp-request:a11f", + "method": "notifications/progress", + "params": { + "progressToken": "progress-1", + "progress": 1, + "total": 2 + } } } ``` -The inner MCP message fields (`method`, `params`) are flattened into the outer params object alongside the required `connectionId`. The inner `method` is a required non-null string. The inner `params` field is optional and accepts an object or `null`, not positional arrays; omission and `null` both mean the inner MCP message has no params. - -Whether the wrapped message is a request or notification is determined by the presence of an `id` field in the outer JSON-RPC envelope. The envelope does not carry a second, nested MCP request ID. For requests, the ACP response result is the inner MCP result payload, and inner MCP errors use the outer JSON-RPC error response, preserving their code, message, and optional data. - -An optional `_meta` object alongside `connectionId` is ACP envelope metadata; omission and `null` are equivalent. It is separate from any MCP `_meta` inside the inner `params` or result. - -### Routing by ID - -The `serverId` in `mcp/connect` matches the `serverId` supplied in the MCP server declaration. The receiving side uses it to route the connection request to the provider. - -Providers must not reuse a server ID for different MCP servers visible on the same ACP connection, even across different ACP sessions. The same server may be offered to multiple sessions using the same server ID. A `connectionId` identifies one active connection to that server and is used for all subsequent messages. - -### Connection multiplexing - -Multiple connections to the same MCP server are supported: every successful `mcp/connect` returns a fresh `connectionId`. Each connection has its own MCP initialization and request state. Closing one connection must not close another connection to the same server or the containing ACP connection. - -### Connection lifetime - -A successful `mcp/connect` response means the provider is ready to route messages for that connection. Both sides must continue dispatching incoming ACP traffic while waiting for MCP responses, since the MCP server can issue a request back to the agent while handling an agent request. - -`mcp/disconnect` stops accepting new messages for that connection, stops its underlying server/relay work, and releases its connection-scoped resources before acknowledging success. Outstanding requests on the closed connection must complete or fail rather than remain pending indefinitely. Messages for an unknown or disconnected connection cannot be delivered: requests receive an error, and notifications do not receive a response. - -Closing the ACP transport releases all of its MCP connections. A disconnect exchange cannot be required after that transport is already gone. A failure in one MCP connection should be contained to that connection rather than terminating unrelated MCP or ACP work. - -### Bridging for agents without native support - -Not all agents will support MCP-over-ACP natively. To maintain compatibility, it is possible to write a bridge that translates ACP-transport MCP servers to transports the agent does support. - -**Bridging approaches:** +The outer notification has no JSON-RPC ID and receives no response. It belongs only to the identified request; a consumer must not deliver unknown or late notifications to a different operation. -- **Stdio shim**: Spawn a small shim process that the agent connects to via stdio. The shim relays MCP messages to/from the ACP channel. This works for agents that support stdio MCP servers. +Progress tokens and other MCP metadata are preserved. Progress and any supported logging notifications belong to their original request, not an unrelated subscription. A provider stops forwarding notifications for a request once its final result or error has been sent. -- **HTTP bridge**: Run a local HTTP server that the agent connects to. MCP messages are relayed to/from the ACP channel. This works for agents that prefer HTTP transport. +### Subscriptions -**How bridging works:** +`subscriptions/listen` is a long-lived `mcp/message` request, not a new ACP connection. Its first associated MCP notification is `notifications/subscriptions/acknowledged`. Subsequent notifications obey the acknowledged filter. -When a client provides an MCP server with `"type": "acp"`, and the agent doesn't advertise native ACP MCP support, a bridge can: +Each subscription notification carries `io.modelcontextprotocol/subscriptionId` in its inner `_meta`. That value is the logical `requestId` of the listen request, not the outer ACP JSON-RPC ID. A graceful completion result carries the same subscription metadata. Multiple subscriptions and ordinary requests may be active concurrently. -1. Rewrite the MCP server declaration in a session setup request to use a transport supported by the agent -2. Spawn the appropriate shim process or HTTP server -3. Open a native MCP connection when a logical MCP client session starts -4. Relay bidirectional requests and notifications between that client and the ACP channel -5. Disconnect that native connection when the logical MCP session ends, without affecting other sessions +## Cancellation and lifetime -From the agent's perspective, it's talking to a normal stdio/HTTP MCP server. From the client's perspective, it's handling MCP-over-ACP messages. The bridge handles the translation transparently. +Use ACP's existing `$/cancel_request` to cancel the **outer ACP request**. For the example request above: -An HTTP bridge may reuse a listening endpoint for a `serverId`, but the listener is not itself an MCP connection. Each independent HTTP MCP session needs its own `mcp/connect` and `connectionId`, so initialization, request IDs, and callbacks cannot cross between clients. A stateful HTTP adapter can identify these sessions using `MCP-Session-Id` and translate HTTP DELETE into `mcp/disconnect`. Closing an individual POST response or GET event stream does not close the logical session. HTTP session management is an adapter concern, not an additional ACP wire method. - -```mermaid -sequenceDiagram - participant Client - participant Bridge - participant Agent - - Note over Bridge: "Agent supports HTTP but not native ACP MCP" - Client->>Bridge: "session/new with an ACP MCP declaration" - Bridge->>Agent: "session/new with an HTTP MCP endpoint" - - Agent->>Bridge: "HTTP POST MCP initialize" - Bridge->>Client: "mcp/connect with serverId" - Client-->>Bridge: "connectionId" - Bridge->>Client: "mcp/message wrapping MCP initialize" - Client-->>Bridge: MCP initialize result - Bridge-->>Agent: "MCP initialize result and MCP-Session-Id" - - Agent->>Bridge: "HTTP POST tools/call with MCP-Session-Id" - Bridge->>Client: "mcp/message with connectionId" - Client-->>Bridge: tool result - Bridge-->>Agent: MCP response - - Agent->>Bridge: "HTTP DELETE with MCP-Session-Id" - Bridge->>Client: "mcp/disconnect with connectionId" - Client-->>Bridge: empty result - Bridge-->>Agent: session closed +```json +{ + "jsonrpc": "2.0", + "method": "$/cancel_request", + "params": { + "requestId": 20 + } +} ``` -The [ACP Rust SDK](https://github.com/agentclientprotocol/rust-sdk) provides native MCP server attachment independently of proxy chains. Its `agent-client-protocol-polyfill` crate provides the explicit HTTP adapter described above; stdio adaptation remains a possible alternative, not a prerequisite for this proposal. - -## Frequently asked questions - -> What questions have arisen over the course of authoring this document or during subsequent discussions? - -### Why use a separate `serverId` instead of server names? - -Server names in `mcpServers` are chosen by whoever adds them to the session, and could potentially collide if multiple components add servers. A provider-generated `serverId` lets each component choose a unique routing identifier independently of its display name. - -This also avoids a potential deadlock: some agents don't return the session ID until after MCP servers have been initialized. Using a provider-generated `serverId` avoids any dependency on agent-provided session identifiers. - -The same field name is used in the declaration and `mcp/connect`. Earlier versions of this draft used `id` and `acpId`, respectively; `serverId` matches the shared schema and distinguishes the server from both an active `connectionId` and the outer JSON-RPC request `id`. Those earlier names are not aliases in the current wire schema. - -### How does this relate to proxy chains? - -MCP-over-ACP is a transport mechanism that works independently of proxy chains. However, proxy chains are a natural use case: a proxy can inject MCP servers into sessions it forwards, handle the tool callbacks, and use the results to enhance its transformations. - -See the [Proxy Chains RFD](./proxy-chains) for details on how MCP-over-ACP enables context-aware tooling. - -### What if the agent doesn't support ACP transport? - -See the [Bridging for agents without native support](#bridging-for-agents-without-native-support) section above. A bridge can transparently translate ACP-transport MCP servers to stdio or HTTP for agents that don't advertise `mcpCapabilities.acp` support. - -### What about security? - -MCP-over-ACP has the same trust model as regular MCP: you're allowing a component to handle tool invocations. The difference is transport, not trust. Components should only add MCP servers from sources they trust, same as with stdio or HTTP transport. - -## Modernization audit: MCP 2026-07-28 - -### Target and non-goals - -The official MCP site identifies [2026-07-28](https://modelcontextprotocol.io/specification/2026-07-28) as the latest published specification, not a future draft. This proposal targets that revision only. Before stabilization, recheck the published revision and pin the chosen target explicitly rather than promising compatibility with an unbounded moving "latest." - -There is no requirement to support legacy initialization, stateful HTTP, the deprecated HTTP+SSE transport, or fallback to an older MCP revision. MCP version selection is separate from ACP version selection: the existing v1/v2 ACP capability shapes do not require supporting two eras of MCP. +This cancellation ID is hop-local. Proxies forward cancellation using their downstream ACP request ID; they do not rewrite the logical MCP `requestId` or tunnel an unrelated hop's cancellation ID. -Stateless does not mean stateless tooling or an absence of open streams. The [base protocol](https://modelcontextprotocol.io/specification/2026-07-28/basic) forbids implicit request context inherited from a connection. Explicit tool arguments, opaque application handles, MRTR retry state, and state scoped to one long-lived request remain possible. +The provider observes cancellation, stops the request's backend work, and answers the original ACP request with a result or cancellation error. Cancelling one operation must not stop sibling requests, subscriptions, the server declaration, or the containing ACP connection. Implementations must settle pending work and stop forwarding late notifications rather than merely delete a routing entry. -### Recommended transport design +Removing a provider/declaration or closing its ACP connection terminates its outstanding work. A failure in one operation is contained to that operation. Unknown servers and invalid or duplicate active request IDs receive errors; malformed notifications do not receive synthetic replies. -Retain the provider-generated `serverId` in the declaration and route ordinary MCP requests directly to that server. Remove `mcp/connect`, `connectionId`, and `mcp/disconnect` from the proposed stabilized wire protocol unless a separate, demonstrated ACP routing need justifies them. A provider being reachable on an ACP connection is not an MCP session. +Bound notification buffering and outstanding work. If an implementation cannot continue a stream safely, fail or cancel that request explicitly instead of silently dropping subscription events or growing an unbounded queue. -The replacement transport needs: +## Interactive tools -1. One independently valid MCP request, including its metadata, addressed to a declared `serverId`. -2. Request-scoped server notifications, followed by one final MCP result or error. -3. Explicit cancellation of one request or subscription without shutting down the server or other work. -4. Routing/cleanup scoped to the containing ACP connection and the lifetime of the server declaration, without carrying implicit MCP capabilities, identity, or authorization between requests. +MCP 2026-07-28 replaces reverse JSON-RPC calls with MRTR. For `tools/call`, `resources/read`, or `prompts/get`, a provider may return `resultType: "input_required"` with input requests and/or opaque `requestState`. That completes the current ACP request. -The exact ACP request/notification schema and request-correlation mechanism remain design work. Reusing an ACP request ID as an MCP ID is not automatically safe: SDK relays can renumber outer JSON-RPC IDs, while MCP cancellation and subscription metadata refer to those IDs inside payloads. Either preserve an explicit logical MCP request identity or specify a complete mapping through every hop. Do not repurpose a connection ID as an unbounded session merely to correlate notifications. +The agent obtains the requested input, then issues a fresh `mcp/message` request for the original operation with `inputResponses` and the exact opaque state. Each retry supplies its own MCP metadata and uses a fresh logical request ID. The transport must not parse retry state, automatically approve an elicitation, or replay side-effecting calls without the agent's policy. -This is a recommendation for the next wire-schema revision, not a claim that the current schema or SDK already implements it. +This flow needs neither a persistent MCP session nor a provider-originated ACP request. -### Protocol changes to incorporate +## Proxying and HTTP adaptation -1. **Initialization and discovery.** No `initialize` / `notifications/initialized`. Servers implement `server/discover`; clients may send ordinary requests without first discovering. Remove connection-opening handshakes and examples. Discovery is a tunneled MCP request, not a replacement ACP setup method. -2. **Per-request context.** Requests carry `io.modelcontextprotocol/protocolVersion` and `io.modelcontextprotocol/clientCapabilities` in their inner `params._meta`. Client identity is recommended, not authentication. Preserve these fields on every request and retry; never cache their meaning in a connection object or substitute outer ACP `_meta`. Return the modern unsupported-version error rather than falling back to legacy MCP. -3. **Results and input.** Results carry `resultType`. MRTR returns `input_required` and retries the original operation with `inputResponses` and any opaque `requestState`, using a fresh request ID. Replace examples of server-initiated RPC callbacks. Support MRTR for `tools/call`, `resources/read`, and `prompts/get`, not arbitrary methods. Preserve ordinary `complete` results and MCP error code/message/data. -4. **Subscriptions.** `subscriptions/listen` is a long-lived request. Its first message is `notifications/subscriptions/acknowledged`; notifications are filtered and tagged with `io.modelcontextprotocol/subscriptionId`. Define acknowledgement ordering, concurrent subscriptions, notification correlation, cancellation, and graceful completion on ACP. Do not implement a general unscoped server event channel. -5. **Request notifications.** Progress and any supported logging notifications belong to their originating request, not a subscription stream. Route them to the correct in-flight operation, stop them on completion/cancellation, and preserve progress tokens. Logging is deprecated and should not be a new dependency of the design. -6. **Cancellation and failure.** HTTP response-stream closure cancels that request. Stdio uses `notifications/cancelled`; broken HTTP streams are not resumable. Specify ACP cancellation rather than treating an entire ACP connection as the request stream. Settle pending work, suppress late messages, and use new request IDs for deliberate retries; do not silently replay side-effecting tool calls. -7. **Tool/resource/prompt catalogs.** Listings must not vary merely because a client uses a different connection. Cacheable results require `ttlMs` and `cacheScope`; deterministic tool order is recommended. Use explicit server identity and authorization scope for distinct offerings. Review registry filtering, list-change subscriptions, cache isolation, and cache-result constructors. Do not add per-connection tool catalogs. -8. **Tool schemas and output.** JSON Schema 2020-12 keywords and arbitrary JSON `structuredContent` are supported, with schema-reference and composition bounds. Preserve schema/output information in typed tool APIs. Opaque envelope forwarding alone is not sufficient evidence that the typed server helpers conform. -9. **Optional extensions.** Tasks are an opt-in `io.modelcontextprotocol/tasks` extension, not the old core task protocol. Preserve extension capability maps and payloads. Do not make a tasks implementation, MCP Apps, or other optional feature a prerequisite for this transport. -10. **Removed/deprecated features.** Removed features include `ping`, `logging/setLevel`, old resource subscription methods, and old completion notifications. Roots, Sampling, and Logging are deprecated. Do not build new transport APIs around these features. The modern elicitation/MRTR flow is the relevant interactive-tool example. +Proxies preserve `serverId`, logical `requestId`, inner payloads, and metadata. Normal ACP forwarding handles outer responses and hop-local cancellation. Providers claim requests for their declared servers; other components forward them normally. There is no conductor MCP connection table or special connect/disconnect routing. -Normative sources: [versioning and discovery](https://modelcontextprotocol.io/specification/2026-07-28/basic/lifecycle), [MRTR](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr), [subscriptions](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/subscriptions), [cancellation](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation), [tools](https://modelcontextprotocol.io/specification/2026-07-28/server/tools), and the [revision changelog](https://modelcontextprotocol.io/specification/2026-07-28/changelog). +An optional adapter may expose a native server to a **modern MCP HTTP client**. HTTP capability alone does not prove that an agent supports this MCP revision. The adapter must not add a legacy fallback. -### HTTP adaptation is a separate conformance surface +The HTTP endpoint can be reused, but every POST represents its own request: -A latest-only [Streamable HTTP](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http) adapter must replace, not extend, the stateful checkpoint: +- Accept a single JSON-RPC request per POST and return JSON or request-scoped SSE. Reject batches and client-sent responses. +- Support subscriptions as long-lived POST response streams. Closing one response stream cancels only its mapped ACP request. +- Return 405 for GET and DELETE. Do not issue session headers or implement SSE resumption. +- Validate protocol-version, method, name, and applicable mirrored tool-parameter headers against the body, including required value decoding. +- Validate supplied Origin headers, bind local listeners to loopback, and enforce access control. A random port is not authentication. +- Allocate independent logical MCP IDs for overlapping HTTP request IDs. Translate MCP ID references at this transport boundary, including subscription IDs, while preserving progress tokens and opaque state. ACP proxies do not perform that translation. -- Accept one JSON-RPC request or notification per POST. Do not forward JSON-RPC batches or client-sent responses as valid modern MCP traffic, even if the underlying ACP JSON-RPC library supports them. -- Return either a single JSON response or request-scoped SSE. Keep subscription streams separate from unrelated progress or tool-call responses. -- Return 405 for GET and DELETE. Do not mint or echo `Mcp-Session-Id`; ignore obsolete session and `Last-Event-ID` headers rather than implementing session/resumption behavior. -- Validate `MCP-Protocol-Version`, `Mcp-Method`, and applicable `Mcp-Name` headers against the body, including the specified Base64 sentinel encoding. Missing/malformed/mismatched required headers produce HTTP 400 with `HeaderMismatch` (`-32020`). Unsupported protocol versions use `-32022`, and unimplemented methods use HTTP 404 with `-32601`. -- Account for recognized tool-parameter headers declared by `x-mcp-header`, their validation/encoding rules, and unknown-header forwarding rules at an HTTP intermediary. These are HTTP requirements, not fields that must be invented in native ACP envelopes. -- Validate `Origin`, bind local adapters to loopback, and define authentication/access control. A random local port is not an authorization boundary. -- Couple HTTP stream closure to cancellation of only the mapped ACP request. Test slow readers, bounded buffering, aborted requests, lost responses, and subscription shutdown. +HTTP is an adapter, not a prerequisite for the native protocol. Its validation and security surface must be tested separately; working native tool calls do not establish HTTP conformance. -The existing polyfill's downstream `http` capability alone does not establish support for the new MCP revision. A latest-only adapter must document that its consuming agent needs a modern MCP HTTP client; it must not advertise compatibility with older agents merely because they advertise HTTP transport support. +## Security -### SDK and schema work +Server IDs and request IDs are routing identifiers, not credentials. Providers bind server ownership and visibility to the supplying ACP component and authorized callers. Self-reported MCP `clientInfo` is not an authentication identity. -The initial audit examined the Rust SDK's pinned `rmcp 2.2.0`, which lacks the modern service model. The available upgrade is tracked in [Rust SDK PR #372](https://github.com/agentclientprotocol/rust-sdk/pull/372): `rmcp 3.4.0` provides discovery, per-request metadata, MRTR, and subscription APIs. Land that dependency migration before building the replacement ACP transport, rather than introducing a temporary MCP implementation. +Keep outer ACP metadata separate from inner MCP metadata. Do not persist runtime credentials from rewritten HTTP declarations in traces. -The upgrade includes adapter tests for real MCP 2026-07-28 tool calls without initialization, discovery, per-request version validation, required cache metadata, and MRTR elicitation with nonempty input responses and fresh metadata. This is evidence for the dependency path, not proof of complete MCP conformance or of a redesigned ACP binding. The public rmcp major-version change also requires the integration crate's next release to be 4.x; the core ACP SDK remains on 2.x. +Servers treat MRTR state as attacker-controlled input. Where it influences authorization or business logic, protect integrity and address principal binding, expiry, replay, and single-use requirements. Intermediaries keep it opaque. -`rmcp 3.4.0` still defaults `ProtocolVersion::LATEST` to `2025-11-25`. The new transport's callers must select 2026-07-28 explicitly and carry its required metadata on every request. Keep MCP-specific types out of the core ACP transport where possible. A raw JSON/byte transport can still carry modern MCP; neither byte streams nor an existing ACP connection inherently violate statelessness. The remaining transport work is request routing, correlation, streaming, cancellation, and removal of implicit session semantics. +Tool lists vary by explicit server identity and authorization scope, not hidden connection state. Preserve required cache metadata and do not share private catalogs across callers. -Implementation work spans: +## Implementation and validation -- Shared ACP schema: replace the unstable connection-oriented MCP envelopes and side/method mappings; specify required/optional/null behavior, cancellation, and request-scoped notifications; regenerate schema and reference documentation. -- Core SDK: direct server routing, request-scoped provider/consumer APIs, transparent modern result/error/metadata handling, bounded notification routing, and cancellation/resource ownership. -- Tool helpers and `rmcp` integration: discovery, per-request capabilities, modern results and MRTR, cache metadata, and a request context that does not require an MCP connection ID. -- Conductor/proxies: route declared servers and logical requests without leaking identifiers or renumbering embedded correlation fields incorrectly. -- HTTP polyfill: replace the session engine with the modern POST/request-stream behavior above; do not retain a second legacy mode. -- Tests, examples, and documentation: replace initialization and reverse-RPC happy paths with modern discovery, direct tool calls, MRTR, subscriptions, and cancellation. +The Rust implementation uses the shared schema's unstable `MessageMcpRequest`, `MessageMcpResponse`, `MessageMcpNotification`, and `McpRequestId` types for both ACP versions. A provider creates request-scoped backend work; it may reuse immutable tool definitions or application state, but does not establish MCP request context through a session. -### Security and resource lifetime +The [rmcp 3.4 upgrade](https://github.com/agentclientprotocol/rust-sdk/pull/372) is landed. It supplies modern discovery, MRTR, and subscription APIs, although its default protocol-version constant still selects 2025-11-25. Tests and clients for this binding explicitly select 2026-07-28. -The modern design needs more than the earlier statement that transport does not change trust: +Acceptance tests must demonstrate: -- Bind `serverId` ownership and visibility to the providing ACP component and authorized callers. Neither server IDs nor self-reported MCP `clientInfo` are credentials. -- Keep outer ACP metadata separate from inner MCP request metadata, and preserve tracing metadata without logging sensitive inputs or opaque retry state by default. -- Treat MRTR `requestState` as opaque in intermediaries and attacker-controlled at the server. Servers must integrity-protect it when it influences authorization or business logic, and address expiry, principal binding, replay, and single-use requirements where applicable. -- Define cancellation, backpressure, limits on outstanding requests/subscriptions, and provider/declaration removal. A request-scoped resource must not remain alive because the containing ACP transport is long-lived. -- Do not translate an input-required result into automatic user approval or an unbounded retry loop. The agent retains responsibility for capability checks, consent, and tool-execution policy. +- Real discovery and tool calls without MCP initialization, directly and through a proxy. +- Stable logical IDs when ACP outer IDs are renumbered. +- Independent concurrent requests, request-specific metadata/errors, and rejection of duplicate active IDs. +- MRTR with nonempty input responses, opaque state, and fresh retry IDs. +- Subscription acknowledgement ordering, filtered notifications, correlation, and graceful completion. +- Cancellation during tool calls and subscriptions, provider removal, late-message rejection, and resource cleanup. +- HTTP request isolation, header/Origin/access checks, request-close cancellation, and rejection of removed transport behavior. -### What remains useful from the implementation checkpoint +These tests verify the binding, not every optional MCP feature. Tasks, Apps, and other extensions remain opt-in. The new design should not depend on deprecated Roots, Sampling, or Logging features. -Provider-generated IDs, shared-schema naming, explicit capability propagation, error/metadata preservation, and the investigation into pending-request cleanup remain useful. Tests proving request isolation and cleanup should be recast around requests and subscriptions. +### Prototype limits and remaining work -The stateful HTTP engine, connect/disconnect wire lifecycle, per-MCP-connection context, and native consumer API that exposes that lifecycle are **not** compatibility commitments. They may be removed or replaced. The checkpoint is not ready for release: its aborted HTTP initialization can leave a session alive, and pending-work teardown coverage is incomplete. +The current HTTP adapter bounds each response queue to 16 notifications and 256 KiB, with a separate terminal path, 64 active operations, and 32 listeners per adapter. Native providers admit 64 active operations per declaration and check a 16 MiB serialized payload limit. These are implementation policies, not fixed wire-protocol constants. -### Delivery order and acceptance criteria +The SDK's public native `Channel` and outgoing queues remain unbounded. Admission and per-message checks do not establish end-to-end native backpressure; a bounded transport path remains required before stabilization. The prototype must not be described as fully resource-bounded. -1. **Land the modern dependency path:** [Rust SDK PR #372](https://github.com/agentclientprotocol/rust-sdk/pull/372) upgrades rmcp and proves real discovery, tool calls without initialization, per-request metadata, and MRTR through the adapter. Keep its tests as the baseline, not a claim of full transport conformance. -2. **Settle the transport contract:** direct server routing, logical MCP request identity, notification correlation, cancellation, and declaration lifetime. Decide the corresponding unstable schema changes before adding another consumer API. -3. **Implement the native transport:** request-scoped tools, MRTR, subscriptions, errors, and cancellation; then add an example with a direct ACP client/agent pair. -4. **Implement optional HTTP adaptation:** full modern header, security, POST/SSE, and request-close behavior. It need not block a native-only first implementation. -5. **Run a conformance matrix:** two independent callers with overlapping local IDs; different per-request capabilities without inherited state; discovery without setup; exact MRTR state round-trips and new retry IDs; concurrent filtered subscriptions and acknowledgement ordering; request-specific progress; cancellation during a pending tool call and subscription; provider loss; late-message rejection; opaque metadata/errors/results; catalog/cache isolation; HTTP malformed headers, forbidden origins, unsupported methods, batch rejection, and broken streams. +HTTP tools with `x-mcp-header` annotations currently fail closed instead of being supported: listings exclude them and calls reject them. For direct tool calls the adapter looks up the current descriptor internally, so no caller-side tools/list handshake is introduced. Complete mirrored-parameter support and the broader native transport work remain outside the verified slice. -The detailed cancellation and subscription pages contain wording that needs care when specifying server-initiated subscription termination (successful completion versus a cancellation notification). Resolve that mapping explicitly in this transport rather than copying an example of arbitrary reverse requests. The pinned schema and detailed normative rules should take precedence over overview prose that still mentions initialization. +## References and revision history -## Revision history +Normative MCP references: [2026-07-28 specification](https://modelcontextprotocol.io/specification/2026-07-28), [versioning](https://modelcontextprotocol.io/specification/2026-07-28/basic/lifecycle), [MRTR](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr), [subscriptions](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/subscriptions), [cancellation](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation), and [Streamable HTTP](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http). -- Split from proxy-chains RFD to enable independent use of MCP-over-ACP transport by any ACP component, not just proxies. -- Aligned declaration and connect identifiers with the shared schema's `serverId`, corrected session setup and v1/v2 capability examples, and documented the explicit HTTP polyfill architecture. -- Clarified readiness during session setup, independent connection lifetimes, disconnect acknowledgement, and the distinction between a shared HTTP listener and independent MCP sessions. -- Set the stabilization target to MCP 2026-07-28 only and added a modernization audit. Retained the earlier connection-oriented design as a draft implementation checkpoint, not a backwards-compatibility requirement. +This proposal was split from the [proxy-chain RFD](./proxy-chains). Earlier drafts used server `id`/`acpId`, an MCP connection ID, connect/disconnect methods, and reverse requests. Those drafts and the stateful implementation checkpoint are not compatibility commitments. This revision replaces them with server-addressed requests and request-scoped notifications targeting MCP 2026-07-28 only. diff --git a/docs/rfds/proxy-chains.mdx b/docs/rfds/proxy-chains.mdx index ea53b469e..0efa07fc5 100644 --- a/docs/rfds/proxy-chains.mdx +++ b/docs/rfds/proxy-chains.mdx @@ -253,11 +253,11 @@ Note: A conductor can be configured to run in either terminal mode (expecting `i ### MCP-over-ACP support -The [MCP-over-ACP modernization audit](./mcp-over-acp#modernization-audit-mcp-2026-07-28) targets stateless MCP 2026-07-28 only. The connection-oriented implementation described here is a draft checkpoint; its connect/disconnect lifecycle is not a compatibility requirement for the stabilized transport. +The [MCP-over-ACP transport](./mcp-over-acp) targets stateless MCP 2026-07-28 only. Requests target a declared `serverId`; a separate logical MCP `requestId` correlates request-scoped notifications and remains unchanged when proxies renumber the outer ACP JSON-RPC ID. There is no MCP connect/disconnect lifecycle. Proxies that provide MCP servers use the [MCP-over-ACP transport](./mcp-over-acp) mechanism. Capability advertising reflects what the downstream chain can consume; the conductor does not unconditionally add MCP-over-ACP support. In the Rust SDK, an explicit `McpOverAcpPolyfill` proxy can be placed immediately before an HTTP-capable agent that lacks native ACP MCP support. -A forwarding proxy preserves downstream MCP capabilities. A bridging proxy may advertise ACP MCP support only when it can adapt to a transport its successor supports. Proxies that publish MCP servers should be prepared to handle `mcp/connect`, `mcp/message`, and `mcp/disconnect` for those servers as soon as their declarations are forwarded, including while session setup is still in progress. See the transport RFD for the v1 and draft-v2 capability shapes. +A forwarding proxy preserves downstream MCP capabilities. A bridging proxy may advertise ACP MCP support only when its successor can consume the target MCP revision over the adapted transport. Proxies that publish MCP servers handle `mcp/message` requests as soon as their declarations are forwarded, including while session setup is still in progress. Ordinary ACP response forwarding and hop-local cancellation apply; logical MCP IDs and payloads are preserved. See the transport RFD for the v1 and draft-v2 capability shapes. ### Message reference @@ -411,7 +411,7 @@ The key advantage is that proxy-based extensions work with any ACP-compatible ag Proxies can provide MCP servers via [MCP-over-ACP transport](./mcp-over-acp), enabling a single proxy to add context, provide tools, and handle callbacks with full awareness of the conversation state. -When the agent supports native ACP MCP transport, no adapter is needed. Otherwise, the chain can include an explicit adapter to a transport the agent does support. The Rust SDK's HTTP polyfill rewrites MCP declarations to local HTTP endpoints and relays messages to and from the providing proxy's ACP channel. Each logical MCP session retains its own native connection, even when sessions share a listening endpoint. +When the agent supports native ACP MCP transport, no adapter is needed. Otherwise, the chain can include an explicit adapter for a modern MCP HTTP client. The Rust SDK's HTTP polyfill rewrites MCP declarations to local HTTP endpoints and relays requests and request-scoped notifications to and from the providing proxy's ACP channel. Requests may share a listening endpoint without sharing MCP session state. Tool-providing proxies implement MCP-over-ACP without managing those alternative transports themselves. The chain's owner chooses an appropriate adapter, and the resulting advertised capability tells providers whether ACP MCP servers can be consumed. From 9d8b499a332ffa0655007d4be7dd949e05180de3 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 24 Sep 2026 20:57:43 +0200 Subject: [PATCH 05/10] feat(unstable): separate MCP outcomes from ACP failures --- agent-client-protocol-schema/src/lib.rs | 2 + agent-client-protocol-schema/src/mcp.rs | 224 ++++++++++++++++++ .../src/serde_util.rs | 9 +- agent-client-protocol-schema/src/v1/mcp.rs | 27 +-- agent-client-protocol-schema/src/v2/mcp.rs | 24 +- docs/protocol/v1/draft/schema.mdx | 62 ++++- docs/protocol/v2/draft/schema.mdx | 65 ++++- docs/protocol/v2/schema.mdx | 2 +- schema-generator/src/main.rs | 22 ++ schema/v1/schema.unstable.json | 65 ++++- schema/v2/schema.unstable.json | 65 ++++- 11 files changed, 507 insertions(+), 60 deletions(-) create mode 100644 agent-client-protocol-schema/src/mcp.rs diff --git a/agent-client-protocol-schema/src/lib.rs b/agent-client-protocol-schema/src/lib.rs index e15aa1903..117c7a3a4 100644 --- a/agent-client-protocol-schema/src/lib.rs +++ b/agent-client-protocol-schema/src/lib.rs @@ -45,6 +45,8 @@ //! For the complete protocol specification and documentation, visit //! . +#[cfg(feature = "unstable_mcp_over_acp")] +mod mcp; pub mod rpc; mod serde_util; pub mod v1; diff --git a/agent-client-protocol-schema/src/mcp.rs b/agent-client-protocol-schema/src/mcp.rs new file mode 100644 index 000000000..31c2c7ccd --- /dev/null +++ b/agent-client-protocol-schema/src/mcp.rs @@ -0,0 +1,224 @@ +//! Shared unstable MCP-over-ACP response carrier. + +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; +use serde_with::{DefaultOnError, serde_as}; + +use crate::{IntoOption, MaybeUndefined}; + +/// **UNSTABLE** +/// +/// An inner MCP error, distinct from an outer ACP binding or runtime error. +/// +/// `code` and `message` are required and non-null. `data` is optional; +/// explicit `null` is preserved separately from an omitted key. +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[non_exhaustive] +pub struct McpError { + /// Inner MCP error code; never an ACP error code. + pub code: i32, + /// Inner MCP error message. + pub message: String, + /// Optional error data; explicit null is retained. + #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")] + pub data: MaybeUndefined, + /// Additional fields on the inner MCP error object. + #[serde(flatten)] + pub extra: Map, +} + +impl McpError { + /// Construct an inner MCP error without data. + #[must_use] + pub fn new(code: i32, message: impl Into) -> Self { + Self { + code, + message: message.into(), + data: MaybeUndefined::Undefined, + extra: Map::new(), + } + } + + /// Set data, preserving explicit JSON null. + #[must_use] + pub fn data(mut self, data: Value) -> Self { + self.data = if data.is_null() { + MaybeUndefined::Null + } else { + MaybeUndefined::Value(data) + }; + self + } +} + +/// **UNSTABLE** +/// +/// The successful outer ACP `mcp/message` response carries exactly one +/// inner MCP outcome: an opaque result (including JSON null), or an MCP error. +/// Outer ACP errors are reserved for binding and runtime failures. +/// +/// Both branches require their carrier key. An error must be a non-null object. +/// Carrier `_meta` is optional; null is equivalent to omission. +#[serde_as] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(untagged, deny_unknown_fields)] +#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = "mcp/message")))] +#[non_exhaustive] +pub enum MessageMcpResponse { + /// An opaque inner MCP result. + Result { + /// Required, even if JSON null. + result: Value, + /// Optional ACP carrier metadata. + #[serde_as(deserialize_as = "DefaultOnError")] + #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + meta: Option>, + }, + /// A structured inner MCP error. + Error { + /// Required, non-null MCP error object. + error: McpError, + /// Optional ACP carrier metadata. + #[serde_as(deserialize_as = "DefaultOnError")] + #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + meta: Option>, + }, +} + +impl MessageMcpResponse { + /// Wrap any JSON result without interpreting its MCP result type. + #[must_use] + pub fn success(result: Value) -> Self { + Self::Result { result, meta: None } + } + + /// Wrap an inner MCP error in a successful outer ACP response. + #[must_use] + pub fn error(error: McpError) -> Self { + Self::Error { error, meta: None } + } + + /// Attach optional carrier-level ACP metadata. + #[must_use] + pub fn meta(mut self, meta: impl IntoOption>) -> Self { + match &mut self { + Self::Result { meta: field, .. } | Self::Error { meta: field, .. } => { + *field = meta.into_option(); + } + } + self + } +} + +#[cfg(test)] +mod tests { + use serde_json::{Value, json}; + + use super::{McpError, MessageMcpResponse}; + use crate::MaybeUndefined; + + #[test] + fn result_is_opaque_and_present_even_when_null() { + for result in [ + Value::Null, + json!(false), + json!(42), + json!("opaque"), + json!([null, 1]), + json!({"resultType": "future", "unknown": {"value": true}}), + ] { + let response = MessageMcpResponse::success(result.clone()); + let wire = json!({"result": result}); + assert_eq!(serde_json::to_value(&response).unwrap(), wire); + assert_eq!( + serde_json::from_value::(wire).unwrap(), + response + ); + } + } + + #[test] + fn error_round_trips_data_and_extensions_without_acp_translation() { + for data in [ + MaybeUndefined::Undefined, + MaybeUndefined::Null, + MaybeUndefined::Value(json!({"arbitrary": [1, null]})), + ] { + let mut error = McpError::new(-32000, "inner error"); + error.data = data.clone(); + error.extra.insert("future".into(), json!({"key": 1})); + let response = MessageMcpResponse::error(error); + let wire = serde_json::to_value(&response).unwrap(); + assert_eq!(wire["error"]["code"], -32000); + assert_eq!(wire["error"].get("data").is_some(), !data.is_undefined()); + assert_eq!(wire["error"]["future"], json!({"key": 1})); + assert_eq!( + serde_json::from_value::(wire).unwrap(), + response + ); + } + assert_eq!( + McpError::new(1, "x").data(Value::Null).data, + MaybeUndefined::Null + ); + } + + #[test] + fn only_one_non_null_carrier_key_is_valid() { + for wire in [ + Value::Null, + json!({}), + json!({"_meta": null}), + json!({"result": 1, "error": {"code": 1, "message": "x"}}), + json!({"result": 1, "error": null}), + json!({"error": null}), + json!({"error": 1}), + json!({"error": {}}), + json!({"error": {"code": null, "message": "x"}}), + json!({"error": {"code": 1, "message": null}}), + json!({"error": {"code": 1.5, "message": "x"}}), + json!({"unexpected": 1, "result": 1}), + ] { + assert!( + serde_json::from_value::(wire.clone()).is_err(), + "accepted {wire}" + ); + } + } + + #[test] + fn carrier_metadata_is_optional_and_null_means_absent() { + for wire in [ + json!({"result": null, "_meta": null}), + json!({"error": {"code": 1, "message": "x"}, "_meta": null}), + ] { + let parsed: MessageMcpResponse = serde_json::from_value(wire).unwrap(); + assert!(serde_json::to_value(parsed).unwrap().get("_meta").is_none()); + } + let meta = json!({"extension": [null, true]}) + .as_object() + .unwrap() + .clone(); + let response = + MessageMcpResponse::success(json!({"_meta": {"inner": true}})).meta(meta.clone()); + assert_eq!( + serde_json::to_value(response).unwrap(), + json!({"result": {"_meta": {"inner": true}}, "_meta": meta}) + ); + } + + #[cfg(feature = "unstable_protocol_v2")] + #[test] + fn both_versions_export_the_same_types() { + let response: crate::v1::MessageMcpResponse = + crate::v2::MessageMcpResponse::error(crate::v2::McpError::new(-32000, "x")); + assert_eq!( + serde_json::to_value(response).unwrap(), + json!({"error": {"code": -32000, "message": "x"}}) + ); + } +} diff --git a/agent-client-protocol-schema/src/serde_util.rs b/agent-client-protocol-schema/src/serde_util.rs index 407a85607..62aa92866 100644 --- a/agent-client-protocol-schema/src/serde_util.rs +++ b/agent-client-protocol-schema/src/serde_util.rs @@ -347,8 +347,9 @@ mod default_on_null_tests { #[cfg(feature = "unstable_mcp_over_acp")] { - let mcp: v1::MessageMcpResponse = serde_json::from_value(Value::Null).unwrap(); - assert_eq!(serde_json::to_value(mcp).unwrap(), Value::Null); + let mcp: v1::MessageMcpResponse = + serde_json::from_value(json!({"result": null})).unwrap(); + assert_eq!(serde_json::to_value(mcp).unwrap(), json!({"result": null})); } #[cfg(feature = "unstable_protocol_v2")] @@ -359,8 +360,8 @@ mod default_on_null_tests { #[cfg(feature = "unstable_mcp_over_acp")] { let mcp: crate::v2::MessageMcpResponse = - serde_json::from_value(Value::Null).unwrap(); - assert_eq!(serde_json::to_value(mcp).unwrap(), Value::Null); + serde_json::from_value(json!({"result": null})).unwrap(); + assert_eq!(serde_json::to_value(mcp).unwrap(), json!({"result": null})); } } } diff --git a/agent-client-protocol-schema/src/v1/mcp.rs b/agent-client-protocol-schema/src/v1/mcp.rs index c4db25076..f721681f5 100644 --- a/agent-client-protocol-schema/src/v1/mcp.rs +++ b/agent-client-protocol-schema/src/v1/mcp.rs @@ -4,13 +4,14 @@ use std::sync::Arc; use derive_more::{Display, From}; use serde::{Deserialize, Serialize}; -use serde_json::value::RawValue; use serde_with::{DefaultOnError, serde_as, skip_serializing_none}; use crate::IntoOption; use super::{McpServerAcpId, Meta}; +pub use crate::mcp::{McpError, MessageMcpResponse}; + /// **UNSTABLE** /// /// This capability is not part of the spec yet, and may be removed or changed at any point. @@ -191,29 +192,5 @@ impl MessageMcpNotification { } } -/// **UNSTABLE** -/// -/// This capability is not part of the spec yet, and may be removed or changed at any point. -/// -/// Response to `mcp/message`. -/// -/// This is the inner MCP response result payload. Any JSON value is valid. -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Debug, Clone, Serialize, Deserialize, From)] -#[serde(transparent)] -#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = MCP_MESSAGE_METHOD_NAME)))] -#[non_exhaustive] -pub struct MessageMcpResponse( - #[cfg_attr(feature = "schemars", schemars(with = "serde_json::Value"))] pub Arc, -); - -impl MessageMcpResponse { - /// Builds [`MessageMcpResponse`] with the required response fields set; optional fields start unset or empty. - #[must_use] - pub fn new(result: Arc) -> Self { - Self(result) - } -} - /// Method name for exchanging MCP-over-ACP messages. pub(crate) const MCP_MESSAGE_METHOD_NAME: &str = "mcp/message"; diff --git a/agent-client-protocol-schema/src/v2/mcp.rs b/agent-client-protocol-schema/src/v2/mcp.rs index 0a8f703cc..7f774634a 100644 --- a/agent-client-protocol-schema/src/v2/mcp.rs +++ b/agent-client-protocol-schema/src/v2/mcp.rs @@ -4,13 +4,14 @@ use std::sync::Arc; use derive_more::{Display, From}; use serde::{Deserialize, Serialize}; -use serde_json::value::RawValue; use serde_with::{DefaultOnError, serde_as, skip_serializing_none}; use crate::IntoOption; use super::{McpServerAcpId, Meta}; +pub use crate::mcp::{McpError, MessageMcpResponse}; + /// **UNSTABLE** /// /// Identifies an inner MCP request active against a server on this ACP connection. @@ -158,26 +159,5 @@ impl MessageMcpNotification { } } -/// **UNSTABLE** -/// -/// Response to `mcp/message`: transparent inner MCP result. MCP errors use -/// the outer ACP JSON-RPC error envelope. -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Debug, Clone, Serialize, Deserialize, From)] -#[serde(transparent)] -#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = MCP_MESSAGE_METHOD_NAME)))] -#[non_exhaustive] -pub struct MessageMcpResponse( - #[cfg_attr(feature = "schemars", schemars(with = "serde_json::Value"))] pub Arc, -); - -impl MessageMcpResponse { - /// Builds [`MessageMcpResponse`] with the result payload. - #[must_use] - pub fn new(result: Arc) -> Self { - Self(result) - } -} - /// Method name for exchanging MCP-over-ACP messages. pub(crate) const MCP_MESSAGE_METHOD_NAME: &str = "mcp/message"; diff --git a/docs/protocol/v1/draft/schema.mdx b/docs/protocol/v1/draft/schema.mdx index d86600e76..6af33a595 100644 --- a/docs/protocol/v1/draft/schema.mdx +++ b/docs/protocol/v1/draft/schema.mdx @@ -2104,11 +2104,44 @@ If omitted or set to `null`, the inner MCP message has no params. **UNSTABLE** -This capability is not part of the spec yet, and may be removed or changed at any point. +The successful outer ACP `mcp/message` response carries exactly one +inner MCP outcome: an opaque result (including JSON null), or an MCP error. +Outer ACP errors are reserved for binding and runtime failures. + +Both branches require their carrier key. An error must be a non-null object. +Carrier `_meta` is optional; null is equivalent to omission. + +**Type:** Union + + +An opaque inner MCP result. + + + + + Optional ACP carrier metadata. + + + Required, even if JSON null. + + + + -Response to `mcp/message`. + +A structured inner MCP error. + + + + + Optional ACP carrier metadata. + +McpError} required> + Required, non-null MCP error object. + -This is the inner MCP response result payload. Any JSON value is valid. + + ### session/request_permission @@ -4837,6 +4870,29 @@ Agent supports `McpServer::Acp`. +## McpError + +**UNSTABLE** + +An inner MCP error, distinct from an outer ACP binding or runtime error. + +`code` and `message` are required and non-null. `data` is optional; +explicit `null` is preserved separately from an omitted key. + +**Type:** Object + +**Properties:** + + + Inner MCP error code; never an ACP error code. + + + Optional error data; explicit null is retained. + + + Inner MCP error message. + + ## McpRequestId **UNSTABLE** diff --git a/docs/protocol/v2/draft/schema.mdx b/docs/protocol/v2/draft/schema.mdx index 8e19c60fa..97793fd25 100644 --- a/docs/protocol/v2/draft/schema.mdx +++ b/docs/protocol/v2/draft/schema.mdx @@ -1476,7 +1476,7 @@ extensions. Unknown values that do not begin with `_` are reserved for future ACP variants. - + Raw value payload for the custom or future value type. @@ -1890,8 +1890,44 @@ Request parameters for `mcp/message`, sent from consumer to provider. **UNSTABLE** -Response to `mcp/message`: transparent inner MCP result. MCP errors use -the outer ACP JSON-RPC error envelope. +The successful outer ACP `mcp/message` response carries exactly one +inner MCP outcome: an opaque result (including JSON null), or an MCP error. +Outer ACP errors are reserved for binding and runtime failures. + +Both branches require their carrier key. An error must be a non-null object. +Carrier `_meta` is optional; null is equivalent to omission. + +**Type:** Union + + +An opaque inner MCP result. + + + + + Optional ACP carrier metadata. + + + Required, even if JSON null. + + + + + + +A structured inner MCP error. + + + + + Optional ACP carrier metadata. + +McpError} required> + Required, non-null MCP error object. + + + + ### session/request_permission @@ -4804,6 +4840,29 @@ Supplying `\{\}` means the agent supports stdio MCP server transports. +## McpError + +**UNSTABLE** + +An inner MCP error, distinct from an outer ACP binding or runtime error. + +`code` and `message` are required and non-null. `data` is optional; +explicit `null` is preserved separately from an omitted key. + +**Type:** Object + +**Properties:** + + + Inner MCP error code; never an ACP error code. + + + Optional error data; explicit null is retained. + + + Inner MCP error message. + + ## McpHttpCapabilities Capabilities for HTTP MCP server transports. diff --git a/docs/protocol/v2/schema.mdx b/docs/protocol/v2/schema.mdx index 0823e0f1a..3460afe40 100644 --- a/docs/protocol/v2/schema.mdx +++ b/docs/protocol/v2/schema.mdx @@ -737,7 +737,7 @@ extensions. Unknown values that do not begin with `_` are reserved for future ACP variants. - + Raw value payload for the custom or future value type. diff --git a/schema-generator/src/main.rs b/schema-generator/src/main.rs index aaa4c4385..b15ca1c50 100644 --- a/schema-generator/src/main.rs +++ b/schema-generator/src/main.rs @@ -1648,6 +1648,15 @@ starting with '$/' it is free to ignore the notification." } fn get_type_string(schema: &Value) -> String { + // An unconstrained JSON Schema (possibly with only a description) + // accepts every JSON value, not only objects. + if schema + .as_object() + .is_some_and(|fields| fields.keys().all(|key| key == "description")) + { + return "\"any\"".to_string(); + } + // Check for $ref if let Some(ref_val) = schema.get("$ref").and_then(|v| v.as_str()) { let type_name = ref_val.strip_prefix("#/$defs/").unwrap_or(ref_val); @@ -2110,6 +2119,19 @@ starting with '$/' it is free to ignore the notification." use super::MarkdownGenerator; use serde_json::json; + #[test] + fn unconstrained_json_schema_renders_any_value() { + assert_eq!(MarkdownGenerator::get_type_string(&json!({})), "\"any\""); + assert_eq!( + MarkdownGenerator::get_type_string(&json!({"description": "Opaque MCP result"})), + "\"any\"" + ); + assert_eq!( + MarkdownGenerator::get_type_string(&json!({"type": "object"})), + "\"object\"" + ); + } + #[test] fn document_union_includes_shared_properties() { let mut generator = MarkdownGenerator::new("schema.json"); diff --git a/schema/v1/schema.unstable.json b/schema/v1/schema.unstable.json index e6493ac1e..3561b2f96 100644 --- a/schema/v1/schema.unstable.json +++ b/schema/v1/schema.unstable.json @@ -8317,10 +8317,73 @@ } }, "MessageMcpResponse": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nResponse to `mcp/message`.\n\nThis is the inner MCP response result payload. Any JSON value is valid.", + "description": "**UNSTABLE**\n\nThe successful outer ACP `mcp/message` response carries exactly one\ninner MCP outcome: an opaque result (including JSON null), or an MCP error.\nOuter ACP errors are reserved for binding and runtime failures.\n\nBoth branches require their carrier key. An error must be a non-null object.\nCarrier `_meta` is optional; null is equivalent to omission.", + "anyOf": [ + { + "title": "Result", + "description": "An opaque inner MCP result.", + "type": "object", + "properties": { + "result": { + "description": "Required, even if JSON null." + }, + "_meta": { + "description": "Optional ACP carrier metadata.", + "type": ["object", "null"], + "x-deserialize-default-on-error": true, + "additionalProperties": true + } + }, + "required": ["result"], + "additionalProperties": false + }, + { + "title": "Error", + "description": "A structured inner MCP error.", + "type": "object", + "properties": { + "error": { + "description": "Required, non-null MCP error object.", + "allOf": [ + { + "$ref": "#/$defs/McpError" + } + ] + }, + "_meta": { + "description": "Optional ACP carrier metadata.", + "type": ["object", "null"], + "x-deserialize-default-on-error": true, + "additionalProperties": true + } + }, + "required": ["error"], + "additionalProperties": false + } + ], "x-side": "client", "x-method": "mcp/message" }, + "McpError": { + "description": "**UNSTABLE**\n\nAn inner MCP error, distinct from an outer ACP binding or runtime error.\n\n`code` and `message` are required and non-null. `data` is optional;\nexplicit `null` is preserved separately from an omitted key.", + "type": "object", + "properties": { + "code": { + "description": "Inner MCP error code; never an ACP error code.", + "type": "integer", + "format": "int32" + }, + "message": { + "description": "Inner MCP error message.", + "type": "string" + }, + "data": { + "description": "Optional error data; explicit null is retained." + } + }, + "required": ["code", "message"], + "additionalProperties": true + }, "ClientNotification": { "description": "A JSON-RPC notification object.", "type": "object", diff --git a/schema/v2/schema.unstable.json b/schema/v2/schema.unstable.json index 18dffc072..3f3afa744 100644 --- a/schema/v2/schema.unstable.json +++ b/schema/v2/schema.unstable.json @@ -9521,10 +9521,73 @@ } }, "MessageMcpResponse": { - "description": "**UNSTABLE**\n\nResponse to `mcp/message`: transparent inner MCP result. MCP errors use\nthe outer ACP JSON-RPC error envelope.", + "description": "**UNSTABLE**\n\nThe successful outer ACP `mcp/message` response carries exactly one\ninner MCP outcome: an opaque result (including JSON null), or an MCP error.\nOuter ACP errors are reserved for binding and runtime failures.\n\nBoth branches require their carrier key. An error must be a non-null object.\nCarrier `_meta` is optional; null is equivalent to omission.", + "anyOf": [ + { + "title": "Result", + "description": "An opaque inner MCP result.", + "type": "object", + "properties": { + "result": { + "description": "Required, even if JSON null." + }, + "_meta": { + "description": "Optional ACP carrier metadata.", + "type": ["object", "null"], + "x-deserialize-default-on-error": true, + "additionalProperties": true + } + }, + "required": ["result"], + "additionalProperties": false + }, + { + "title": "Error", + "description": "A structured inner MCP error.", + "type": "object", + "properties": { + "error": { + "description": "Required, non-null MCP error object.", + "allOf": [ + { + "$ref": "#/$defs/McpError" + } + ] + }, + "_meta": { + "description": "Optional ACP carrier metadata.", + "type": ["object", "null"], + "x-deserialize-default-on-error": true, + "additionalProperties": true + } + }, + "required": ["error"], + "additionalProperties": false + } + ], "x-side": "client", "x-method": "mcp/message" }, + "McpError": { + "description": "**UNSTABLE**\n\nAn inner MCP error, distinct from an outer ACP binding or runtime error.\n\n`code` and `message` are required and non-null. `data` is optional;\nexplicit `null` is preserved separately from an omitted key.", + "type": "object", + "properties": { + "code": { + "description": "Inner MCP error code; never an ACP error code.", + "type": "integer", + "format": "int32" + }, + "message": { + "description": "Inner MCP error message.", + "type": "string" + }, + "data": { + "description": "Optional error data; explicit null is retained." + } + }, + "required": ["code", "message"], + "additionalProperties": true + }, "ClientNotification": { "description": "A JSON-RPC notification object.", "type": "object", From 62905ef04b69eda39fc73e4ec654a57589aa3635 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 24 Sep 2026 21:31:36 +0200 Subject: [PATCH 06/10] docs(rfd): define owned stateless MCP operations and error domains --- docs/rfds/mcp-over-acp.mdx | 100 ++++++++++++++++++++++++++++--------- 1 file changed, 77 insertions(+), 23 deletions(-) diff --git a/docs/rfds/mcp-over-acp.mdx b/docs/rfds/mcp-over-acp.mdx index cddab64c4..0d1505dcd 100644 --- a/docs/rfds/mcp-over-acp.mdx +++ b/docs/rfds/mcp-over-acp.mdx @@ -26,6 +26,8 @@ There are three identities, with different purposes: Every operation carries its own MCP version, client capabilities, and other request context. Nothing about a previous request establishes that context for the next one. A subscription may keep one request alive; that is not a session for unrelated calls. +Stateless MCP does not require stateless application services. Providers may reuse tool implementations, database pools, caches, and authorization services. Execution and notification permissions belong to each operation, while application state may outlive it. Discovery, listing, and execution must use that operation's explicit caller context, not the identity or capabilities of an earlier request. + The binding uses one method name, `mcp/message`, in two directions: - An **agent-to-provider request** invokes one MCP operation and eventually receives one result or error. @@ -50,10 +52,14 @@ The client checks the agent's ACP MCP capability, then includes a declaration in } ``` -The provider generates the opaque `serverId`. Different servers visible on the same ACP connection must have different IDs. The same server may be offered to multiple ACP sessions; distinct offerings should use distinct server IDs rather than hidden per-MCP-connection catalogs. +The provider generates the opaque `serverId`. A server ID identifies one registration for the lifetime of the ACP connection and must not be rebound to a different registration, including after removal. The same registration may be offered to multiple ACP sessions; distinct offerings use distinct server IDs rather than hidden per-MCP-connection catalogs. The provider must be ready to serve requests when it publishes the declaration. An agent may invoke tools or discover the server before returning the ACP session ID. +Only the owning provider claims requests for its registered IDs. Intermediaries without a matching registration forward the request. If no provider can resolve the ID, the final recipient returns the binding's server-unavailable error. + +A declaration is a reference to a registration, not a remote allocation request. This RFD adds no server-update or unadvertisement method. The provider owns the registration's local lifetime: releasing it rejects future requests and cancels outstanding work. Closing the ACP connection releases all its registrations. Merely omitting a previously declared server from a later setup request does not revoke a registration used by another session. + ### Capability advertising In **ACP v1**, the relevant `InitializeResponse` fragment is: @@ -88,6 +94,8 @@ The v2 field is an optional object: omission or `null` means support is not adve An intermediary only advertises support when its downstream chain can consume this transport. The conductor does not unconditionally add the capability. A proxy that provides no adaptation preserves its successor's capabilities. +Advertising this binding also commits the implementation to request-scoped notifications and `$/cancel_request` forwarding and handling. Cancellation is required for this binding even on ACP versions where general request cancellation is otherwise optional. Support for ordinary tool calls alone is insufficient. + ## Requests and results The agent sends an ACP request with a server ID, a fresh logical MCP request ID, and flattened MCP method/parameters: @@ -122,26 +130,60 @@ The agent sends an ACP request with a server ID, a fresh logical MCP request ID, The provider executes the inner request using `"mcp-request:a11f"` as its MCP JSON-RPC ID. It does not use the outer ACP ID `20`, which may differ on another hop. -The ACP response carries the inner MCP result directly: +The successful ACP response carries exactly one inner MCP outcome. A result is nested under `result`: ```json { "jsonrpc": "2.0", "id": 20, "result": { - "resultType": "complete", - "content": [ - { - "type": "text", - "text": "hello" - } - ], - "isError": false + "result": { + "resultType": "complete", + "content": [ + { + "type": "text", + "text": "hello" + } + ], + "isError": false + } } } ``` -An inner MCP protocol error uses the outer ACP error response, preserving its code, message, and optional data. An MCP tool-execution error remains a tool result with `isError`, not an ACP transport error. +An inner MCP protocol error is also a successful **outer ACP response**, using the carrier's `error` branch: + +```json +{ + "jsonrpc": "2.0", + "id": 20, + "result": { + "error": { + "code": -32602, + "message": "Unknown tool", + "data": { "name": "echo" } + } + } +} +``` + +This preserves error-domain provenance. An inner error code is an MCP code; it must not be interpreted as an ACP error, even if the numerical codes collide. For example, an inner `-32000` must not trigger ACP authentication. An MCP tool-execution error remains a result with `isError`, not either kind of protocol error. + +The carrier requires exactly one of `result` or `error`. `result` accepts any JSON value, including explicit `null`. `error` is a non-null object with required integer `code` and string `message`; optional `data` distinguishes omission from explicit `null`. Unknown inner result and error fields are preserved. An optional carrier `_meta` contains ACP metadata and is separate from the inner outcome's metadata; omission and `null` are equivalent. + +### Binding failures + +Outer ACP errors are reserved for failures to admit, route, or execute the binding itself: + +| Code | Meaning | +| -------- | ------------------------------------------------------------------ | +| `-32602` | Malformed ACP envelope or duplicate active `(serverId, requestId)` | +| `-32800` | ACP operation cancelled | +| `-33000` | Binding resource limit exceeded | +| `-33001` | Server registration unavailable | +| `-33002` | Backend or transport failed without a valid MCP outcome | + +These binding-specific codes do not allocate new meanings in MCP's reserved `-32000` through `-32019` range. No implementation may report overload as ACP's authentication-required error. A malformed inner MCP request or an MCP error returned by a backend belongs in the outcome carrier instead. `server/discover` is an ordinary inner MCP request. It is supported but is not a prerequisite for calling a tool. The transport does not silently perform an MCP handshake or substitute ACP capability discovery for MCP server discovery. @@ -155,7 +197,7 @@ The inner `params` field accepts an object or `null` and is optional at the enve An optional outer `_meta` object alongside `serverId` is ACP envelope metadata. Omission and `null` are equivalent. It is distinct from MCP metadata inside the inner parameters or result, which must be preserved. -Providers validate the per-request MCP version and capabilities. Missing or malformed required metadata is an invalid-parameters error. An unsupported version is MCP's `UnsupportedProtocolVersion` error (`-32022`), with the supported and requested versions. There is no fallback to `initialize` or an earlier protocol revision. +Providers validate the per-request MCP version and capabilities. Missing or malformed required inner metadata is an MCP invalid-parameters outcome. An unsupported version is MCP's `UnsupportedProtocolVersion` error (`-32022`), with the supported and requested versions, inside the outcome carrier. This binding's supported set contains only 2026-07-28: a caller without a mutual version surfaces that error rather than falling back to `initialize` or an earlier revision. ## Request-scoped notifications @@ -204,11 +246,13 @@ Use ACP's existing `$/cancel_request` to cancel the **outer ACP request**. For t This cancellation ID is hop-local. Proxies forward cancellation using their downstream ACP request ID; they do not rewrite the logical MCP `requestId` or tunnel an unrelated hop's cancellation ID. -The provider observes cancellation, stops the request's backend work, and answers the original ACP request with a result or cancellation error. Cancelling one operation must not stop sibling requests, subscriptions, the server declaration, or the containing ACP connection. Implementations must settle pending work and stop forwarding late notifications rather than merely delete a routing entry. +The provider observes cancellation, immediately revokes that operation's notification permission, and stops its owned backend work. It answers the original ACP request with a cancellation error, or with the already-completed outcome if completion won the race. Cancelling one operation must not stop sibling requests, subscriptions, the server registration, or the containing ACP connection. -Removing a provider/declaration or closing its ACP connection terminates its outstanding work. A failure in one operation is contained to that operation. Unknown servers and invalid or duplicate active request IDs receive errors; malformed notifications do not receive synthetic replies. +Cancellation is not merely deletion of a routing entry. Execution and cleanup remain supervised; admission permits and the active logical ID stay owned until cleanup finishes. A reusable service remains available for sibling operations. Queued work cancelled before execution must not start later. Cancellation cannot roll back already-performed external side effects, and an application must not detach work while promising request-owned cancellation. -Bound notification buffering and outstanding work. If an implementation cannot continue a stream safely, fail or cancel that request explicitly instead of silently dropping subscription events or growing an unbounded queue. +Releasing a provider registration or closing its ACP connection cancels all work owned by it. Transport EOF initiates cancellation; it must not wait indefinitely for an application future that is itself awaiting the disconnected peer. Unknown servers and invalid or duplicate active request IDs receive errors; malformed notifications do not receive synthetic replies. + +Bound notification buffering, frame sizes, and outstanding work. Retain resource accounting while messages are queued, deferred, serialized, or held in an unread response body, not only while backend execution is active. Cancellation and shutdown must remain possible when data capacity is exhausted. If an implementation cannot continue a stream safely, fail or cancel that request explicitly instead of silently dropping subscription events or growing an unbounded queue. Do not block unrelated dispatch while waiting for a slow consumer. ## Interactive tools @@ -233,6 +277,14 @@ The HTTP endpoint can be reused, but every POST represents its own request: - Validate supplied Origin headers, bind local listeners to loopback, and enforce access control. A random port is not authentication. - Allocate independent logical MCP IDs for overlapping HTTP request IDs. Translate MCP ID references at this transport boundary, including subscription IDs, while preserving progress tokens and opaque state. ACP proxies do not perform that translation. +### Native-tool re-export + +The Rust adapter exposes a **new local HTTP endpoint for native tools**, not a transparent tunnel for another HTTP endpoint's routing or authorization policy. It uses one loopback listener per ACP connection. A non-secret encoding of `serverId` identifies the route, and a connection-specific bearer credential is bound to that server. Credentials never appear in URLs. The provider still decides whether the registration exists and the caller is authorized; retaining an old URL does not resurrect a removed registration. + +This endpoint does not advertise tool-parameter header mirroring. It removes `x-mcp-header` only from actual schema annotation positions in returned tool descriptors, preserving validation keywords, argument names, and example/default data. It rejects supplied `Mcp-Param-*` headers rather than assigning them authority. Standard method/name/version header validation still applies. + +Each `tools/call` goes directly to its native provider without hidden `tools/list` requests. This avoids a descriptor lookup/execution race and does not introduce a discovery prerequisite. Native ACP passthrough preserves descriptors unchanged. A deployment requiring an existing HTTP gateway's parameter-header policy must implement that policy at the new endpoint or decline this re-export; native execution cannot inherit HTTP headers that were never transported. + HTTP is an adapter, not a prerequisite for the native protocol. Its validation and security surface must be tested separately; working native tool calls do not establish HTTP conformance. ## Security @@ -247,7 +299,9 @@ Tool lists vary by explicit server identity and authorization scope, not hidden ## Implementation and validation -The Rust implementation uses the shared schema's unstable `MessageMcpRequest`, `MessageMcpResponse`, `MessageMcpNotification`, and `McpRequestId` types for both ACP versions. A provider creates request-scoped backend work; it may reuse immutable tool definitions or application state, but does not establish MCP request context through a session. +The Rust implementation uses the shared schema's unstable `MessageMcpRequest`, `MessageMcpResponse`, `MessageMcpNotification`, `McpError`, and `McpRequestId` types for both ACP versions. The response and error carrier types are shared across versions. + +The SDK's target API separates a reusable `McpService` from each owned operation. A `McpRequestContext` supplies logical/server identity, validated metadata and capabilities, cancellation, and request-scoped notifications. A backend factory is an explicit adapter for implementations that need per-operation construction, not a requirement imposed by stateless MCP. Integration adapters must supervise any tasks spawned by their underlying library, not assume dropping a wrapper joins detached handlers. The [rmcp 3.4 upgrade](https://github.com/agentclientprotocol/rust-sdk/pull/372) is landed. It supplies modern discovery, MRTR, and subscription APIs, although its default protocol-version constant still selects 2025-11-25. Tests and clients for this binding explicitly select 2026-07-28. @@ -256,20 +310,20 @@ Acceptance tests must demonstrate: - Real discovery and tool calls without MCP initialization, directly and through a proxy. - Stable logical IDs when ACP outer IDs are renumbered. - Independent concurrent requests, request-specific metadata/errors, and rejection of duplicate active IDs. +- Separation of inner MCP errors from outer ACP errors, including colliding codes, explicit null data, and unknown extension fields. - MRTR with nonempty input responses, opaque state, and fresh retry IDs. - Subscription acknowledgement ordering, filtered notifications, correlation, and graceful completion. -- Cancellation during tool calls and subscriptions, provider removal, late-message rejection, and resource cleanup. -- HTTP request isolation, header/Origin/access checks, request-close cancellation, and rejection of removed transport behavior. +- Cancellation of queued and running tools and subscriptions, including noncooperative user futures, registration removal, transport EOF, late-message rejection, and joined resource cleanup. +- Slow readers, bounded pending work, control-plane liveness under saturation, and admission recovery after response-body release. +- HTTP request isolation, header/Origin/access checks, direct annotated native-tool calls without preliminary requests, request-close cancellation, and rejection of removed transport behavior. These tests verify the binding, not every optional MCP feature. Tasks, Apps, and other extensions remain opt-in. The new design should not depend on deprecated Roots, Sampling, or Logging features. -### Prototype limits and remaining work - -The current HTTP adapter bounds each response queue to 16 notifications and 256 KiB, with a separate terminal path, 64 active operations, and 32 listeners per adapter. Native providers admit 64 active operations per declaration and check a 16 MiB serialized payload limit. These are implementation policies, not fixed wire-protocol constants. +### Stabilization gates -The SDK's public native `Channel` and outgoing queues remain unbounded. Admission and per-message checks do not establish end-to-end native backpressure; a bounded transport path remains required before stabilization. The prototype must not be described as fully resource-bounded. +The Rust implementation and this RFD are being revised together. The response carrier is defined in the shared schema; the owned-service, bounded-transport, and HTTP re-export changes require integrated verification before this draft is ready to stabilize. Independent item-count limits are not evidence of end-to-end memory bounds, and dropping a task handle is not evidence that its work stopped. -HTTP tools with `x-mcp-header` annotations currently fail closed instead of being supported: listings exclude them and calls reject them. For direct tool calls the adapter looks up the current descriptor internally, so no caller-side tools/list handshake is introduced. Complete mirrored-parameter support and the broader native transport work remain outside the verified slice. +Concrete buffer sizes and concurrency quotas are implementation policies, not wire-protocol constants. Their behavior must be configurable or documented, and quota failures must preserve the error-domain distinction above. Both ACP v1 and draft v2 must exercise the same binding semantics; this proposal does not otherwise stabilize ACP v2 or optional MCP extensions. ## References and revision history From fbe40169a20594660ad7a0c70d53e6e8efaa595d Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Fri, 25 Sep 2026 11:45:15 +0200 Subject: [PATCH 07/10] docs(rfd): record verified cleanup ownership and release gates --- docs/rfds/mcp-over-acp.mdx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/rfds/mcp-over-acp.mdx b/docs/rfds/mcp-over-acp.mdx index 0d1505dcd..9807f19c0 100644 --- a/docs/rfds/mcp-over-acp.mdx +++ b/docs/rfds/mcp-over-acp.mdx @@ -321,10 +321,12 @@ These tests verify the binding, not every optional MCP feature. Tasks, Apps, and ### Stabilization gates -The Rust implementation and this RFD are being revised together. The response carrier is defined in the shared schema; the owned-service, bounded-transport, and HTTP re-export changes require integrated verification before this draft is ready to stabilize. Independent item-count limits are not evidence of end-to-end memory bounds, and dropping a task handle is not evidence that its work stopped. +The Rust reference implementation exercises the shared response carrier, reusable services, bounded transport queues, and native-tool HTTP re-export together. Its cleanup regression deliberately pauses a tool runner while ACP continues dispatching: cancellation cannot settle or release the logical request ID until the runner drops the tool future. Both mutable and concurrent tools use this rule. Independent item-count limits are not evidence of complete memory bounds, and dropping a task handle is not evidence that its work stopped. Concrete buffer sizes and concurrency quotas are implementation policies, not wire-protocol constants. Their behavior must be configurable or documented, and quota failures must preserve the error-domain distinction above. Both ACP v1 and draft v2 must exercise the same binding semantics; this proposal does not otherwise stabilize ACP v2 or optional MCP extensions. +Custom service adapters are responsible for observing operation cancellation and joining their owned backend work before returning. The binding waits for that completion; it cannot forcibly terminate detached application work. The reference tests establish the covered binding behavior, not full conformance for every MCP feature or readiness to publish packages. The draft schema must be released and dependent SDK major versions coordinated before publication. + ## References and revision history Normative MCP references: [2026-07-28 specification](https://modelcontextprotocol.io/specification/2026-07-28), [versioning](https://modelcontextprotocol.io/specification/2026-07-28/basic/lifecycle), [MRTR](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr), [subscriptions](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/subscriptions), [cancellation](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation), and [Streamable HTTP](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http). From e5c36d2671fd355f983533bc83b5feb7981d25a6 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Fri, 25 Sep 2026 13:09:27 +0200 Subject: [PATCH 08/10] refactor(unstable): separate v1 and v2 MCP outcome schemas --- agent-client-protocol-schema/src/lib.rs | 2 - agent-client-protocol-schema/src/mcp.rs | 224 --------------------- agent-client-protocol-schema/src/v1/mcp.rs | 209 ++++++++++++++++++- agent-client-protocol-schema/src/v2/mcp.rs | 209 ++++++++++++++++++- 4 files changed, 414 insertions(+), 230 deletions(-) delete mode 100644 agent-client-protocol-schema/src/mcp.rs diff --git a/agent-client-protocol-schema/src/lib.rs b/agent-client-protocol-schema/src/lib.rs index 117c7a3a4..e15aa1903 100644 --- a/agent-client-protocol-schema/src/lib.rs +++ b/agent-client-protocol-schema/src/lib.rs @@ -45,8 +45,6 @@ //! For the complete protocol specification and documentation, visit //! . -#[cfg(feature = "unstable_mcp_over_acp")] -mod mcp; pub mod rpc; mod serde_util; pub mod v1; diff --git a/agent-client-protocol-schema/src/mcp.rs b/agent-client-protocol-schema/src/mcp.rs deleted file mode 100644 index 31c2c7ccd..000000000 --- a/agent-client-protocol-schema/src/mcp.rs +++ /dev/null @@ -1,224 +0,0 @@ -//! Shared unstable MCP-over-ACP response carrier. - -use serde::{Deserialize, Serialize}; -use serde_json::{Map, Value}; -use serde_with::{DefaultOnError, serde_as}; - -use crate::{IntoOption, MaybeUndefined}; - -/// **UNSTABLE** -/// -/// An inner MCP error, distinct from an outer ACP binding or runtime error. -/// -/// `code` and `message` are required and non-null. `data` is optional; -/// explicit `null` is preserved separately from an omitted key. -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[non_exhaustive] -pub struct McpError { - /// Inner MCP error code; never an ACP error code. - pub code: i32, - /// Inner MCP error message. - pub message: String, - /// Optional error data; explicit null is retained. - #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")] - pub data: MaybeUndefined, - /// Additional fields on the inner MCP error object. - #[serde(flatten)] - pub extra: Map, -} - -impl McpError { - /// Construct an inner MCP error without data. - #[must_use] - pub fn new(code: i32, message: impl Into) -> Self { - Self { - code, - message: message.into(), - data: MaybeUndefined::Undefined, - extra: Map::new(), - } - } - - /// Set data, preserving explicit JSON null. - #[must_use] - pub fn data(mut self, data: Value) -> Self { - self.data = if data.is_null() { - MaybeUndefined::Null - } else { - MaybeUndefined::Value(data) - }; - self - } -} - -/// **UNSTABLE** -/// -/// The successful outer ACP `mcp/message` response carries exactly one -/// inner MCP outcome: an opaque result (including JSON null), or an MCP error. -/// Outer ACP errors are reserved for binding and runtime failures. -/// -/// Both branches require their carrier key. An error must be a non-null object. -/// Carrier `_meta` is optional; null is equivalent to omission. -#[serde_as] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(untagged, deny_unknown_fields)] -#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = "mcp/message")))] -#[non_exhaustive] -pub enum MessageMcpResponse { - /// An opaque inner MCP result. - Result { - /// Required, even if JSON null. - result: Value, - /// Optional ACP carrier metadata. - #[serde_as(deserialize_as = "DefaultOnError")] - #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] - #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - meta: Option>, - }, - /// A structured inner MCP error. - Error { - /// Required, non-null MCP error object. - error: McpError, - /// Optional ACP carrier metadata. - #[serde_as(deserialize_as = "DefaultOnError")] - #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] - #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - meta: Option>, - }, -} - -impl MessageMcpResponse { - /// Wrap any JSON result without interpreting its MCP result type. - #[must_use] - pub fn success(result: Value) -> Self { - Self::Result { result, meta: None } - } - - /// Wrap an inner MCP error in a successful outer ACP response. - #[must_use] - pub fn error(error: McpError) -> Self { - Self::Error { error, meta: None } - } - - /// Attach optional carrier-level ACP metadata. - #[must_use] - pub fn meta(mut self, meta: impl IntoOption>) -> Self { - match &mut self { - Self::Result { meta: field, .. } | Self::Error { meta: field, .. } => { - *field = meta.into_option(); - } - } - self - } -} - -#[cfg(test)] -mod tests { - use serde_json::{Value, json}; - - use super::{McpError, MessageMcpResponse}; - use crate::MaybeUndefined; - - #[test] - fn result_is_opaque_and_present_even_when_null() { - for result in [ - Value::Null, - json!(false), - json!(42), - json!("opaque"), - json!([null, 1]), - json!({"resultType": "future", "unknown": {"value": true}}), - ] { - let response = MessageMcpResponse::success(result.clone()); - let wire = json!({"result": result}); - assert_eq!(serde_json::to_value(&response).unwrap(), wire); - assert_eq!( - serde_json::from_value::(wire).unwrap(), - response - ); - } - } - - #[test] - fn error_round_trips_data_and_extensions_without_acp_translation() { - for data in [ - MaybeUndefined::Undefined, - MaybeUndefined::Null, - MaybeUndefined::Value(json!({"arbitrary": [1, null]})), - ] { - let mut error = McpError::new(-32000, "inner error"); - error.data = data.clone(); - error.extra.insert("future".into(), json!({"key": 1})); - let response = MessageMcpResponse::error(error); - let wire = serde_json::to_value(&response).unwrap(); - assert_eq!(wire["error"]["code"], -32000); - assert_eq!(wire["error"].get("data").is_some(), !data.is_undefined()); - assert_eq!(wire["error"]["future"], json!({"key": 1})); - assert_eq!( - serde_json::from_value::(wire).unwrap(), - response - ); - } - assert_eq!( - McpError::new(1, "x").data(Value::Null).data, - MaybeUndefined::Null - ); - } - - #[test] - fn only_one_non_null_carrier_key_is_valid() { - for wire in [ - Value::Null, - json!({}), - json!({"_meta": null}), - json!({"result": 1, "error": {"code": 1, "message": "x"}}), - json!({"result": 1, "error": null}), - json!({"error": null}), - json!({"error": 1}), - json!({"error": {}}), - json!({"error": {"code": null, "message": "x"}}), - json!({"error": {"code": 1, "message": null}}), - json!({"error": {"code": 1.5, "message": "x"}}), - json!({"unexpected": 1, "result": 1}), - ] { - assert!( - serde_json::from_value::(wire.clone()).is_err(), - "accepted {wire}" - ); - } - } - - #[test] - fn carrier_metadata_is_optional_and_null_means_absent() { - for wire in [ - json!({"result": null, "_meta": null}), - json!({"error": {"code": 1, "message": "x"}, "_meta": null}), - ] { - let parsed: MessageMcpResponse = serde_json::from_value(wire).unwrap(); - assert!(serde_json::to_value(parsed).unwrap().get("_meta").is_none()); - } - let meta = json!({"extension": [null, true]}) - .as_object() - .unwrap() - .clone(); - let response = - MessageMcpResponse::success(json!({"_meta": {"inner": true}})).meta(meta.clone()); - assert_eq!( - serde_json::to_value(response).unwrap(), - json!({"result": {"_meta": {"inner": true}}, "_meta": meta}) - ); - } - - #[cfg(feature = "unstable_protocol_v2")] - #[test] - fn both_versions_export_the_same_types() { - let response: crate::v1::MessageMcpResponse = - crate::v2::MessageMcpResponse::error(crate::v2::McpError::new(-32000, "x")); - assert_eq!( - serde_json::to_value(response).unwrap(), - json!({"error": {"code": -32000, "message": "x"}}) - ); - } -} diff --git a/agent-client-protocol-schema/src/v1/mcp.rs b/agent-client-protocol-schema/src/v1/mcp.rs index f721681f5..0cbcba67f 100644 --- a/agent-client-protocol-schema/src/v1/mcp.rs +++ b/agent-client-protocol-schema/src/v1/mcp.rs @@ -4,13 +4,120 @@ use std::sync::Arc; use derive_more::{Display, From}; use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; use serde_with::{DefaultOnError, serde_as, skip_serializing_none}; -use crate::IntoOption; +use crate::{IntoOption, MaybeUndefined}; use super::{McpServerAcpId, Meta}; -pub use crate::mcp::{McpError, MessageMcpResponse}; +/// **UNSTABLE** +/// +/// An inner MCP error, distinct from an outer ACP binding or runtime error. +/// +/// `code` and `message` are required and non-null. `data` is optional; +/// explicit `null` is preserved separately from an omitted key. +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[non_exhaustive] +pub struct McpError { + /// Inner MCP error code; never an ACP error code. + pub code: i32, + /// Inner MCP error message. + pub message: String, + /// Optional error data; explicit null is retained. + #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")] + pub data: MaybeUndefined, + /// Additional fields on the inner MCP error object. + #[serde(flatten)] + pub extra: Map, +} + +impl McpError { + /// Construct an inner MCP error without data. + #[must_use] + pub fn new(code: i32, message: impl Into) -> Self { + Self { + code, + message: message.into(), + data: MaybeUndefined::Undefined, + extra: Map::new(), + } + } + + /// Set data, preserving explicit JSON null. + #[must_use] + pub fn data(mut self, data: Value) -> Self { + self.data = if data.is_null() { + MaybeUndefined::Null + } else { + MaybeUndefined::Value(data) + }; + self + } +} + +/// **UNSTABLE** +/// +/// The successful outer ACP `mcp/message` response carries exactly one +/// inner MCP outcome: an opaque result (including JSON null), or an MCP error. +/// Outer ACP errors are reserved for binding and runtime failures. +/// +/// Both branches require their carrier key. An error must be a non-null object. +/// Carrier `_meta` is optional; null is equivalent to omission. +#[serde_as] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(untagged, deny_unknown_fields)] +#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = "mcp/message")))] +#[non_exhaustive] +pub enum MessageMcpResponse { + /// An opaque inner MCP result. + Result { + /// Required, even if JSON null. + result: Value, + /// Optional ACP carrier metadata. + #[serde_as(deserialize_as = "DefaultOnError")] + #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + meta: Option>, + }, + /// A structured inner MCP error. + Error { + /// Required, non-null MCP error object. + error: McpError, + /// Optional ACP carrier metadata. + #[serde_as(deserialize_as = "DefaultOnError")] + #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + meta: Option>, + }, +} + +impl MessageMcpResponse { + /// Wrap any JSON result without interpreting its MCP result type. + #[must_use] + pub fn success(result: Value) -> Self { + Self::Result { result, meta: None } + } + + /// Wrap an inner MCP error in a successful outer ACP response. + #[must_use] + pub fn error(error: McpError) -> Self { + Self::Error { error, meta: None } + } + + /// Attach optional carrier-level ACP metadata. + #[must_use] + pub fn meta(mut self, meta: impl IntoOption>) -> Self { + match &mut self { + Self::Result { meta: field, .. } | Self::Error { meta: field, .. } => { + *field = meta.into_option(); + } + } + self + } +} /// **UNSTABLE** /// @@ -194,3 +301,101 @@ impl MessageMcpNotification { /// Method name for exchanging MCP-over-ACP messages. pub(crate) const MCP_MESSAGE_METHOD_NAME: &str = "mcp/message"; + +#[cfg(test)] +mod tests { + use serde_json::{Value, json}; + + use super::{McpError, MessageMcpResponse}; + use crate::MaybeUndefined; + + #[test] + fn result_is_opaque_and_present_even_when_null() { + for result in [ + Value::Null, + json!(false), + json!(42), + json!("opaque"), + json!([null, 1]), + json!({"resultType": "future", "unknown": {"value": true}}), + ] { + let response = MessageMcpResponse::success(result.clone()); + let wire = json!({"result": result}); + assert_eq!(serde_json::to_value(&response).unwrap(), wire); + assert_eq!( + serde_json::from_value::(wire).unwrap(), + response + ); + } + } + + #[test] + fn error_round_trips_data_and_extensions_without_acp_translation() { + for data in [ + MaybeUndefined::Undefined, + MaybeUndefined::Null, + MaybeUndefined::Value(json!({"arbitrary": [1, null]})), + ] { + let mut error = McpError::new(-32000, "inner error"); + error.data = data.clone(); + error.extra.insert("future".into(), json!({"key": 1})); + let response = MessageMcpResponse::error(error); + let wire = serde_json::to_value(&response).unwrap(); + assert_eq!(wire["error"]["code"], -32000); + assert_eq!(wire["error"].get("data").is_some(), !data.is_undefined()); + assert_eq!(wire["error"]["future"], json!({"key": 1})); + assert_eq!( + serde_json::from_value::(wire).unwrap(), + response + ); + } + assert_eq!( + McpError::new(1, "x").data(Value::Null).data, + MaybeUndefined::Null + ); + } + + #[test] + fn only_one_non_null_carrier_key_is_valid() { + for wire in [ + Value::Null, + json!({}), + json!({"_meta": null}), + json!({"result": 1, "error": {"code": 1, "message": "x"}}), + json!({"result": 1, "error": null}), + json!({"error": null}), + json!({"error": 1}), + json!({"error": {}}), + json!({"error": {"code": null, "message": "x"}}), + json!({"error": {"code": 1, "message": null}}), + json!({"error": {"code": 1.5, "message": "x"}}), + json!({"unexpected": 1, "result": 1}), + ] { + assert!( + serde_json::from_value::(wire.clone()).is_err(), + "accepted {wire}" + ); + } + } + + #[test] + fn carrier_metadata_is_optional_and_null_means_absent() { + for wire in [ + json!({"result": null, "_meta": null}), + json!({"error": {"code": 1, "message": "x"}, "_meta": null}), + ] { + let parsed: MessageMcpResponse = serde_json::from_value(wire).unwrap(); + assert!(serde_json::to_value(parsed).unwrap().get("_meta").is_none()); + } + let meta = json!({"extension": [null, true]}) + .as_object() + .unwrap() + .clone(); + let response = + MessageMcpResponse::success(json!({"_meta": {"inner": true}})).meta(meta.clone()); + assert_eq!( + serde_json::to_value(response).unwrap(), + json!({"result": {"_meta": {"inner": true}}, "_meta": meta}) + ); + } +} diff --git a/agent-client-protocol-schema/src/v2/mcp.rs b/agent-client-protocol-schema/src/v2/mcp.rs index 7f774634a..15987a6a6 100644 --- a/agent-client-protocol-schema/src/v2/mcp.rs +++ b/agent-client-protocol-schema/src/v2/mcp.rs @@ -4,13 +4,120 @@ use std::sync::Arc; use derive_more::{Display, From}; use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; use serde_with::{DefaultOnError, serde_as, skip_serializing_none}; -use crate::IntoOption; +use crate::{IntoOption, MaybeUndefined}; use super::{McpServerAcpId, Meta}; -pub use crate::mcp::{McpError, MessageMcpResponse}; +/// **UNSTABLE** +/// +/// An inner MCP error, distinct from an outer ACP binding or runtime error. +/// +/// `code` and `message` are required and non-null. `data` is optional; +/// explicit `null` is preserved separately from an omitted key. +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[non_exhaustive] +pub struct McpError { + /// Inner MCP error code; never an ACP error code. + pub code: i32, + /// Inner MCP error message. + pub message: String, + /// Optional error data; explicit null is retained. + #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")] + pub data: MaybeUndefined, + /// Additional fields on the inner MCP error object. + #[serde(flatten)] + pub extra: Map, +} + +impl McpError { + /// Construct an inner MCP error without data. + #[must_use] + pub fn new(code: i32, message: impl Into) -> Self { + Self { + code, + message: message.into(), + data: MaybeUndefined::Undefined, + extra: Map::new(), + } + } + + /// Set data, preserving explicit JSON null. + #[must_use] + pub fn data(mut self, data: Value) -> Self { + self.data = if data.is_null() { + MaybeUndefined::Null + } else { + MaybeUndefined::Value(data) + }; + self + } +} + +/// **UNSTABLE** +/// +/// The successful outer ACP `mcp/message` response carries exactly one +/// inner MCP outcome: an opaque result (including JSON null), or an MCP error. +/// Outer ACP errors are reserved for binding and runtime failures. +/// +/// Both branches require their carrier key. An error must be a non-null object. +/// Carrier `_meta` is optional; null is equivalent to omission. +#[serde_as] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(untagged, deny_unknown_fields)] +#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "client", "x-method" = "mcp/message")))] +#[non_exhaustive] +pub enum MessageMcpResponse { + /// An opaque inner MCP result. + Result { + /// Required, even if JSON null. + result: Value, + /// Optional ACP carrier metadata. + #[serde_as(deserialize_as = "DefaultOnError")] + #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + meta: Option>, + }, + /// A structured inner MCP error. + Error { + /// Required, non-null MCP error object. + error: McpError, + /// Optional ACP carrier metadata. + #[serde_as(deserialize_as = "DefaultOnError")] + #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + meta: Option>, + }, +} + +impl MessageMcpResponse { + /// Wrap any JSON result without interpreting its MCP result type. + #[must_use] + pub fn success(result: Value) -> Self { + Self::Result { result, meta: None } + } + + /// Wrap an inner MCP error in a successful outer ACP response. + #[must_use] + pub fn error(error: McpError) -> Self { + Self::Error { error, meta: None } + } + + /// Attach optional carrier-level ACP metadata. + #[must_use] + pub fn meta(mut self, meta: impl IntoOption>) -> Self { + match &mut self { + Self::Result { meta: field, .. } | Self::Error { meta: field, .. } => { + *field = meta.into_option(); + } + } + self + } +} /// **UNSTABLE** /// @@ -161,3 +268,101 @@ impl MessageMcpNotification { /// Method name for exchanging MCP-over-ACP messages. pub(crate) const MCP_MESSAGE_METHOD_NAME: &str = "mcp/message"; + +#[cfg(test)] +mod tests { + use serde_json::{Value, json}; + + use super::{McpError, MessageMcpResponse}; + use crate::MaybeUndefined; + + #[test] + fn result_is_opaque_and_present_even_when_null() { + for result in [ + Value::Null, + json!(false), + json!(42), + json!("opaque"), + json!([null, 1]), + json!({"resultType": "future", "unknown": {"value": true}}), + ] { + let response = MessageMcpResponse::success(result.clone()); + let wire = json!({"result": result}); + assert_eq!(serde_json::to_value(&response).unwrap(), wire); + assert_eq!( + serde_json::from_value::(wire).unwrap(), + response + ); + } + } + + #[test] + fn error_round_trips_data_and_extensions_without_acp_translation() { + for data in [ + MaybeUndefined::Undefined, + MaybeUndefined::Null, + MaybeUndefined::Value(json!({"arbitrary": [1, null]})), + ] { + let mut error = McpError::new(-32000, "inner error"); + error.data = data.clone(); + error.extra.insert("future".into(), json!({"key": 1})); + let response = MessageMcpResponse::error(error); + let wire = serde_json::to_value(&response).unwrap(); + assert_eq!(wire["error"]["code"], -32000); + assert_eq!(wire["error"].get("data").is_some(), !data.is_undefined()); + assert_eq!(wire["error"]["future"], json!({"key": 1})); + assert_eq!( + serde_json::from_value::(wire).unwrap(), + response + ); + } + assert_eq!( + McpError::new(1, "x").data(Value::Null).data, + MaybeUndefined::Null + ); + } + + #[test] + fn only_one_non_null_carrier_key_is_valid() { + for wire in [ + Value::Null, + json!({}), + json!({"_meta": null}), + json!({"result": 1, "error": {"code": 1, "message": "x"}}), + json!({"result": 1, "error": null}), + json!({"error": null}), + json!({"error": 1}), + json!({"error": {}}), + json!({"error": {"code": null, "message": "x"}}), + json!({"error": {"code": 1, "message": null}}), + json!({"error": {"code": 1.5, "message": "x"}}), + json!({"unexpected": 1, "result": 1}), + ] { + assert!( + serde_json::from_value::(wire.clone()).is_err(), + "accepted {wire}" + ); + } + } + + #[test] + fn carrier_metadata_is_optional_and_null_means_absent() { + for wire in [ + json!({"result": null, "_meta": null}), + json!({"error": {"code": 1, "message": "x"}, "_meta": null}), + ] { + let parsed: MessageMcpResponse = serde_json::from_value(wire).unwrap(); + assert!(serde_json::to_value(parsed).unwrap().get("_meta").is_none()); + } + let meta = json!({"extension": [null, true]}) + .as_object() + .unwrap() + .clone(); + let response = + MessageMcpResponse::success(json!({"_meta": {"inner": true}})).meta(meta.clone()); + assert_eq!( + serde_json::to_value(response).unwrap(), + json!({"result": {"_meta": {"inner": true}}, "_meta": meta}) + ); + } +} From a61f3d0d5cafa5e89a0cfa4e8f3c09ade4a69443 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Fri, 25 Sep 2026 13:18:33 +0200 Subject: [PATCH 09/10] docs(rfd): clarify MCP envelope and cancellation choices --- docs/rfds/mcp-over-acp.mdx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/rfds/mcp-over-acp.mdx b/docs/rfds/mcp-over-acp.mdx index 9807f19c0..3978011be 100644 --- a/docs/rfds/mcp-over-acp.mdx +++ b/docs/rfds/mcp-over-acp.mdx @@ -33,6 +33,8 @@ The binding uses one method name, `mcp/message`, in two directions: - An **agent-to-provider request** invokes one MCP operation and eventually receives one result or error. - A **provider-to-agent notification** carries an MCP notification belonging to that active operation. +`mcp/message` is this binding's ACP transport envelope, not an MCP-defined method. JSON-RPC distinguishes the two forms by the outer `id`: requests have one, notifications do not. The inner MCP method is preserved, such as `tools/call` for a request or `notifications/progress` for a notification. Implementations dispatch by JSON-RPC message kind and direction, not the outer method name alone. + There are no provider-originated MCP requests. Interactive tools use MCP's multi round-trip request pattern (MRTR). ## Declaring a server @@ -94,7 +96,9 @@ The v2 field is an optional object: omission or `null` means support is not adve An intermediary only advertises support when its downstream chain can consume this transport. The conductor does not unconditionally add the capability. A proxy that provides no adaptation preserves its successor's capabilities. -Advertising this binding also commits the implementation to request-scoped notifications and `$/cancel_request` forwarding and handling. Cancellation is required for this binding even on ACP versions where general request cancellation is otherwise optional. Support for ordinary tool calls alone is insufficient. +The advertised capability covers this request-scoped transport, including notification delivery and a per-request cancellation path. Agents can cancel their outgoing MCP operations using `$/cancel_request`; providers handle cancellation for those operations, and adapting proxies forward it using the appropriate hop-local request ID. This does not make cancellation mandatory for unrelated ACP methods or require support for every optional MCP feature. + +The cancellation requirement is a choice of this binding, not a claim that MCP universally mandates ACP cancellation. A `subscriptions/listen` request can remain open indefinitely, and an HTTP adapter must translate response-stream closure into cancellation of that request. Without a per-request signal, the caller would have to close the whole ACP connection to release one stream, disrupting unrelated work. [MCP defines cancellation per transport](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation); this binding reuses ACP's existing mechanism rather than adding another cancellation message. Cancellation remains subject to completion races and cannot undo external side effects. ## Requests and results @@ -299,12 +303,10 @@ Tool lists vary by explicit server identity and authorization scope, not hidden ## Implementation and validation -The Rust implementation uses the shared schema's unstable `MessageMcpRequest`, `MessageMcpResponse`, `MessageMcpNotification`, `McpError`, and `McpRequestId` types for both ACP versions. The response and error carrier types are shared across versions. +The Rust implementation uses each ACP version's unstable `MessageMcpRequest`, `MessageMcpResponse`, `MessageMcpNotification`, `McpError`, and `McpRequestId` schema types. The v1 and v2 response and error types are defined independently so either version can evolve without changing the other. Their JSON representation is currently the same. The SDK's target API separates a reusable `McpService` from each owned operation. A `McpRequestContext` supplies logical/server identity, validated metadata and capabilities, cancellation, and request-scoped notifications. A backend factory is an explicit adapter for implementations that need per-operation construction, not a requirement imposed by stateless MCP. Integration adapters must supervise any tasks spawned by their underlying library, not assume dropping a wrapper joins detached handlers. -The [rmcp 3.4 upgrade](https://github.com/agentclientprotocol/rust-sdk/pull/372) is landed. It supplies modern discovery, MRTR, and subscription APIs, although its default protocol-version constant still selects 2025-11-25. Tests and clients for this binding explicitly select 2026-07-28. - Acceptance tests must demonstrate: - Real discovery and tool calls without MCP initialization, directly and through a proxy. @@ -321,7 +323,7 @@ These tests verify the binding, not every optional MCP feature. Tasks, Apps, and ### Stabilization gates -The Rust reference implementation exercises the shared response carrier, reusable services, bounded transport queues, and native-tool HTTP re-export together. Its cleanup regression deliberately pauses a tool runner while ACP continues dispatching: cancellation cannot settle or release the logical request ID until the runner drops the tool future. Both mutable and concurrent tools use this rule. Independent item-count limits are not evidence of complete memory bounds, and dropping a task handle is not evidence that its work stopped. +The Rust reference implementation exercises the versioned response carriers, reusable services, bounded transport queues, and native-tool HTTP re-export together. Its cleanup regression deliberately pauses a tool runner while ACP continues dispatching: cancellation cannot settle or release the logical request ID until the runner drops the tool future. Both mutable and concurrent tools use this rule. Independent item-count limits are not evidence of complete memory bounds, and dropping a task handle is not evidence that its work stopped. Concrete buffer sizes and concurrency quotas are implementation policies, not wire-protocol constants. Their behavior must be configurable or documented, and quota failures must preserve the error-domain distinction above. Both ACP v1 and draft v2 must exercise the same binding semantics; this proposal does not otherwise stabilize ACP v2 or optional MCP extensions. From bae32423f0b7108b2f901769f32d557cfec47efc Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Fri, 25 Sep 2026 13:35:07 +0200 Subject: [PATCH 10/10] docs(rfd): keep MCP cancellation best effort --- docs/rfds/mcp-over-acp.mdx | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/rfds/mcp-over-acp.mdx b/docs/rfds/mcp-over-acp.mdx index 3978011be..f525cbb26 100644 --- a/docs/rfds/mcp-over-acp.mdx +++ b/docs/rfds/mcp-over-acp.mdx @@ -96,9 +96,7 @@ The v2 field is an optional object: omission or `null` means support is not adve An intermediary only advertises support when its downstream chain can consume this transport. The conductor does not unconditionally add the capability. A proxy that provides no adaptation preserves its successor's capabilities. -The advertised capability covers this request-scoped transport, including notification delivery and a per-request cancellation path. Agents can cancel their outgoing MCP operations using `$/cancel_request`; providers handle cancellation for those operations, and adapting proxies forward it using the appropriate hop-local request ID. This does not make cancellation mandatory for unrelated ACP methods or require support for every optional MCP feature. - -The cancellation requirement is a choice of this binding, not a claim that MCP universally mandates ACP cancellation. A `subscriptions/listen` request can remain open indefinitely, and an HTTP adapter must translate response-stream closure into cancellation of that request. Without a per-request signal, the caller would have to close the whole ACP connection to release one stream, disrupting unrelated work. [MCP defines cancellation per transport](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation); this binding reuses ACP's existing mechanism rather than adding another cancellation message. Cancellation remains subject to completion races and cannot undo external side effects. +This capability advertises the MCP transport, not every optional MCP feature or a guarantee that every operation can be cancelled. MCP discovery and per-request capabilities describe the supported features. ## Requests and results @@ -236,7 +234,7 @@ Each subscription notification carries `io.modelcontextprotocol/subscriptionId` ## Cancellation and lifetime -Use ACP's existing `$/cancel_request` to cancel the **outer ACP request**. For the example request above: +Cancellation uses ACP's existing `$/cancel_request` for the **outer ACP request**; this binding adds no separate cancellation method or support requirement. A caller can request cancellation when it no longer needs an operation's result. For the example request above: ```json { @@ -248,15 +246,17 @@ Use ACP's existing `$/cancel_request` to cancel the **outer ACP request**. For t } ``` -This cancellation ID is hop-local. Proxies forward cancellation using their downstream ACP request ID; they do not rewrite the logical MCP `requestId` or tunnel an unrelated hop's cancellation ID. +This cancellation ID is hop-local. When a proxy forwards cancellation, it uses its downstream ACP request ID; it does not rewrite the logical MCP `requestId` or tunnel an unrelated hop's cancellation ID. + +Cancellation is best effort. A provider may complete an operation normally if it cannot cancel it or completion wins the race. When it honors cancellation, it stops producing new notifications for that operation and answers the original ACP request with a cancellation error after cleanup. Cancelling one operation does not cancel sibling requests, subscriptions, the server registration, or the containing ACP connection. -The provider observes cancellation, immediately revokes that operation's notification permission, and stops its owned backend work. It answers the original ACP request with a cancellation error, or with the already-completed outcome if completion won the race. Cancelling one operation must not stop sibling requests, subscriptions, the server registration, or the containing ACP connection. +For long-lived operations such as `subscriptions/listen`, per-request cancellation is useful because it avoids closing the whole ACP connection to stop one stream. An HTTP adapter translates response-stream closure into a cancellation request upstream, following [MCP's transport-specific cancellation rules](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation). -Cancellation is not merely deletion of a routing entry. Execution and cleanup remain supervised; admission permits and the active logical ID stay owned until cleanup finishes. A reusable service remains available for sibling operations. Queued work cancelled before execution must not start later. Cancellation cannot roll back already-performed external side effects, and an application must not detach work while promising request-owned cancellation. +Honoring cancellation is not merely deletion of a routing entry. Execution and cleanup remain supervised; admission permits and the active logical ID stay owned until cleanup finishes. A reusable service remains available for sibling operations. Work reported as cancelled before execution does not start later. Cancellation cannot roll back already-performed external side effects. Releasing a provider registration or closing its ACP connection cancels all work owned by it. Transport EOF initiates cancellation; it must not wait indefinitely for an application future that is itself awaiting the disconnected peer. Unknown servers and invalid or duplicate active request IDs receive errors; malformed notifications do not receive synthetic replies. -Bound notification buffering, frame sizes, and outstanding work. Retain resource accounting while messages are queued, deferred, serialized, or held in an unread response body, not only while backend execution is active. Cancellation and shutdown must remain possible when data capacity is exhausted. If an implementation cannot continue a stream safely, fail or cancel that request explicitly instead of silently dropping subscription events or growing an unbounded queue. Do not block unrelated dispatch while waiting for a slow consumer. +Bound notification buffering, frame sizes, and outstanding work. Retain resource accounting while messages are queued, deferred, serialized, or held in an unread response body, not only while backend execution is active. Keep shutdown and any supported cancellation responsive when data capacity is exhausted. If an implementation cannot continue a stream safely, fail or cancel that request explicitly instead of silently dropping subscription events or growing an unbounded queue. Do not block unrelated dispatch while waiting for a slow consumer. ## Interactive tools @@ -268,14 +268,14 @@ This flow needs neither a persistent MCP session nor a provider-originated ACP r ## Proxying and HTTP adaptation -Proxies preserve `serverId`, logical `requestId`, inner payloads, and metadata. Normal ACP forwarding handles outer responses and hop-local cancellation. Providers claim requests for their declared servers; other components forward them normally. There is no conductor MCP connection table or special connect/disconnect routing. +Proxies preserve `serverId`, logical `requestId`, inner payloads, and metadata. Normal ACP forwarding handles outer responses and, where supported, hop-local cancellation. Providers claim requests for their declared servers; other components forward them normally. There is no conductor MCP connection table or special connect/disconnect routing. An optional adapter may expose a native server to a **modern MCP HTTP client**. HTTP capability alone does not prove that an agent supports this MCP revision. The adapter must not add a legacy fallback. The HTTP endpoint can be reused, but every POST represents its own request: - Accept a single JSON-RPC request per POST and return JSON or request-scoped SSE. Reject batches and client-sent responses. -- Support subscriptions as long-lived POST response streams. Closing one response stream cancels only its mapped ACP request. +- Support subscriptions as long-lived POST response streams. Closing one response stream requests cancellation of only its mapped ACP request and stops delivery on that stream. - Return 405 for GET and DELETE. Do not issue session headers or implement SSE resumption. - Validate protocol-version, method, name, and applicable mirrored tool-parameter headers against the body, including required value decoding. - Validate supplied Origin headers, bind local listeners to loopback, and enforce access control. A random port is not authentication. @@ -307,7 +307,7 @@ The Rust implementation uses each ACP version's unstable `MessageMcpRequest`, `M The SDK's target API separates a reusable `McpService` from each owned operation. A `McpRequestContext` supplies logical/server identity, validated metadata and capabilities, cancellation, and request-scoped notifications. A backend factory is an explicit adapter for implementations that need per-operation construction, not a requirement imposed by stateless MCP. Integration adapters must supervise any tasks spawned by their underlying library, not assume dropping a wrapper joins detached handlers. -Acceptance tests must demonstrate: +Reference implementation tests cover: - Real discovery and tool calls without MCP initialization, directly and through a proxy. - Stable logical IDs when ACP outer IDs are renumbered. @@ -327,7 +327,7 @@ The Rust reference implementation exercises the versioned response carriers, reu Concrete buffer sizes and concurrency quotas are implementation policies, not wire-protocol constants. Their behavior must be configurable or documented, and quota failures must preserve the error-domain distinction above. Both ACP v1 and draft v2 must exercise the same binding semantics; this proposal does not otherwise stabilize ACP v2 or optional MCP extensions. -Custom service adapters are responsible for observing operation cancellation and joining their owned backend work before returning. The binding waits for that completion; it cannot forcibly terminate detached application work. The reference tests establish the covered binding behavior, not full conformance for every MCP feature or readiness to publish packages. The draft schema must be released and dependent SDK major versions coordinated before publication. +The Rust reference implementation supervises cancellation and joins owned backend work before reporting it complete; it cannot forcibly terminate detached application work. These are implementation safeguards, not additional cancellation requirements for advertising the transport. The reference tests establish the covered binding behavior, not full conformance for every MCP feature or readiness to publish packages. The draft schema must be released and dependent SDK major versions coordinated before publication. ## References and revision history