Skip to content

Commit a395b65

Browse files
committed
feat(mcp)!: adopt stateless MCP-over-ACP binding
1 parent 8a99407 commit a395b65

32 files changed

Lines changed: 2726 additions & 1127 deletions

‎docs/experimental-v2.md‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
> **Experimental.** Protocol v2 is a draft. Import it from `acp.experimental` and
44
> expect its API and generated models to change with the upstream schema.
55
6-
The bindings use `schema-v2.0.0-alpha.5`.
6+
The bindings use `schema-v2.0.0-alpha.7`.
77

88
The v2 runtime is separate from the stable v1 API. Like v1, connection methods
99
and agent/client handlers accept expanded, snake-case parameters. Responses and
@@ -155,3 +155,13 @@ by the shared connection layer, as in v1: cancelling the task awaiting a request
155155
sends a best-effort cancellation to the peer, and an incoming cancellation
156156
cancels the handler task, which answers with its result or `-32800`.
157157
`session/cancel` remains available for cancelling active session work.
158+
159+
## Stateless MCP-over-ACP
160+
161+
V2 uses the same [stateless MCP binding](quickstart.md#stateless-mcp-over-acp)
162+
as v1. Use the independent `v2.schema.McpResultResponse`, `McpErrorResponse`,
163+
`McpError`, and `MessageMcpResponse` types. The agent advertises support with
164+
`capabilities.session.mcp.acp = {}`; omission or null does not advertise support.
165+
After ACP initialization, agents send `mcp_message` requests and providers send
166+
`notify_mcp` notifications with explicit `server_id` and `request_id` strings.
167+
The old MCP connect/disconnect methods and reverse requests are removed.

‎docs/quickstart.md‎

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ update = AgentMessageChunk(
159159

160160
## Session notices (unstable)
161161

162-
The v1 bindings use `schema-v1.23.0`. Clients that can display live advisory
162+
The v1 bindings use `schema-v1.24.1`. Clients that can display live advisory
163163
notices advertise `ClientCapabilities(session=ClientSessionCapabilities(notices=NoticeCapabilities()))`
164164
in `initialize`; these models are available from `acp.schema`.
165165

@@ -189,7 +189,7 @@ nested objects are not valid form values.
189189

190190
## Additional v1 protocol methods
191191

192-
The SDK exposes all agent and client methods in the bundled `schema-v1.23.0`
192+
The SDK exposes all agent and client methods in the bundled `schema-v1.24.1`
193193
method tables. `session/delete` and `logout` are stable and do not require
194194
`use_unstable_protocol=True`. The other methods in the table below are unstable;
195195
enable that flag on the receiving connection (or `run_agent`) to route them.
@@ -200,9 +200,8 @@ Check the peer's advertised capabilities before calling these methods.
200200
| `session/delete` | `delete_session(session_id=...)` | Agent |
201201
| `providers/list`, `providers/set`, `providers/disable` | `list_providers`, `set_provider`, `disable_provider` | Agent |
202202
| `logout` | `logout()` | Agent |
203-
| `mcp/connect`, `mcp/disconnect` | `connect_mcp`, `disconnect_mcp` | Client |
204-
| `mcp/message` (request) | `mcp_message` | Either peer |
205-
| `mcp/message` (notification) | `notify_mcp` | Either peer |
203+
| `mcp/message` (request) | `mcp_message` | Client/provider |
204+
| `mcp/message` (notification) | `notify_mcp` | Agent |
206205
| `nes/start`, `nes/suggest`, `nes/close` | `start_nes`, `suggest_nes`, `close_nes` | Agent |
207206
| `nes/accept`, `nes/reject` | `accept_nes`, `reject_nes` | Agent |
208207
| `document/didOpen`, `document/didChange`, `document/didClose`, `document/didSave`, `document/didFocus` | `did_open`, `did_change`, `did_close`, `did_save`, `did_focus` | Agent |
@@ -223,10 +222,70 @@ session. The SDK dispatches these calls to your implementation; it does not
223222
provide session storage or provider management itself. Missing request handlers
224223
return the JSON-RPC method-not-found error.
225224

226-
MCP requests return the inner JSON result unchanged, including `null`. Use
227-
`notify_mcp` for one-way MCP messages. Both APIs accept `connection_id`, `method`,
228-
and optional `params`. These methods share the same connections and routers
229-
across stdio, HTTP, and WebSocket transports.
225+
## Stateless MCP-over-ACP
226+
227+
The unstable native binding targets **MCP 2026-07-28**. A provider advertises an
228+
`AcpMcpServer(name=..., server_id=...)` in session setup after checking the
229+
agent's `mcpCapabilities.acp` capability. Register the provider's handler before
230+
publishing the declaration: the agent may call it before session setup returns.
231+
There is no MCP initialization or connect/disconnect exchange.
232+
233+
An agent calls its connection's `mcp_message`; the client/provider implements
234+
that handler. Supply a fresh string `request_id` for every operation, including
235+
an interactive-tool retry. This logical MCP ID is separate from the outer ACP
236+
JSON-RPC request ID. Include the required MCP metadata in the inner parameters:
237+
238+
```python
239+
from uuid import uuid4
240+
from acp import McpErrorResponse
241+
242+
response = await connection.mcp_message(
243+
server_id="project-tools",
244+
request_id=uuid4().hex,
245+
method="tools/list",
246+
params={
247+
"_meta": {
248+
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
249+
"io.modelcontextprotocol/clientCapabilities": {},
250+
},
251+
},
252+
trace="outer ACP metadata",
253+
)
254+
if isinstance(response, McpErrorResponse):
255+
handle_mcp_error(response.error)
256+
else:
257+
use_mcp_result(response.result)
258+
```
259+
260+
Providers return `McpResultResponse(result=...)` or
261+
`McpErrorResponse(error=McpError(code=..., message=...))`. Results are opaque JSON,
262+
including explicit `null`, tool errors with `isError`, and interactive
263+
`input_required` results. Error data preserves omission versus explicit null,
264+
and unknown inner error fields survive. Outer ACP failures raise `RequestError`;
265+
an inner MCP error, even code `-32000`, remains inside the response carrier.
266+
Send exactly one outcome; receivers prefer `result` if both keys are received.
267+
268+
The provider sends `notify_mcp(server_id=..., request_id=..., method=..., params=...)`
269+
for an active request; the agent implements `notify_mcp` to receive it. Keep the
270+
same server and logical request IDs and stop notifications before sending the
271+
final outcome. Consumers must correlate notifications with active operations
272+
and ignore unknown or completed ones. There are no provider-originated MCP requests.
273+
274+
The SDK provides typed envelopes and routing. Applications own server registrations,
275+
MCP metadata/version validation, duplicate-active-ID rejection, operation admission,
276+
and notification lifetimes. They also decide how to handle interactive input;
277+
retry with fresh IDs and preserve opaque request state. Cancelling the task awaiting
278+
`mcp_message` uses the existing ACP cancellation mechanism described below.
279+
These methods work over stdio, HTTP, and WebSocket ACP connections.
280+
281+
**Migration:** `connect_mcp`, `disconnect_mcp`, and `connection_id` are removed.
282+
Replace them with server-addressed requests carrying `server_id` and `request_id`,
283+
add per-request MCP metadata, and wrap handler outcomes in the typed carrier.
284+
Older MCP revisions and the connection-oriented prototype are not supported.
285+
286+
Run `uv run python examples/mcp_over_acp.py` for a local request/progress example.
287+
See the [upstream binding specification](https://agentclientprotocol.com/rfds/mcp-over-acp)
288+
for registration, subscription, and error-code requirements.
230289

231290
## Request cancellation
232291

‎examples/mcp_over_acp.py‎

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""Run a native MCP request and progress notification over an in-memory ACP connection.
2+
3+
Run with: uv run python examples/mcp_over_acp.py
4+
No MCP initialization, HTTP listener, or external provider is needed.
5+
"""
6+
7+
import asyncio
8+
import logging
9+
from typing import Any
10+
from uuid import uuid4
11+
12+
from acp import Client, McpError, McpErrorResponse, McpResultResponse, MessageMcpResponse, RequestError
13+
from acp._transport import memory_transport_pair
14+
from acp.agent.connection import AgentSideConnection
15+
from acp.client.connection import ClientSideConnection
16+
17+
18+
class EchoProvider:
19+
connection: ClientSideConnection
20+
21+
def on_connect(self, connection: ClientSideConnection) -> None:
22+
self.connection = connection
23+
24+
async def mcp_message(
25+
self, server_id: str, request_id: str, method: str, params: dict[str, Any] | None = None, **kwargs: Any
26+
) -> MessageMcpResponse:
27+
if server_id != "echo":
28+
raise RequestError(-33001, "Server registration unavailable")
29+
params = params or {}
30+
meta = params.get("_meta")
31+
if not isinstance(meta, dict) or not isinstance(meta.get("io.modelcontextprotocol/protocolVersion"), str):
32+
return McpErrorResponse(error=McpError(code=-32602, message="Missing MCP protocol version"))
33+
if meta["io.modelcontextprotocol/protocolVersion"] != "2026-07-28":
34+
return McpErrorResponse(
35+
error=McpError(
36+
code=-32022,
37+
message="Unsupported MCP protocol version",
38+
data={"supported": ["2026-07-28"], "requested": meta["io.modelcontextprotocol/protocolVersion"]},
39+
)
40+
)
41+
if not isinstance(meta.get("io.modelcontextprotocol/clientCapabilities"), dict):
42+
return McpErrorResponse(error=McpError(code=-32602, message="Missing MCP client capabilities"))
43+
if method != "tools/call" or params.get("name") != "echo":
44+
return McpErrorResponse(error=McpError(code=-32601, message="Unknown MCP operation"))
45+
await self.connection.notify_mcp(
46+
server_id=server_id,
47+
request_id=request_id,
48+
method="notifications/progress",
49+
params={"progressToken": meta["progressToken"], "progress": 1, "total": 1},
50+
)
51+
return McpResultResponse(
52+
result={
53+
"resultType": "complete",
54+
"content": [{"type": "text", "text": params["arguments"]["message"]}],
55+
"isError": False,
56+
}
57+
)
58+
59+
60+
class Consumer:
61+
async def notify_mcp(
62+
self, server_id: str, request_id: str, method: str, params: dict[str, Any] | None = None, **kwargs: Any
63+
) -> None:
64+
logging.info("%s / %s: %s %s", server_id, request_id, method, params)
65+
66+
67+
async def main() -> None:
68+
agent_transport, client_transport = memory_transport_pair()
69+
async with (
70+
AgentSideConnection(Consumer(), agent_transport, use_unstable_protocol=True) as agent,
71+
ClientSideConnection(EchoProvider(), client_transport, use_unstable_protocol=True),
72+
):
73+
# The provider has already registered "echo" locally. In an application,
74+
# advertise AcpMcpServer(name="echo", server_id="echo") in session setup
75+
# after checking the agent's MCP-over-ACP capability.
76+
connection: Client = agent
77+
response = await connection.mcp_message(
78+
server_id="echo",
79+
request_id=uuid4().hex,
80+
method="tools/call",
81+
params={
82+
"name": "echo",
83+
"arguments": {"message": "Hello over ACP"},
84+
"_meta": {
85+
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
86+
"io.modelcontextprotocol/clientCapabilities": {},
87+
"progressToken": "echo-progress",
88+
},
89+
},
90+
)
91+
if isinstance(response, McpErrorResponse):
92+
logging.info("MCP error: %s", response.error)
93+
else:
94+
logging.info("MCP result: %s", response.result)
95+
96+
97+
if __name__ == "__main__":
98+
logging.basicConfig(level=logging.INFO)
99+
asyncio.run(main())

‎schema/VERSION‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
refs/tags/schema-v1.23.0
1+
refs/tags/schema-v1.24.1

‎schema/meta.json‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,7 @@
4040
"terminal_release": "terminal/release",
4141
"terminal_wait_for_exit": "terminal/wait_for_exit",
4242
"terminal_kill": "terminal/kill",
43-
"mcp_connect": "mcp/connect",
4443
"mcp_message": "mcp/message",
45-
"mcp_disconnect": "mcp/disconnect",
4644
"elicitation_create": "elicitation/create",
4745
"elicitation_complete": "elicitation/complete"
4846
},

0 commit comments

Comments
 (0)