Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion docs/experimental-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
> **Experimental.** Protocol v2 is a draft. Import it from `acp.experimental` and
> expect its API and generated models to change with the upstream schema.

The bindings use `schema-v2.0.0-alpha.5`.
The bindings use `schema-v2.0.0-alpha.7`.

The v2 runtime is separate from the stable v1 API. Like v1, connection methods
and agent/client handlers accept expanded, snake-case parameters. Responses and
Expand Down Expand Up @@ -155,3 +155,13 @@ by the shared connection layer, as in v1: cancelling the task awaiting a request
sends a best-effort cancellation to the peer, and an incoming cancellation
cancels the handler task, which answers with its result or `-32800`.
`session/cancel` remains available for cancelling active session work.

## Stateless MCP-over-ACP

V2 uses the same [stateless MCP binding](quickstart.md#stateless-mcp-over-acp)
as v1. Use the independent `v2.schema.McpResultResponse`, `McpErrorResponse`,
`McpError`, and `MessageMcpResponse` types. The agent advertises support with
`capabilities.session.mcp.acp = {}`; omission or null does not advertise support.
After ACP initialization, agents send `mcp_message` requests and providers send
`notify_mcp` notifications with explicit `server_id` and `request_id` strings.
The old MCP connect/disconnect methods and reverse requests are removed.
77 changes: 68 additions & 9 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ update = AgentMessageChunk(

## Session notices (unstable)

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

Expand Down Expand Up @@ -189,7 +189,7 @@ nested objects are not valid form values.

## Additional v1 protocol methods

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

MCP requests return the inner JSON result unchanged, including `null`. Use
`notify_mcp` for one-way MCP messages. Both APIs accept `connection_id`, `method`,
and optional `params`. These methods share the same connections and routers
across stdio, HTTP, and WebSocket transports.
## Stateless MCP-over-ACP

The unstable native binding targets **MCP 2026-07-28**. A provider advertises an
`AcpMcpServer(name=..., server_id=...)` in session setup after checking the
agent's `mcpCapabilities.acp` capability. Register the provider's handler before
publishing the declaration: the agent may call it before session setup returns.
There is no MCP initialization or connect/disconnect exchange.

An agent calls its connection's `mcp_message`; the client/provider implements
that handler. Supply a fresh string `request_id` for every operation, including
an interactive-tool retry. This logical MCP ID is separate from the outer ACP
JSON-RPC request ID. Include the required MCP metadata in the inner parameters:

```python
from uuid import uuid4
from acp import McpErrorResponse

response = await connection.mcp_message(
server_id="project-tools",
request_id=uuid4().hex,
method="tools/list",
params={
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {},
},
},
trace="outer ACP metadata",
)
if isinstance(response, McpErrorResponse):
handle_mcp_error(response.error)
else:
use_mcp_result(response.result)
```

Providers return `McpResultResponse(result=...)` or
`McpErrorResponse(error=McpError(code=..., message=...))`. Results are opaque JSON,
including explicit `null`, tool errors with `isError`, and interactive
`input_required` results. Error data preserves omission versus explicit null,
and unknown inner error fields survive. Outer ACP failures raise `RequestError`;
an inner MCP error, even code `-32000`, remains inside the response carrier.
Send exactly one outcome; receivers prefer `result` if both keys are received.

The provider sends `notify_mcp(server_id=..., request_id=..., method=..., params=...)`
for an active request; the agent implements `notify_mcp` to receive it. Keep the
same server and logical request IDs and stop notifications before sending the
final outcome. Consumers must correlate notifications with active operations
and ignore unknown or completed ones. There are no provider-originated MCP requests.

The SDK provides typed envelopes and routing. Applications own server registrations,
MCP metadata/version validation, duplicate-active-ID rejection, operation admission,
and notification lifetimes. They also decide how to handle interactive input;
retry with fresh IDs and preserve opaque request state. Cancelling the task awaiting
`mcp_message` uses the existing ACP cancellation mechanism described below.
These methods work over stdio, HTTP, and WebSocket ACP connections.

**Migration:** `connect_mcp`, `disconnect_mcp`, and `connection_id` are removed.
Replace them with server-addressed requests carrying `server_id` and `request_id`,
add per-request MCP metadata, and wrap handler outcomes in the typed carrier.
Older MCP revisions and the connection-oriented prototype are not supported.

Run `uv run python examples/mcp_over_acp.py` for a local request/progress example.
See the [upstream binding specification](https://agentclientprotocol.com/rfds/mcp-over-acp)
for registration, subscription, and error-code requirements.

## Request cancellation

Expand Down
99 changes: 99 additions & 0 deletions examples/mcp_over_acp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""Run a native MCP request and progress notification over an in-memory ACP connection.

Run with: uv run python examples/mcp_over_acp.py
No MCP initialization, HTTP listener, or external provider is needed.
"""

import asyncio
import logging
from typing import Any
from uuid import uuid4

from acp import Client, McpError, McpErrorResponse, McpResultResponse, MessageMcpResponse, RequestError
from acp._transport import memory_transport_pair
from acp.agent.connection import AgentSideConnection
from acp.client.connection import ClientSideConnection


class EchoProvider:
connection: ClientSideConnection

def on_connect(self, connection: ClientSideConnection) -> None:
self.connection = connection

async def mcp_message(
self, server_id: str, request_id: str, method: str, params: dict[str, Any] | None = None, **kwargs: Any
) -> MessageMcpResponse:
if server_id != "echo":
raise RequestError(-33001, "Server registration unavailable")
params = params or {}
meta = params.get("_meta")
if not isinstance(meta, dict) or not isinstance(meta.get("io.modelcontextprotocol/protocolVersion"), str):
return McpErrorResponse(error=McpError(code=-32602, message="Missing MCP protocol version"))
if meta["io.modelcontextprotocol/protocolVersion"] != "2026-07-28":
return McpErrorResponse(
error=McpError(
code=-32022,
message="Unsupported MCP protocol version",
data={"supported": ["2026-07-28"], "requested": meta["io.modelcontextprotocol/protocolVersion"]},
)
)
if not isinstance(meta.get("io.modelcontextprotocol/clientCapabilities"), dict):
return McpErrorResponse(error=McpError(code=-32602, message="Missing MCP client capabilities"))
if method != "tools/call" or params.get("name") != "echo":
return McpErrorResponse(error=McpError(code=-32601, message="Unknown MCP operation"))
await self.connection.notify_mcp(
server_id=server_id,
request_id=request_id,
method="notifications/progress",
params={"progressToken": meta["progressToken"], "progress": 1, "total": 1},
)
return McpResultResponse(
result={
"resultType": "complete",
"content": [{"type": "text", "text": params["arguments"]["message"]}],
"isError": False,
}
)


class Consumer:
async def notify_mcp(
self, server_id: str, request_id: str, method: str, params: dict[str, Any] | None = None, **kwargs: Any
) -> None:
logging.info("%s / %s: %s %s", server_id, request_id, method, params)


async def main() -> None:
agent_transport, client_transport = memory_transport_pair()
async with (
AgentSideConnection(Consumer(), agent_transport, use_unstable_protocol=True) as agent,
ClientSideConnection(EchoProvider(), client_transport, use_unstable_protocol=True),
):
# The provider has already registered "echo" locally. In an application,
# advertise AcpMcpServer(name="echo", server_id="echo") in session setup
# after checking the agent's MCP-over-ACP capability.
connection: Client = agent
response = await connection.mcp_message(
server_id="echo",
request_id=uuid4().hex,
method="tools/call",
params={
"name": "echo",
"arguments": {"message": "Hello over ACP"},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {},
"progressToken": "echo-progress",
},
},
)
if isinstance(response, McpErrorResponse):
logging.info("MCP error: %s", response.error)
else:
logging.info("MCP result: %s", response.result)


if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
asyncio.run(main())
2 changes: 1 addition & 1 deletion schema/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
refs/tags/schema-v1.23.0
refs/tags/schema-v1.24.1
2 changes: 0 additions & 2 deletions schema/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
Loading