From a395b65777f37e62f4fb35e358186ca414aa8b3a Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 10 Nov 2026 11:54:22 +0100 Subject: [PATCH] feat(mcp)!: adopt stateless MCP-over-ACP binding --- docs/experimental-v2.md | 12 +- docs/quickstart.md | 77 +- examples/mcp_over_acp.py | 99 +++ schema/VERSION | 2 +- schema/meta.json | 2 - schema/schema.json | 989 +++++++++++++++------ schema/v2/VERSION | 2 +- schema/v2/meta.json | 2 - schema/v2/schema.json | 1083 ++++++++++++++--------- scripts/gen_schema_v1.py | 13 +- scripts/gen_schema_v2.py | 23 +- src/acp/__init__.py | 16 +- src/acp/_mcp.py | 18 + src/acp/_schema_base.py | 20 + src/acp/agent/connection.py | 57 +- src/acp/client/connection.py | 17 +- src/acp/experimental/v2/_methods.py | 3 + src/acp/experimental/v2/_router.py | 2 + src/acp/experimental/v2/_schema_base.py | 7 +- src/acp/experimental/v2/agent.py | 40 +- src/acp/experimental/v2/client.py | 15 +- src/acp/experimental/v2/interfaces.py | 38 +- src/acp/experimental/v2/meta.py | 4 +- src/acp/experimental/v2/schema.py | 511 +++++++---- src/acp/interfaces.py | 44 +- src/acp/meta.py | 4 +- src/acp/schema.py | 428 +++++++-- tests/conftest.py | 15 +- tests/test_protocol_method_coverage.py | 43 +- tests/test_stateless_mcp.py | 247 ++++++ tests/test_v2_routing.py | 10 +- tests/test_v2_runtime.py | 10 +- 32 files changed, 2726 insertions(+), 1127 deletions(-) create mode 100644 examples/mcp_over_acp.py create mode 100644 src/acp/_mcp.py create mode 100644 tests/test_stateless_mcp.py diff --git a/docs/experimental-v2.md b/docs/experimental-v2.md index e62ecb6d..087dbe17 100644 --- a/docs/experimental-v2.md +++ b/docs/experimental-v2.md @@ -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 @@ -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. diff --git a/docs/quickstart.md b/docs/quickstart.md index 14c43d9a..00d619ca 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -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`. @@ -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. @@ -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 | @@ -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 diff --git a/examples/mcp_over_acp.py b/examples/mcp_over_acp.py new file mode 100644 index 00000000..b09f6ace --- /dev/null +++ b/examples/mcp_over_acp.py @@ -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()) diff --git a/schema/VERSION b/schema/VERSION index cb282c42..ee41110d 100644 --- a/schema/VERSION +++ b/schema/VERSION @@ -1 +1 @@ -refs/tags/schema-v1.23.0 +refs/tags/schema-v1.24.1 diff --git a/schema/meta.json b/schema/meta.json index d8937f38..956449fc 100644 --- a/schema/meta.json +++ b/schema/meta.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/schema.json b/schema/schema.json index 041f6edc..4938b2ff 100644 --- a/schema/schema.json +++ b/schema/schema.json @@ -235,15 +235,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.", @@ -253,15 +244,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)", @@ -2595,47 +2577,23 @@ "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" } ] }, @@ -2662,43 +2620,20 @@ } }, "required": [ - "connectionId", + "serverId", + "requestId", "method" ], - "x-side": "both", + "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)" @@ -2901,15 +2836,6 @@ "$ref": "#/$defs/ExtResponse" } ] - }, - { - "title": "MessageMcpResponse", - "description": "Successful result returned by an MCP-over-ACP `mcp/message` request.", - "allOf": [ - { - "$ref": "#/$defs/MessageMcpResponse" - } - ] } ] } @@ -5577,11 +5503,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", @@ -5707,15 +5628,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)", @@ -6065,6 +5977,60 @@ "$ref": "#/$defs/CompactionSummaryChunk" } ] + }, + { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nAnnounces a child session created and owned by this session, or updates\nthat ownership association's metadata.", + "type": "object", + "properties": { + "sessionUpdate": { + "type": "string", + "const": "subagent_update" + } + }, + "required": [ + "sessionUpdate" + ], + "allOf": [ + { + "$ref": "#/$defs/SubagentUpdate" + } + ] + }, + { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nA message upsert observed in this session's transcript, sent to or\nreceived from another session.", + "type": "object", + "properties": { + "sessionUpdate": { + "type": "string", + "const": "session_message" + } + }, + "required": [ + "sessionUpdate" + ], + "allOf": [ + { + "$ref": "#/$defs/SessionMessage" + } + ] + }, + { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nOne content block appended to a sent or received session message.", + "type": "object", + "properties": { + "sessionUpdate": { + "type": "string", + "const": "session_message_chunk" + } + }, + "required": [ + "sessionUpdate" + ], + "allOf": [ + { + "$ref": "#/$defs/SessionMessageChunk" + } + ] } ], "discriminator": { @@ -6965,20 +6931,24 @@ "content" ] }, - "CompleteElicitationNotification": { - "description": "Notification sent by the agent when a URL-based elicitation is complete.", + "SubagentSessionCapabilities": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nClient-initiated session mutations permitted for a specific subagent session.\n\nA mutation requires an explicit per-child capability; support for the method\non ordinary sessions does not grant support on a child.", "type": "object", "properties": { - "elicitationId": { - "description": "The ID of the elicitation that completed.", - "allOf": [ + "cancel": { + "description": "Permits the client to cancel this child's current work without ending\nthe session. Omitted or `null` means unsupported; an object (including\n`{}`) means supported.", + "anyOf": [ { - "$ref": "#/$defs/ElicitationId" + "$ref": "#/$defs/SessionCancelCapabilities" + }, + { + "type": "null" } - ] + ], + "x-deserialize-default-on-error": 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\nOptional. Omitted and `null` are equivalent and mean no metadata.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", + "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.", "type": [ "object", "null" @@ -6986,38 +6956,12 @@ "x-deserialize-default-on-error": true, "additionalProperties": true } - }, - "required": [ - "elicitationId" - ], - "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`.", + "SessionCancelCapabilities": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nCapability to cancel work in a subagent session without ending that session.\n\nSupplying `{}` advertises support; an omitted or `null` `cancel` does not.", "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": [ @@ -7027,76 +6971,513 @@ "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)" + } }, - "ClientRequest": { - "description": "A JSON-RPC request object.", - "type": "object", - "properties": { - "id": { - "description": "The request id used to correlate the matching response.", + "StateUpdate": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nCurrent foreground-work state of a reusable child session.\n\nEach update is a whole-object snapshot. Idle does not terminate the child;\nthe parent can message it again, transitioning it back to running.\nBackground activity may still emit other session updates while idle.", + "anyOf": [ + { + "description": "Foreground work is in progress.", + "type": "object", + "properties": { + "state": { + "type": "string", + "const": "running" + } + }, + "required": [ + "state" + ], "allOf": [ { - "$ref": "#/$defs/RequestId" + "$ref": "#/$defs/RunningStateUpdate" } ] }, - "method": { - "description": "The method name to invoke.", - "type": "string" + { + "description": "The child is ready to process another prompt.", + "type": "object", + "properties": { + "state": { + "type": "string", + "const": "idle" + } + }, + "required": [ + "state" + ], + "allOf": [ + { + "$ref": "#/$defs/IdleStateUpdate" + } + ] }, - "params": { - "description": "Method-specific request parameters.", - "anyOf": [ + { + "description": "Foreground work is blocked on user action.", + "type": "object", + "properties": { + "state": { + "type": "string", + "const": "requires_action" + } + }, + "required": [ + "state" + ], + "allOf": [ { - "description": "All possible requests that a client can send to an agent.\n\nThis enum is used internally for routing RPC requests. You typically won't need\nto use this directly.\n\nThis enum encompasses all method calls from client to agent.", - "anyOf": [ - { - "title": "InitializeRequest", - "description": "Establishes the connection with a client and negotiates protocol capabilities.\n\nThis method is called once at the beginning of the connection to:\n- Negotiate the protocol version to use\n- Exchange capability information between client and agent\n- Determine available authentication methods\n\nThe agent should respond with its supported protocol version and capabilities.\n\nSee protocol docs: [Initialization](https://agentclientprotocol.com/protocol/initialization)", - "allOf": [ - { - "$ref": "#/$defs/InitializeRequest" - } - ] - }, - { - "title": "AuthenticateRequest", - "description": "Authenticates the client using the specified authentication method.\n\nCalled when the agent requires authentication before allowing session creation.\nThe client provides an authentication method ID that was advertised during\ninitialization and whose type defines the `authenticate` flow.\n\nAfter successful authentication, the client can proceed to create sessions with\n`new_session` without receiving an `auth_required` error.\n\nSee protocol docs: [Initialization](https://agentclientprotocol.com/protocol/initialization)", - "allOf": [ - { - "$ref": "#/$defs/AuthenticateRequest" - } - ] - }, - { - "title": "ListProvidersRequest", - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nLists providers that can be configured by the client.", - "allOf": [ - { - "$ref": "#/$defs/ListProvidersRequest" - } - ] - }, - { - "title": "SetProviderRequest", - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nReplaces the configuration for a provider.", - "allOf": [ - { - "$ref": "#/$defs/SetProviderRequest" - } - ] - }, - { + "$ref": "#/$defs/RequiresActionStateUpdate" + } + ] + }, + { + "description": "The Agent cannot currently determine foreground activity.\n\nThis replaces previously confirmed activity without ending the work or session.", + "type": "object", + "properties": { + "state": { + "type": "string", + "const": "unknown" + } + }, + "required": [ + "state" + ], + "allOf": [ + { + "$ref": "#/$defs/UnknownStateUpdate" + } + ] + }, + { + "title": "other", + "description": "Custom or future state.\n\nValues beginning with `_` are reserved for implementation-specific\nextensions. Other unknown values are reserved for future ACP variants.", + "type": "object", + "properties": { + "state": { + "description": "Unrecognized state discriminator.", + "type": "string" + } + }, + "required": [ + "state" + ], + "not": { + "anyOf": [ + { + "type": "object", + "properties": { + "state": { + "type": "string", + "const": "running" + } + }, + "required": [ + "state" + ] + }, + { + "type": "object", + "properties": { + "state": { + "type": "string", + "const": "idle" + } + }, + "required": [ + "state" + ] + }, + { + "type": "object", + "properties": { + "state": { + "type": "string", + "const": "requires_action" + } + }, + "required": [ + "state" + ] + }, + { + "type": "object", + "properties": { + "state": { + "type": "string", + "const": "unknown" + } + }, + "required": [ + "state" + ] + } + ] + }, + "additionalProperties": true + } + ] + }, + "RunningStateUpdate": { + "description": "Foreground work is in progress.", + "type": "object", + "properties": { + "_meta": { + "description": "The _meta property is reserved by ACP for additional metadata.\nImplementations MUST NOT make assumptions about values at these keys.\nOptional; omitted and `null` mean no metadata for this state snapshot.", + "type": [ + "object", + "null" + ], + "x-deserialize-default-on-error": true, + "additionalProperties": true + } + } + }, + "IdleStateUpdate": { + "description": "The child is ready to process another prompt.", + "type": "object", + "properties": { + "stopReason": { + "description": "Reason foreground work stopped. Optional; omitted or `null` means not reported.", + "anyOf": [ + { + "$ref": "#/$defs/StopReason" + }, + { + "type": "null" + } + ], + "x-deserialize-default-on-error": true + }, + "usage": { + "description": "**UNSTABLE** Token usage for completed foreground work.\n\nOptional; omitted or `null` means not reported.", + "anyOf": [ + { + "$ref": "#/$defs/Usage" + }, + { + "type": "null" + } + ], + "x-deserialize-default-on-error": true + }, + "_meta": { + "description": "The _meta property is reserved by ACP for additional metadata.\nImplementations MUST NOT make assumptions about values at these keys.\nOptional; omitted and `null` mean no metadata for this state snapshot.", + "type": [ + "object", + "null" + ], + "x-deserialize-default-on-error": true, + "additionalProperties": true + } + } + }, + "RequiresActionStateUpdate": { + "description": "Foreground work is blocked on user action.", + "type": "object", + "properties": { + "_meta": { + "description": "The _meta property is reserved by ACP for additional metadata.\nImplementations MUST NOT make assumptions about values at these keys.\nOptional; omitted and `null` mean no metadata for this state snapshot.", + "type": [ + "object", + "null" + ], + "x-deserialize-default-on-error": true, + "additionalProperties": true + } + } + }, + "UnknownStateUpdate": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nThe Agent cannot currently determine foreground activity.\n\nReport this when activity becomes unobservable, not merely because the child\nhas been quiet. The Client MUST stop presenting the previous state as confirmed\ncurrent activity, but may retain it as last known. A later state replaces this\nsnapshot normally.\n\nThis is not a task outcome or session closure. It does not cancel work, resolve\npending requests, or revoke capabilities; capabilities are updated separately.", + "type": "object", + "properties": { + "_meta": { + "description": "The _meta property is reserved by ACP for additional metadata.\nImplementations MUST NOT make assumptions about values at these keys.\nOptional; omitted and `null` mean no metadata for this state snapshot.", + "type": [ + "object", + "null" + ], + "x-deserialize-default-on-error": true, + "additionalProperties": true + } + } + }, + "SubagentUpdate": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nNotification that the enclosing parent session created and owns a child session.\n\nLater updates modify the existing association's metadata, not its ownership.\n\nSent on the immediate parent session. The first update for an unknown\n[`SubagentUpdate::session_id`] announces the child and MUST be sent\nbefore any live child traffic or live message naming the child as sender\nor recipient. Parents may message and reuse an announced child across\nmultiple operations.\nChild events are delivered automatically on the same connection; no child\nload, resume, or subscription is needed.\n\nOnly the subagent session ID is required. Omitted patch fields keep their\nprevious values; `null` clears them. Clearing capabilities disables child\nmutations. Clearing state leaves current activity unset/unconfirmed: it does\nnot imply idle, stop work, or create an `unknown` state snapshot. A concrete\nstate replaces the entire previous state object, not the session.\nThe title and description provide the parent's display metadata for the\nchild. They do not replace the content of individual messages or operations.", + "type": "object", + "properties": { + "sessionId": { + "description": "The opaque session ID identifying the child in all ACP messages.", + "allOf": [ + { + "$ref": "#/$defs/SessionId" + } + ] + }, + "title": { + "description": "The parent's human-readable display title for this child. It need not be unique.\n\nOmitted means unchanged; `null` clears it. If unset, the Client chooses\na fallback presentation.", + "type": [ + "string", + "null" + ], + "x-deserialize-default-on-error": true + }, + "description": { + "description": "The parent's human-readable description of the child's role or purpose.\n\nOmitted means unchanged; `null` clears it. If unset, the Client chooses\na fallback presentation. This is current display metadata, not the\nhistory of instructions sent to the child.", + "type": [ + "string", + "null" + ], + "x-deserialize-default-on-error": true + }, + "capabilities": { + "description": "Client-initiated session mutations permitted for this subagent session.\n\nOmitted means unchanged; `null` clears the capability set and disables\nchild mutations. If never supplied, no session mutations are permitted.\nRead-only operations retain their normal protocol semantics and capability\nrequirements. A concrete object replaces the whole capability set.", + "anyOf": [ + { + "$ref": "#/$defs/SubagentSessionCapabilities" + }, + { + "type": "null" + } + ], + "x-deserialize-default-on-error": true + }, + "state": { + "description": "Current state snapshot for the child session.\n\nOmitted means unchanged; `null` clears the current activity without\nasserting idle or sending an `unknown` snapshot. A concrete state\nreplaces the previous state object wholesale. If never supplied, the\ncurrent activity is unset/unconfirmed.", + "anyOf": [ + { + "$ref": "#/$defs/StateUpdate" + }, + { + "type": "null" + } + ], + "x-deserialize-default-on-error": 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)\nOmitted means unchanged; `null` removes the metadata.", + "type": [ + "object", + "null" + ], + "x-deserialize-default-on-error": true, + "additionalProperties": true + } + }, + "required": [ + "sessionId" + ] + }, + "SessionMessage": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nAn upsert for an inter-session message.", + "type": "object", + "properties": { + "messageId": { + "description": "Identifier of this message within the enclosing session's transcript.", + "allOf": [ + { + "$ref": "#/$defs/MessageId" + } + ] + }, + "senderSessionId": { + "description": "Optional sending session identity; omission or `null` retains a known value.", + "anyOf": [ + { + "$ref": "#/$defs/SessionId" + }, + { + "type": "null" + } + ], + "x-deserialize-default-on-error": true + }, + "recipientSessionId": { + "description": "Optional receiving session identity; omission or `null` retains a known value.", + "anyOf": [ + { + "$ref": "#/$defs/SessionId" + }, + { + "type": "null" + } + ], + "x-deserialize-default-on-error": true + }, + "content": { + "description": "Omitted leaves content unchanged; `null` or `[]` clears it.\nA non-empty array replaces all content.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/$defs/ContentBlock" + }, + "x-deserialize-default-on-error": true, + "x-deserialize-skip-invalid-items": true + }, + "_meta": { + "description": "Omitted leaves metadata unchanged; `null` removes it.\n\nImplementations MUST NOT make assumptions about values in `_meta`.", + "type": [ + "object", + "null" + ], + "x-deserialize-default-on-error": true, + "additionalProperties": true + } + }, + "required": [ + "messageId" + ] + }, + "SessionMessageChunk": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nA streamed content block of an inter-session message.", + "type": "object", + "properties": { + "messageId": { + "description": "Identifier of this message within the enclosing session's transcript.", + "allOf": [ + { + "$ref": "#/$defs/MessageId" + } + ] + }, + "senderSessionId": { + "description": "Optional sending session identity; omission or `null` retains a known value.", + "anyOf": [ + { + "$ref": "#/$defs/SessionId" + }, + { + "type": "null" + } + ], + "x-deserialize-default-on-error": true + }, + "recipientSessionId": { + "description": "Optional receiving session identity; omission or `null` retains a known value.", + "anyOf": [ + { + "$ref": "#/$defs/SessionId" + }, + { + "type": "null" + } + ], + "x-deserialize-default-on-error": true + }, + "content": { + "description": "A single content block appended to the message.", + "allOf": [ + { + "$ref": "#/$defs/ContentBlock" + } + ] + }, + "_meta": { + "description": "Optional and nullable chunk-scoped metadata; omitted or `null` means none.\n\nImplementations MUST NOT make assumptions about values in `_meta`.", + "type": [ + "object", + "null" + ], + "x-deserialize-default-on-error": true, + "additionalProperties": true + } + }, + "required": [ + "messageId", + "content" + ] + }, + "CompleteElicitationNotification": { + "description": "Notification sent by the agent when a URL-based elicitation is complete.", + "type": "object", + "properties": { + "elicitationId": { + "description": "The ID of the elicitation that completed.", + "allOf": [ + { + "$ref": "#/$defs/ElicitationId" + } + ] + }, + "_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\nOptional. Omitted and `null` are equivalent and mean no metadata.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", + "type": [ + "object", + "null" + ], + "x-deserialize-default-on-error": true, + "additionalProperties": true + } + }, + "required": [ + "elicitationId" + ], + "x-side": "client", + "x-method": "elicitation/complete" + }, + "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)" + }, + "ClientRequest": { + "description": "A JSON-RPC request object.", + "type": "object", + "properties": { + "id": { + "description": "The request id used to correlate the matching response.", + "allOf": [ + { + "$ref": "#/$defs/RequestId" + } + ] + }, + "method": { + "description": "The method name to invoke.", + "type": "string" + }, + "params": { + "description": "Method-specific request parameters.", + "anyOf": [ + { + "description": "All possible requests that a client can send to an agent.\n\nThis enum is used internally for routing RPC requests. You typically won't need\nto use this directly.\n\nThis enum encompasses all method calls from client to agent.", + "anyOf": [ + { + "title": "InitializeRequest", + "description": "Establishes the connection with a client and negotiates protocol capabilities.\n\nThis method is called once at the beginning of the connection to:\n- Negotiate the protocol version to use\n- Exchange capability information between client and agent\n- Determine available authentication methods\n\nThe agent should respond with its supported protocol version and capabilities.\n\nSee protocol docs: [Initialization](https://agentclientprotocol.com/protocol/initialization)", + "allOf": [ + { + "$ref": "#/$defs/InitializeRequest" + } + ] + }, + { + "title": "AuthenticateRequest", + "description": "Authenticates the client using the specified authentication method.\n\nCalled when the agent requires authentication before allowing session creation.\nThe client provides an authentication method ID that was advertised during\ninitialization and whose type defines the `authenticate` flow.\n\nAfter successful authentication, the client can proceed to create sessions with\n`new_session` without receiving an `auth_required` error.\n\nSee protocol docs: [Initialization](https://agentclientprotocol.com/protocol/initialization)", + "allOf": [ + { + "$ref": "#/$defs/AuthenticateRequest" + } + ] + }, + { + "title": "ListProvidersRequest", + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nLists providers that can be configured by the client.", + "allOf": [ + { + "$ref": "#/$defs/ListProvidersRequest" + } + ] + }, + { + "title": "SetProviderRequest", + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nReplaces the configuration for a provider.", + "allOf": [ + { + "$ref": "#/$defs/SetProviderRequest" + } + ] + }, + { "title": "DisableProviderRequest", "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nDisables a provider.", "allOf": [ @@ -7231,15 +7612,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)", @@ -7357,6 +7729,18 @@ ], "x-deserialize-default-on-error": true }, + "subagents": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nWhether the client understands exposed subagent sessions.\n\nOptional and nullable. Omitted or `null` both mean the client does not\nadvertise support.\nSupplying `{}` means the client understands child associations, work-state\nsnapshots, session-directed messages, and restricted-session semantics.", + "anyOf": [ + { + "$ref": "#/$defs/SubagentCapabilities" + }, + { + "type": "null" + } + ], + "x-deserialize-default-on-error": true + }, "plan": { "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nWhether the client supports `plan_update` and `plan_removed` session updates.\n\nOptional. Omitted or `null` both mean the client does not advertise support.\nSupplying `{}` means the client can receive both update types.", "anyOf": [ @@ -7553,6 +7937,21 @@ "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nClient support for presenting live advisory notices to the user.", "type": "object" }, + "SubagentCapabilities": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nCapability marker for exposing reusable child sessions as restricted ACP sessions.\n\nSupplying `{}` advertises support for child association and state updates,\nsession-directed messages, and restricted-session semantics. The client\nmust advertise this capability before the agent sends subagent updates or\nsession-directed messages.", + "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.", + "type": [ + "object", + "null" + ], + "x-deserialize-default-on-error": true, + "additionalProperties": true + } + } + }, "PlanCapabilities": { "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nCapabilities for receiving `plan_update` and `plan_removed` session updates.", "type": "object", @@ -8102,7 +8501,7 @@ ] }, "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": { @@ -9259,24 +9658,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.", @@ -9807,50 +10188,84 @@ } } }, - "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" + "MessageMcpResponse": { + "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.\nUnknown outer fields are ignored; inner result and error fields are preserved.\nCarrier `_meta` is optional; null or invalid values are treated as absent.\nSenders must include exactly one outcome; receivers prefer `result` if both are present.", + "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" ] }, - "_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 + { + "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" + ] } - }, - "required": [ - "connectionId" ], "x-side": "client", - "x-method": "mcp/connect" + "x-method": "mcp/message" }, - "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`.", + "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": { - "_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 + "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." } }, - "x-side": "client", - "x-method": "mcp/disconnect" + "required": [ + "code", + "message" + ], + "additionalProperties": true }, "ClientNotification": { "description": "A JSON-RPC notification object.", @@ -10362,6 +10777,56 @@ } ] }, + "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/VERSION b/schema/v2/VERSION index feb38860..c7bdc61f 100644 --- a/schema/v2/VERSION +++ b/schema/v2/VERSION @@ -1 +1 @@ -refs/tags/schema-v2.0.0-alpha.5 +refs/tags/schema-v2.0.0-alpha.7 diff --git a/schema/v2/meta.json b/schema/v2/meta.json index 47d7973c..1912c791 100644 --- a/schema/v2/meta.json +++ b/schema/v2/meta.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.json b/schema/v2/schema.json index 759b9d47..0c867dc5 100644 --- a/schema/v2/schema.json +++ b/schema/v2/schema.json @@ -329,15 +329,6 @@ "$ref": "#/$defs/ExtResponse" } ] - }, - { - "title": "MessageMcpResponse", - "description": "Successful result returned by an MCP-over-ACP `mcp/message` request.", - "allOf": [ - { - "$ref": "#/$defs/MessageMcpResponse" - } - ] } ] } @@ -479,24 +470,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.", @@ -641,15 +614,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.", @@ -659,15 +623,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)", @@ -3448,47 +3403,23 @@ "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" } ] }, @@ -3497,7 +3428,7 @@ "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" @@ -3505,7 +3436,7 @@ "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" @@ -3515,43 +3446,20 @@ } }, "required": [ - "connectionId", + "serverId", + "requestId", "method" ], - "x-side": "both", + "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)" @@ -3736,15 +3644,6 @@ "$ref": "#/$defs/ExtResponse" } ] - }, - { - "title": "MessageMcpResponse", - "description": "Successful result returned by an MCP-over-ACP `mcp/message` request.", - "allOf": [ - { - "$ref": "#/$defs/MessageMcpResponse" - } - ] } ] } @@ -5249,6 +5148,15 @@ "x-deserialize-default-on-error": true, "x-deserialize-skip-invalid-items": true }, + "availableCommands": { + "description": "Initial commands the agent can execute in this session.\n\nOptional. Omitted or empty means no initial commands are advertised.\nSenders MUST use an array, not `null`; receivers treat `null` like omission.\nLater `available_commands_update` notifications replace this list.", + "type": "array", + "items": { + "$ref": "#/$defs/AvailableCommand" + }, + "x-deserialize-default-on-error": true, + "x-deserialize-skip-invalid-items": 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": [ @@ -5579,6 +5487,121 @@ "currentValue" ] }, + "AvailableCommand": { + "description": "Information about a command.", + "type": "object", + "properties": { + "name": { + "description": "Command name (e.g., `create_plan`, `research_codebase`).", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the command does.", + "type": "string" + }, + "input": { + "description": "Input for the command if required", + "anyOf": [ + { + "$ref": "#/$defs/AvailableCommandInput" + }, + { + "type": "null" + } + ], + "x-deserialize-default-on-error": 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": [ + "name", + "description" + ] + }, + "AvailableCommandInput": { + "description": "The input specification for a command.", + "anyOf": [ + { + "description": "All text that was typed after the command name is provided as input.", + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "text" + } + }, + "required": [ + "type" + ], + "allOf": [ + { + "$ref": "#/$defs/TextCommandInput" + } + ] + }, + { + "title": "other", + "description": "Custom or future command input specification.\n\nValues beginning with `_` are reserved for implementation-specific\nextensions. Unknown values that do not begin with `_` are reserved for\nfuture ACP variants.\n\nClients that do not understand this input type should preserve the raw\npayload when storing, replaying, proxying, or forwarding command\nmetadata, and otherwise ignore the input specification or display the\ncommand without structured input.", + "type": "object", + "properties": { + "type": { + "description": "Custom or future command input type.\n\nValues beginning with `_` are reserved for implementation-specific\nextensions. Unknown values that do not begin with `_` are reserved for\nfuture ACP variants.", + "type": "string" + } + }, + "required": [ + "type" + ], + "not": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "text" + } + }, + "required": [ + "type" + ] + } + ] + }, + "additionalProperties": true + } + ] + }, + "TextCommandInput": { + "description": "All text that was typed after the command name is provided as input.", + "type": "object", + "properties": { + "hint": { + "description": "A hint to display when the input hasn't been provided yet", + "type": "string" + }, + "_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": [ + "hint" + ] + }, "ListSessionsResponse": { "description": "Response from listing sessions.", "type": "object", @@ -5723,6 +5746,15 @@ "x-deserialize-default-on-error": true, "x-deserialize-skip-invalid-items": true }, + "availableCommands": { + "description": "Initial commands the agent can execute in this session.\n\nOptional. Omitted or empty means no initial commands are advertised.\nSenders MUST use an array, not `null`; receivers treat `null` like omission.\nLater `available_commands_update` notifications replace this list.", + "type": "array", + "items": { + "$ref": "#/$defs/AvailableCommand" + }, + "x-deserialize-default-on-error": true, + "x-deserialize-skip-invalid-items": 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": [ @@ -5752,6 +5784,15 @@ "x-deserialize-default-on-error": true, "x-deserialize-skip-invalid-items": true }, + "availableCommands": { + "description": "Initial commands the agent can execute in this session.\n\nOptional. Omitted or empty means no initial commands are advertised.\nSenders MUST use an array, not `null`; receivers treat `null` like omission.\nLater `available_commands_update` notifications replace this list.", + "type": "array", + "items": { + "$ref": "#/$defs/AvailableCommand" + }, + "x-deserialize-default-on-error": true, + "x-deserialize-skip-invalid-items": 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": [ @@ -6361,11 +6402,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", @@ -6491,15 +6527,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)", @@ -6923,30 +6950,84 @@ ] }, { - "title": "other", - "description": "Custom or future session update.\n\nValues beginning with `_` are reserved for implementation-specific\nextensions. Unknown values that do not begin with `_` are reserved for\nfuture ACP variants.\n\nReceivers that do not understand this update type should preserve the\nraw payload when storing, replaying, proxying, or forwarding session\nhistory, and otherwise ignore it or display it generically.", + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nAnnounces a child session created and owned by this session, or updates\nthat ownership association's metadata.", "type": "object", "properties": { "sessionUpdate": { - "description": "Custom or future session update type.\n\nValues beginning with `_` are reserved for implementation-specific\nextensions. Unknown values that do not begin with `_` are reserved for\nfuture ACP variants.", - "type": "string" + "type": "string", + "const": "subagent_update" } }, "required": [ "sessionUpdate" ], - "not": { - "anyOf": [ - { - "type": "object", - "properties": { - "sessionUpdate": { - "type": "string", - "const": "user_message_chunk" - } - }, - "required": [ - "sessionUpdate" + "allOf": [ + { + "$ref": "#/$defs/SubagentUpdate" + } + ] + }, + { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nA message upsert observed in this session's transcript, sent to or\nreceived from another session.", + "type": "object", + "properties": { + "sessionUpdate": { + "type": "string", + "const": "session_message" + } + }, + "required": [ + "sessionUpdate" + ], + "allOf": [ + { + "$ref": "#/$defs/SessionMessage" + } + ] + }, + { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nOne content block appended to a sent or received session message.", + "type": "object", + "properties": { + "sessionUpdate": { + "type": "string", + "const": "session_message_chunk" + } + }, + "required": [ + "sessionUpdate" + ], + "allOf": [ + { + "$ref": "#/$defs/SessionMessageChunk" + } + ] + }, + { + "title": "other", + "description": "Custom or future session update.\n\nValues beginning with `_` are reserved for implementation-specific\nextensions. Unknown values that do not begin with `_` are reserved for\nfuture ACP variants.\n\nReceivers that do not understand this update type should preserve the\nraw payload when storing, replaying, proxying, or forwarding session\nhistory, and otherwise ignore it or display it generically.", + "type": "object", + "properties": { + "sessionUpdate": { + "description": "Custom or future session update type.\n\nValues beginning with `_` are reserved for implementation-specific\nextensions. Unknown values that do not begin with `_` are reserved for\nfuture ACP variants.", + "type": "string" + } + }, + "required": [ + "sessionUpdate" + ], + "not": { + "anyOf": [ + { + "type": "object", + "properties": { + "sessionUpdate": { + "type": "string", + "const": "user_message_chunk" + } + }, + "required": [ + "sessionUpdate" ] }, { @@ -7176,6 +7257,42 @@ "required": [ "sessionUpdate" ] + }, + { + "type": "object", + "properties": { + "sessionUpdate": { + "type": "string", + "const": "subagent_update" + } + }, + "required": [ + "sessionUpdate" + ] + }, + { + "type": "object", + "properties": { + "sessionUpdate": { + "type": "string", + "const": "session_message" + } + }, + "required": [ + "sessionUpdate" + ] + }, + { + "type": "object", + "properties": { + "sessionUpdate": { + "type": "string", + "const": "session_message_chunk" + } + }, + "required": [ + "sessionUpdate" + ] } ] }, @@ -7504,6 +7621,21 @@ } } }, + "UnknownStateUpdate": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nThe Agent cannot currently determine foreground activity.\n\nReport this when activity becomes unobservable, not merely because the session\nhas been quiet. The Client MUST stop presenting the previous state as confirmed\ncurrent activity, but may retain it as last known. A later state replaces this\nsnapshot normally.\n\nThis is not a task outcome or session closure. It does not cancel work, resolve\npending requests, or revoke capabilities; capabilities are updated separately.", + "type": "object", + "properties": { + "_meta": { + "description": "The _meta property is reserved by ACP for additional metadata.\nImplementations MUST NOT make assumptions about values at these keys.\nOptional; omitted and `null` mean no metadata for this state snapshot.", + "type": [ + "object", + "null" + ], + "x-deserialize-default-on-error": true, + "additionalProperties": true + } + } + }, "StateUpdate": { "description": "The state of the agent's foreground work has changed.\n\nBackground activity can continue and emit other `session/update` notifications\nwhile `idle`. Those notifications do not change this state.", "anyOf": [ @@ -7561,6 +7693,24 @@ } ] }, + { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nThe Agent cannot currently determine foreground activity.\nThis replaces previously confirmed activity without ending the work or session.", + "type": "object", + "properties": { + "state": { + "type": "string", + "const": "unknown" + } + }, + "required": [ + "state" + ], + "allOf": [ + { + "$ref": "#/$defs/UnknownStateUpdate" + } + ] + }, { "title": "other", "description": "Custom or future session state.\n\nValues beginning with `_` are reserved for implementation-specific\nextensions. Unknown values that do not begin with `_` are reserved for\nfuture ACP variants.", @@ -7611,6 +7761,18 @@ "required": [ "state" ] + }, + { + "type": "object", + "properties": { + "state": { + "type": "string", + "const": "unknown" + } + }, + "required": [ + "state" + ] } ] }, @@ -8184,121 +8346,6 @@ "planId" ] }, - "AvailableCommand": { - "description": "Information about a command.", - "type": "object", - "properties": { - "name": { - "description": "Command name (e.g., `create_plan`, `research_codebase`).", - "type": "string" - }, - "description": { - "description": "Human-readable description of what the command does.", - "type": "string" - }, - "input": { - "description": "Input for the command if required", - "anyOf": [ - { - "$ref": "#/$defs/AvailableCommandInput" - }, - { - "type": "null" - } - ], - "x-deserialize-default-on-error": 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": [ - "name", - "description" - ] - }, - "AvailableCommandInput": { - "description": "The input specification for a command.", - "anyOf": [ - { - "description": "All text that was typed after the command name is provided as input.", - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "text" - } - }, - "required": [ - "type" - ], - "allOf": [ - { - "$ref": "#/$defs/TextCommandInput" - } - ] - }, - { - "title": "other", - "description": "Custom or future command input specification.\n\nValues beginning with `_` are reserved for implementation-specific\nextensions. Unknown values that do not begin with `_` are reserved for\nfuture ACP variants.\n\nClients that do not understand this input type should preserve the raw\npayload when storing, replaying, proxying, or forwarding command\nmetadata, and otherwise ignore the input specification or display the\ncommand without structured input.", - "type": "object", - "properties": { - "type": { - "description": "Custom or future command input type.\n\nValues beginning with `_` are reserved for implementation-specific\nextensions. Unknown values that do not begin with `_` are reserved for\nfuture ACP variants.", - "type": "string" - } - }, - "required": [ - "type" - ], - "not": { - "anyOf": [ - { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "text" - } - }, - "required": [ - "type" - ] - } - ] - }, - "additionalProperties": true - } - ] - }, - "TextCommandInput": { - "description": "All text that was typed after the command name is provided as input.", - "type": "object", - "properties": { - "hint": { - "description": "A hint to display when the input hasn't been provided yet", - "type": "string" - }, - "_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": [ - "hint" - ] - }, "AvailableCommandsUpdate": { "description": "Available commands are ready or have changed", "type": "object", @@ -8556,28 +8603,242 @@ } ] }, - "CompactionUpdate": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nA context compaction upsert. The first update fixes the compaction's\ntimeline position. Later updates with the same ID patch that entity in place.\n\n`summary`, `error`, and `_meta` have patch semantics: omission leaves the\nstored value unchanged, `null` clears it, and a concrete value replaces it.\n`summary: []` also clears the retained summary. A non-empty summary is only\nvalid with `completed`; `error` is only valid with `failed`.", + "CompactionUpdate": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nA context compaction upsert. The first update fixes the compaction's\ntimeline position. Later updates with the same ID patch that entity in place.\n\n`summary`, `error`, and `_meta` have patch semantics: omission leaves the\nstored value unchanged, `null` clears it, and a concrete value replaces it.\n`summary: []` also clears the retained summary. A non-empty summary is only\nvalid with `completed`; `error` is only valid with `failed`.", + "type": "object", + "properties": { + "compactionId": { + "description": "The Agent-owned ID of this compaction, unique within the session.", + "allOf": [ + { + "$ref": "#/$defs/CompactionId" + } + ] + }, + "status": { + "description": "Current lifecycle status.", + "allOf": [ + { + "$ref": "#/$defs/CompactionStatus" + } + ] + }, + "summary": { + "description": "Complete replacement user-displayable summary retained by the compaction.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/$defs/ContentBlock" + }, + "x-deserialize-default-on-error": true, + "x-deserialize-skip-invalid-items": true + }, + "error": { + "description": "Human-readable description of why the compaction failed.", + "type": [ + "string", + "null" + ], + "x-deserialize-default-on-error": true + }, + "_meta": { + "description": "Extensible metadata patch for this compaction.", + "type": [ + "object", + "null" + ], + "x-deserialize-default-on-error": true, + "additionalProperties": true + } + }, + "required": [ + "compactionId", + "status" + ] + }, + "CompactionSummaryChunk": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nA content block appended to the retained summary of an in-progress\ncompaction. Agents send chunks only after an `in_progress` update and before\nthe terminal update for the same ID.", + "type": "object", + "properties": { + "compactionId": { + "description": "ID of the compaction whose summary receives this content.", + "allOf": [ + { + "$ref": "#/$defs/CompactionId" + } + ] + }, + "content": { + "description": "One content block to append.", + "allOf": [ + { + "$ref": "#/$defs/ContentBlock" + } + ] + }, + "_meta": { + "description": "Metadata scoped to this chunk. Omission and `null` both mean absent.", + "type": [ + "object", + "null" + ], + "x-deserialize-default-on-error": true, + "additionalProperties": true + } + }, + "required": [ + "compactionId", + "content" + ] + }, + "SubagentSessionCapabilities": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nClient-initiated session mutations permitted for a specific subagent session.\n\nA mutation requires an explicit per-child capability; support for the method\non ordinary sessions does not grant support on a child.", + "type": "object", + "properties": { + "cancel": { + "description": "Permits the client to cancel this child's current work without ending\nthe session. Omitted or `null` means unsupported; an object (including\n`{}`) means supported.", + "anyOf": [ + { + "$ref": "#/$defs/SessionCancelCapabilities" + }, + { + "type": "null" + } + ], + "x-deserialize-default-on-error": 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.", + "type": [ + "object", + "null" + ], + "x-deserialize-default-on-error": true, + "additionalProperties": true + } + } + }, + "SessionCancelCapabilities": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nCapability to cancel work in a subagent session without ending that session.\n\nSupplying `{}` advertises support; an omitted or `null` `cancel` does not.", + "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 + } + } + }, + "SubagentUpdate": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nNotification that the enclosing parent session created and owns a child session.\n\nLater updates modify the existing association's metadata, not its ownership.\n\nSent on the immediate parent session. The first update for an unknown\n[`SubagentUpdate::session_id`] announces the child and MUST be sent\nbefore any live request or notification bearing the child's session ID,\nincluding messages naming it as sender or recipient. Child events are\ndelivered automatically; no separate child load, resume, or subscription is needed.\nUnderstanding this update, registering child sessions, and applying their\noperation restrictions are baseline v2 requirements; no Client capability\nis required.\n\nOnly [`SubagentUpdate::session_id`] is required. Other fields have\npatch semantics: omitted fields leave the stored value unchanged, `null`\nclears or unsets the value, and concrete values replace it. For `state`,\n`null` removes the current state report and leaves activity unconfirmed;\nit does not report idle or cancellation. A child whose capabilities are\nunset permits no Client-initiated session mutations.\n\nThe title and description provide the parent's display metadata for the\nchild. Agents SHOULD mirror known child state changes in the parent's\n[`SubagentUpdate::state`] using the same [`StateUpdate`] snapshot as the\nordinary `state_update` notification on the child session. This reports\nchild state, not a separate parent-owned lifecycle. Consumers should\ntreat duplicate reports idempotently.\nCompleting or cancelling work does not end the association: the parent may\nmessage the same child again. Individual messages and their outcomes do not\nchange this association.", + "type": "object", + "properties": { + "sessionId": { + "description": "The opaque session ID identifying the child in all ACP messages.", + "allOf": [ + { + "$ref": "#/$defs/SessionId" + } + ] + }, + "title": { + "description": "The parent's human-readable display title for this child. It need not be unique.\n\nOptional and nullable. Omitted means unchanged; `null` clears it. If no\ntitle is set, the Client chooses a fallback presentation.", + "type": [ + "string", + "null" + ], + "x-deserialize-default-on-error": true + }, + "description": { + "description": "The parent's human-readable description of the child's role or purpose.\n\nOptional and nullable. Omitted means unchanged; `null` clears it. This is\ncurrent display metadata, not the history of instructions sent to the child.", + "type": [ + "string", + "null" + ], + "x-deserialize-default-on-error": true + }, + "capabilities": { + "description": "Client-initiated session mutations permitted for this subagent session.\n\nRead-only operations retain their normal protocol semantics and\ncapability requirements.", + "anyOf": [ + { + "$ref": "#/$defs/SubagentSessionCapabilities" + }, + { + "type": "null" + } + ], + "x-deserialize-default-on-error": true + }, + "state": { + "description": "The child's current foreground state, mirrored onto its parent association.\n\nOptional and nullable. Omitted means unchanged; `null` removes the\ncurrent report and leaves activity unconfirmed (not idle or cancelled).\nA concrete [`StateUpdate`] replaces the entire previous snapshot.", + "anyOf": [ + { + "$ref": "#/$defs/StateUpdate" + }, + { + "type": "null" + } + ], + "x-deserialize-default-on-error": true + }, + "_meta": { + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Omitted means no metadata update; `null` is an\nexplicit clear signal. Implementations MUST NOT make assumptions about values at these 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": [ + "sessionId" + ] + }, + "SessionMessage": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nAn upsert for an inter-session message.", "type": "object", "properties": { - "compactionId": { - "description": "The Agent-owned ID of this compaction, unique within the session.", + "messageId": { + "description": "Identifier of this message within the enclosing session's transcript.", "allOf": [ { - "$ref": "#/$defs/CompactionId" + "$ref": "#/$defs/MessageId" } ] }, - "status": { - "description": "Current lifecycle status.", - "allOf": [ + "senderSessionId": { + "description": "Optional sending session identity; omission or `null` retains a known value.", + "anyOf": [ { - "$ref": "#/$defs/CompactionStatus" + "$ref": "#/$defs/SessionId" + }, + { + "type": "null" } - ] + ], + "x-deserialize-default-on-error": true }, - "summary": { - "description": "Complete replacement user-displayable summary retained by the compaction.", + "recipientSessionId": { + "description": "Optional receiving session identity; omission or `null` retains a known value.", + "anyOf": [ + { + "$ref": "#/$defs/SessionId" + }, + { + "type": "null" + } + ], + "x-deserialize-default-on-error": true + }, + "content": { + "description": "Omitted leaves content unchanged; `null` clears it; a concrete array\nreplaces the whole content collection.", "type": [ "array", "null" @@ -8588,16 +8849,8 @@ "x-deserialize-default-on-error": true, "x-deserialize-skip-invalid-items": true }, - "error": { - "description": "Human-readable description of why the compaction failed.", - "type": [ - "string", - "null" - ], - "x-deserialize-default-on-error": true - }, "_meta": { - "description": "Extensible metadata patch for this compaction.", + "description": "Omitted leaves metadata unchanged; `null` clears it.\n\nImplementations MUST NOT make assumptions about values in `_meta`.", "type": [ "object", "null" @@ -8607,24 +8860,47 @@ } }, "required": [ - "compactionId", - "status" + "messageId" ] }, - "CompactionSummaryChunk": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nA content block appended to the retained summary of an in-progress\ncompaction. Agents send chunks only after an `in_progress` update and before\nthe terminal update for the same ID.", + "SessionMessageChunk": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nA streamed content block of an inter-session message.", "type": "object", "properties": { - "compactionId": { - "description": "ID of the compaction whose summary receives this content.", + "messageId": { + "description": "Identifier of this message within the enclosing session's transcript.", "allOf": [ { - "$ref": "#/$defs/CompactionId" + "$ref": "#/$defs/MessageId" } ] }, + "senderSessionId": { + "description": "Optional sending session identity; omission or `null` retains a known value.", + "anyOf": [ + { + "$ref": "#/$defs/SessionId" + }, + { + "type": "null" + } + ], + "x-deserialize-default-on-error": true + }, + "recipientSessionId": { + "description": "Optional receiving session identity; omission or `null` retains a known value.", + "anyOf": [ + { + "$ref": "#/$defs/SessionId" + }, + { + "type": "null" + } + ], + "x-deserialize-default-on-error": true + }, "content": { - "description": "One content block to append.", + "description": "A single content block appended to the message.", "allOf": [ { "$ref": "#/$defs/ContentBlock" @@ -8632,7 +8908,7 @@ ] }, "_meta": { - "description": "Metadata scoped to this chunk. Omission and `null` both mean absent.", + "description": "Optional and nullable chunk-scoped metadata; omitted or `null` means none.\n\nImplementations MUST NOT make assumptions about values in `_meta`.", "type": [ "object", "null" @@ -8642,7 +8918,7 @@ } }, "required": [ - "compactionId", + "messageId", "content" ] }, @@ -8674,48 +8950,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)" }, @@ -8894,15 +9128,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)", @@ -9603,7 +9828,7 @@ ] }, "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": { @@ -10786,24 +11011,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.", @@ -11162,50 +11369,84 @@ } } }, - "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" + "MessageMcpResponse": { + "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.\nUnknown outer fields are ignored; inner result and error fields are preserved.\nCarrier `_meta` is optional; null or invalid values are treated as absent.\nSenders must include exactly one outcome; receivers prefer `result` if both are present.", + "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" ] }, - "_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 + { + "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" + ] } - }, - "required": [ - "connectionId" ], "x-side": "client", - "x-method": "mcp/connect" + "x-method": "mcp/message" }, - "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`.", + "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": { - "_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 + "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." } }, - "x-side": "client", - "x-method": "mcp/disconnect" + "required": [ + "code", + "message" + ], + "additionalProperties": true }, "ClientNotification": { "description": "A JSON-RPC notification object.", @@ -11727,6 +11968,56 @@ } ] }, + "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", diff --git a/scripts/gen_schema_v1.py b/scripts/gen_schema_v1.py index d2088076..fe655b9f 100644 --- a/scripts/gen_schema_v1.py +++ b/scripts/gen_schema_v1.py @@ -111,6 +111,8 @@ MODEL_NAME_MAP.update(variant_names) MODEL_NAME_MAP.update({ + inline_model_ref("MessageMcpResponse", ("anyOf", 0), ("object", None)): "McpResultResponse", + inline_model_ref("MessageMcpResponse", ("anyOf", 1), ("object", None)): "McpErrorResponse", inline_model_ref("RequestPermissionOutcome", ("oneOf", 0), ("object", None)): "DeniedOutcome", inline_model_ref("RequestPermissionOutcome", ("oneOf", 1), ("allOf", None)): "AllowedOutcome", inline_model_ref("ElicitationPropertySchema", ("anyOf", 5), ("object", None)): ("ElicitationOtherPropertySchema"), @@ -161,8 +163,10 @@ }) # datamodel-code-generator owns schema interpretation and its internal model names. -# This block only preserves the Python names already published by the SDK. +# Public aliases preserve published Python names and name the MCP outcome union. COMPATIBILITY_ALIASES = textwrap.dedent(""" + MessageMcpResponse = Annotated[Union[McpResultResponse, McpErrorResponse], Field(union_mode="left_to_right")] + PermissionOptionKind = Literal["allow_once", "allow_always", "reject_once", "reject_always"] PlanEntryPriority = Literal["high", "medium", "low"] PlanEntryStatus = Literal["pending", "in_progress", "completed"] @@ -230,6 +234,9 @@ class Jsonrpc(Enum): base_class="acp._schema_base.BaseModel", model_name_map=MODEL_NAME_MAP, compatibility_aliases=COMPATIBILITY_ALIASES, - # The generator loses additionalProperties types on nullable objects. - field_type_overrides={"ElicitationAcceptAction.content": "acp._schema_base.ElicitationContent"}, + # Preserve nullable object value types and reject non-integer MCP error codes. + field_type_overrides={ + "ElicitationAcceptAction.content": "acp._schema_base.ElicitationContent", + "McpError.code": "pydantic.StrictInt", + }, ) diff --git a/scripts/gen_schema_v2.py b/scripts/gen_schema_v2.py index eed4bcf1..980072d8 100644 --- a/scripts/gen_schema_v2.py +++ b/scripts/gen_schema_v2.py @@ -55,7 +55,7 @@ "StateUpdate", "anyOf", "allOf", - ("RunningState", "IdleState", "RequiresActionState"), + ("RunningState", "IdleState", "RequiresActionState", "UnknownState"), ), variant_model_map( "ToolCallContent", @@ -130,12 +130,17 @@ "SessionNotice", "SessionCompactionUpdate", "SessionCompactionSummaryChunk", + "SessionSubagentUpdate", + "SessionMessageUpdate", + "SessionMessageChunkUpdate", ), ), ): MODEL_NAME_MAP.update(variant_names) MODEL_NAME_MAP.update({ + inline_model_ref("MessageMcpResponse", ("anyOf", 0), ("object", None)): "McpResultResponse", + inline_model_ref("MessageMcpResponse", ("anyOf", 1), ("object", None)): "McpErrorResponse", inline_model_ref("AuthMethod", ("anyOf", 2), ("object", None)): "OtherAuthMethod", inline_model_ref("AvailableCommandInput", ("anyOf", 1), ("object", None)): "OtherAvailableCommandInput", inline_model_ref("ContentBlock", ("anyOf", 5), ("object", None)): "OtherContentBlock", @@ -156,25 +161,27 @@ inline_model_ref("SetSessionConfigOptionRequest", ("anyOf", 2), ("object", None)): ( "SetSessionConfigOptionOtherRequest" ), - inline_model_ref("StateUpdate", ("anyOf", 3), ("object", None)): "OtherState", + inline_model_ref("StateUpdate", ("anyOf", 4), ("object", None)): "OtherState", inline_model_ref("ToolCallContent", ("anyOf", 3), ("object", None)): "OtherToolCallContent", inline_model_ref("PlanUpdateContent", ("anyOf", 3), ("object", None)): "OtherPlanUpdateContent", inline_model_ref("NesSuggestion", ("anyOf", 4), ("object", None)): "OtherNesSuggestion", inline_model_ref("ElicitationPropertySchema", ("anyOf", 5), ("object", None)): ("ElicitationOtherPropertySchema"), inline_model_ref("MultiSelectItems", ("anyOf", 1), ("object", None)): "OtherMultiSelectItems", - inline_model_ref("SessionUpdate", ("anyOf", 20), ("object", None)): "OtherSessionUpdate", + inline_model_ref("SessionUpdate", ("anyOf", 23), ("object", None)): "OtherSessionUpdate", inline_model_ref("SessionUpdate", ("anyOf", 6), ("allOf", 0), ("allOf", None)): ("RunningSessionStateUpdateBase"), inline_model_ref("SessionUpdate", ("anyOf", 6), ("allOf", 1), ("allOf", None)): ("IdleSessionStateUpdateBase"), inline_model_ref("SessionUpdate", ("anyOf", 6), ("allOf", 2), ("allOf", None)): ( "RequiresActionSessionStateUpdateBase" ), - inline_model_ref("SessionUpdate", ("anyOf", 6), ("allOf", 3), ("object", None)): ("OtherSessionStateUpdateBase"), + inline_model_ref("SessionUpdate", ("anyOf", 6), ("allOf", 3), ("allOf", None)): "UnknownSessionStateUpdateBase", + inline_model_ref("SessionUpdate", ("anyOf", 6), ("allOf", 4), ("object", None)): ("OtherSessionStateUpdateBase"), inline_model_ref("SessionUpdate", ("anyOf", 6), ("allOf", None), ("allOf", None)): ("SessionStateUpdateBase"), }) for index, name in enumerate(( "RunningSessionStateUpdate", "IdleSessionStateUpdate", "RequiresActionSessionStateUpdate", + "UnknownSessionStateUpdate", "OtherSessionStateUpdate", )): MODEL_NAME_MAP[inline_model_ref("SessionUpdate", ("anyOf", 6), ("allOf", None), (f"union_model-{index}", None))] = ( @@ -229,7 +236,11 @@ version_file=ROOT / "schema" / "v2" / "VERSION", schema_out=ROOT / "src" / "acp" / "experimental" / "v2" / "schema.py", base_class="acp.experimental.v2._schema_base.BaseModel", + compatibility_aliases='MessageMcpResponse = Annotated[Union[McpResultResponse, McpErrorResponse], Field(union_mode="left_to_right")]', model_name_map=MODEL_NAME_MAP, - # The generator loses additionalProperties types on nullable objects. - field_type_overrides={"ElicitationAcceptAction.content": "acp._schema_base.ElicitationContent"}, + # Preserve nullable object value types and reject non-integer MCP error codes. + field_type_overrides={ + "ElicitationAcceptAction.content": "acp._schema_base.ElicitationContent", + "McpError.code": "pydantic.StrictInt", + }, ) diff --git a/src/acp/__init__.py b/src/acp/__init__.py index 02928a1b..ede36b13 100644 --- a/src/acp/__init__.py +++ b/src/acp/__init__.py @@ -22,8 +22,6 @@ CloseNesRequest, CloseNesResponse, CompleteElicitationNotification, - ConnectMcpRequest, - ConnectMcpResponse, CreateElicitationRequest, CreateElicitationResponse, CreateFormElicitationRequest, @@ -45,8 +43,6 @@ DidSaveDocumentNotification, DisableProviderRequest, DisableProviderResponse, - DisconnectMcpRequest, - DisconnectMcpResponse, ElicitationBooleanPropertySchema, ElicitationCapabilities, ElicitationFormCapabilities, @@ -72,8 +68,12 @@ LoadSessionResponse, LogoutRequest, LogoutResponse, + McpError, + McpErrorResponse, + McpResultResponse, MessageMcpNotification, MessageMcpRequest, + MessageMcpResponse, NewSessionRequest, NewSessionResponse, OtherElicitationResponse, @@ -129,8 +129,6 @@ "AcceptNesNotification", "CloseNesRequest", "CloseNesResponse", - "ConnectMcpRequest", - "ConnectMcpResponse", "DeleteSessionRequest", "DeleteSessionResponse", "DidChangeDocumentNotification", @@ -140,14 +138,16 @@ "DidSaveDocumentNotification", "DisableProviderRequest", "DisableProviderResponse", - "DisconnectMcpRequest", - "DisconnectMcpResponse", "ListProvidersRequest", "ListProvidersResponse", "LogoutRequest", "LogoutResponse", "MessageMcpNotification", "MessageMcpRequest", + "MessageMcpResponse", + "McpResultResponse", + "McpErrorResponse", + "McpError", "RejectNesNotification", "SetProviderRequest", "SetProviderResponse", diff --git a/src/acp/_mcp.py b/src/acp/_mcp.py new file mode 100644 index 00000000..8294c000 --- /dev/null +++ b/src/acp/_mcp.py @@ -0,0 +1,18 @@ +"""Validation of outgoing MCP outcomes, separate from ACP errors.""" + +from typing import Any + +from pydantic import BaseModel, TypeAdapter, ValidationError + +from .exceptions import RequestError + + +def validate_mcp_response(payload: Any, *, adapter: TypeAdapter[Any]) -> Any: + """Require exactly one outcome when sending; receivers may prefer result.""" + fields = type(payload).model_fields if isinstance(payload, BaseModel) else payload + if not isinstance(fields, dict) or (("result" in fields) == ("error" in fields)): + raise RequestError(-33002, "Invalid MCP response carrier: expected exactly one result or error") + try: + return adapter.validate_python(payload) + except ValidationError as exc: + raise RequestError(-33002, "Invalid MCP response carrier") from exc diff --git a/src/acp/_schema_base.py b/src/acp/_schema_base.py index 7719a015..c5a05fb9 100644 --- a/src/acp/_schema_base.py +++ b/src/acp/_schema_base.py @@ -21,6 +21,8 @@ class BaseModel(pydantic.BaseModel): """Runtime behavior shared by generated ACP schema models.""" + _preserved_null_fields: ClassVar[dict[str, str]] = {"McpResultResponse": "result", "McpError": "data"} + _reserved_tags: ClassVar[dict[str, tuple[str, frozenset[str]]]] = { "CreateOtherSessionElicitationRequest": ("mode", frozenset({"form", "url"})), "CreateOtherRequestElicitationRequest": ("mode", frozenset({"form", "url"})), @@ -49,6 +51,24 @@ def _include_literal_defaults( info: SerializationInfo, ) -> Any: data = handler(self) + # MCP preserves explicit null outcomes, error data, and opaque error extensions. + mcp_field = self._preserved_null_fields.get(type(self).__name__) + if ( + mcp_field is not None + and mcp_field in self.model_fields_set + and getattr(self, mcp_field) is None + and (info.include is None or mcp_field in info.include) + and (info.exclude is None or mcp_field not in info.exclude) + ): + data[mcp_field] = None + if type(self).__name__ == "McpError": + for name, value in (self.model_extra or {}).items(): + if ( + value is None + and (info.include is None or name in info.include) + and (info.exclude is None or name not in info.exclude) + ): + data[name] = None for name, field in type(self).model_fields.items(): annotation = field.annotation if info.include is not None and name not in info.include: diff --git a/src/acp/agent/connection.py b/src/acp/agent/connection.py index f0c793c8..9646fb0a 100644 --- a/src/acp/agent/connection.py +++ b/src/acp/agent/connection.py @@ -21,8 +21,6 @@ CancelElicitationResponse, CompleteElicitationNotification, ConfigOptionUpdate, - ConnectMcpRequest, - ConnectMcpResponse, CreateElicitationResponse, CreateFormElicitationRequest, CreateFormRequestElicitationRequest, @@ -34,8 +32,6 @@ CreateUrlSessionElicitationRequest, CurrentModeUpdate, DeclineElicitationResponse, - DisconnectMcpRequest, - DisconnectMcpResponse, ElicitationFormRequestMode, ElicitationFormSessionMode, ElicitationMode, @@ -44,8 +40,8 @@ EnvVariable, KillTerminalRequest, KillTerminalResponse, - MessageMcpNotification, MessageMcpRequest, + MessageMcpResponse, PermissionOption, ReadTextFileRequest, ReadTextFileResponse, @@ -58,6 +54,9 @@ SessionUpdateCompactionSummaryChunk, SessionUpdateCompactionUpdate, SessionUpdateNotice, + SessionUpdateSessionMessage, + SessionUpdateSessionMessageChunk, + SessionUpdateSubagentUpdate, TerminalOutputRequest, TerminalOutputResponse, ToolCallProgress, @@ -75,7 +74,6 @@ notify_model, param_model, request_model, - request_model_from_dict, request_optional_model, serialize_params, ) @@ -84,6 +82,7 @@ __all__ = ["AgentSideConnection"] _AGENT_CONNECTION_ERROR = "AgentSideConnection requires asyncio StreamWriter/StreamReader" _CREATE_ELICITATION_RESPONSE_ADAPTER = TypeAdapter(CreateElicitationResponse) +_MCP_RESPONSE_ADAPTER = TypeAdapter(MessageMcpResponse) @final @@ -167,7 +166,10 @@ async def session_update( | UsageUpdate | SessionUpdateNotice | SessionUpdateCompactionUpdate - | SessionUpdateCompactionSummaryChunk, + | SessionUpdateCompactionSummaryChunk + | SessionUpdateSubagentUpdate + | SessionUpdateSessionMessage + | SessionUpdateSessionMessageChunk, **kwargs: Any, ) -> None: await notify_model( @@ -292,46 +294,19 @@ async def complete_elicitation(self, elicitation_id: str, **kwargs: Any) -> None CompleteElicitationNotification(elicitation_id=elicitation_id, field_meta=kwargs or None), ) - @param_model(ConnectMcpRequest) - async def connect_mcp(self, server_id: str, **kwargs: Any) -> ConnectMcpResponse: - return await request_model( - self._conn, - CLIENT_METHODS["mcp_connect"], - ConnectMcpRequest(server_id=server_id, field_meta=kwargs or None), - ConnectMcpResponse, - ) - - @param_model(DisconnectMcpRequest) - async def disconnect_mcp(self, connection_id: str, **kwargs: Any) -> DisconnectMcpResponse: - return await request_model_from_dict( - self._conn, - CLIENT_METHODS["mcp_disconnect"], - DisconnectMcpRequest(connection_id=connection_id, field_meta=kwargs or None), - DisconnectMcpResponse, - ) - @param_model(MessageMcpRequest) async def mcp_message( - self, connection_id: str, method: str, params: dict[str, Any] | None = None, **kwargs: Any - ) -> Any: - return await self._conn.send_request( + self, server_id: str, request_id: str, method: str, params: dict[str, Any] | None = None, **kwargs: Any + ) -> MessageMcpResponse: + response = await self._conn.send_request( CLIENT_METHODS["mcp_message"], serialize_params( - MessageMcpRequest(connection_id=connection_id, method=method, params=params, field_meta=kwargs or None) - ), - ) - - @param_model(MessageMcpNotification) - async def notify_mcp( - self, connection_id: str, method: str, params: dict[str, Any] | None = None, **kwargs: Any - ) -> None: - await notify_model( - self._conn, - CLIENT_METHODS["mcp_message"], - MessageMcpNotification( - connection_id=connection_id, method=method, params=params, field_meta=kwargs or None + MessageMcpRequest( + server_id=server_id, request_id=request_id, method=method, params=params, field_meta=kwargs or None + ) ), ) + return _MCP_RESPONSE_ADAPTER.validate_python(response) async def ext_method(self, method: str, params: dict[str, Any]) -> dict[str, Any]: return await self._conn.send_request(f"_{method}", params) diff --git a/src/acp/client/connection.py b/src/acp/client/connection.py index 3cd73d22..5135341a 100644 --- a/src/acp/client/connection.py +++ b/src/acp/client/connection.py @@ -50,7 +50,6 @@ LogoutResponse, McpServerStdio, MessageMcpNotification, - MessageMcpRequest, NesRepository, NesSuggestContext, NewSessionRequest, @@ -86,7 +85,6 @@ param_model, request_model, request_model_from_dict, - serialize_params, ) from .router import build_client_router @@ -434,26 +432,15 @@ async def logout(self, **kwargs: Any) -> LogoutResponse: self._conn, AGENT_METHODS["logout"], LogoutRequest(field_meta=kwargs or None), LogoutResponse ) - @param_model(MessageMcpRequest) - async def mcp_message( - self, connection_id: str, method: str, params: dict[str, Any] | None = None, **kwargs: Any - ) -> Any: - return await self._conn.send_request( - AGENT_METHODS["mcp_message"], - serialize_params( - MessageMcpRequest(connection_id=connection_id, method=method, params=params, field_meta=kwargs or None) - ), - ) - @param_model(MessageMcpNotification) async def notify_mcp( - self, connection_id: str, method: str, params: dict[str, Any] | None = None, **kwargs: Any + self, server_id: str, request_id: str, method: str, params: dict[str, Any] | None = None, **kwargs: Any ) -> None: await notify_model( self._conn, AGENT_METHODS["mcp_message"], MessageMcpNotification( - connection_id=connection_id, method=method, params=params, field_meta=kwargs or None + server_id=server_id, request_id=request_id, method=method, params=params, field_meta=kwargs or None ), ) diff --git a/src/acp/experimental/v2/_methods.py b/src/acp/experimental/v2/_methods.py index 3ecd7876..f0d39516 100644 --- a/src/acp/experimental/v2/_methods.py +++ b/src/acp/experimental/v2/_methods.py @@ -1,5 +1,6 @@ from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass from functools import cache from typing import Any, get_type_hints @@ -16,6 +17,7 @@ class RequestSpec: request: TypeAdapter[Any] response: TypeAdapter[Any] empty_response: bool = False + adapt_result: Callable[[Any], Any] | None = None @dataclass(frozen=True, slots=True) @@ -48,6 +50,7 @@ def protocol_specs(protocol: type) -> tuple[tuple[RequestSpec, ...], tuple[Notif TypeAdapter(metadata.request_type), TypeAdapter(get_type_hints(handler, include_extras=True)["return"]), empty_response=metadata.default_result == {}, + adapt_result=metadata.adapt_result, ) return tuple(requests.values()), tuple(notifications.values()) diff --git a/src/acp/experimental/v2/_router.py b/src/acp/experimental/v2/_router.py index 4afb0e7e..4dddb537 100644 --- a/src/acp/experimental/v2/_router.py +++ b/src/acp/experimental/v2/_router.py @@ -41,6 +41,8 @@ async def handle_request(self, spec: RequestSpec, params: Any) -> Any: response = await handler(**model_to_kwargs(request, type(request))) if response is None and spec.empty_response: response = {} + if spec.adapt_result is not None: + response = spec.adapt_result(response) return spec.response.validate_python(response) async def handle_notification(self, spec: NotificationSpec, params: Any) -> None: diff --git a/src/acp/experimental/v2/_schema_base.py b/src/acp/experimental/v2/_schema_base.py index e2812efa..b63d4b32 100644 --- a/src/acp/experimental/v2/_schema_base.py +++ b/src/acp/experimental/v2/_schema_base.py @@ -20,7 +20,7 @@ class BaseModel(_BaseModel): "OtherPlanUpdateContent": ("type", frozenset({"file", "items", "markdown"})), "OtherReplayFrom": ("type", frozenset({"start"})), "OtherSessionConfigOption": ("type", frozenset({"boolean", "select"})), - "OtherSessionStateUpdate": ("state", frozenset({"idle", "requires_action", "running"})), + "OtherSessionStateUpdate": ("state", frozenset({"idle", "requires_action", "running", "unknown"})), "OtherSessionUpdate": ( "sessionUpdate", frozenset({ @@ -44,9 +44,12 @@ class BaseModel(_BaseModel): "usage_update", "user_message", "user_message_chunk", + "subagent_update", + "session_message", + "session_message_chunk", }), ), - "OtherState": ("state", frozenset({"idle", "requires_action", "running"})), + "OtherState": ("state", frozenset({"idle", "requires_action", "running", "unknown"})), "OtherToolCallContent": ("type", frozenset({"content", "diff", "terminal"})), "SetSessionConfigOptionOtherRequest": ("type", frozenset({"boolean", "id"})), "CreateOtherSessionElicitationRequest": ("mode", frozenset({"form", "url"})), diff --git a/src/acp/experimental/v2/agent.py b/src/acp/experimental/v2/agent.py index 4eaf2f05..ab829105 100644 --- a/src/acp/experimental/v2/agent.py +++ b/src/acp/experimental/v2/agent.py @@ -12,9 +12,7 @@ from . import schema from ._connection import open_connection from ._initialization import InitializationState -from ._methods import ( - CLIENT_REQUESTS_BY_METHOD, -) +from ._methods import CLIENT_REQUESTS_BY_METHOD from ._params import build_elicitation_request, build_request from ._router import MethodRouter from .interfaces import Agent, CreateElicitationRequest, CreateElicitationResponse @@ -143,10 +141,14 @@ async def session_update( | schema.SessionNotice | schema.SessionCompactionUpdate | schema.SessionCompactionSummaryChunk + | schema.SessionSubagentUpdate + | schema.SessionMessageUpdate + | schema.SessionMessageChunkUpdate | schema.OtherSessionUpdate | schema.RunningSessionStateUpdate | schema.IdleSessionStateUpdate | schema.RequiresActionSessionStateUpdate + | schema.UnknownSessionStateUpdate | schema.OtherSessionStateUpdate, **kwargs: Any, ) -> None: @@ -155,43 +157,19 @@ async def session_update( build_request(schema.UpdateSessionNotification, {"session_id": session_id, "update": update}, kwargs), ) - @param_model(schema.ConnectMcpRequest) - async def connect_mcp(self, server_id: str, **kwargs: Any) -> schema.ConnectMcpResponse: - return await self._request( - CLIENT_METHODS["mcp_connect"], build_request(schema.ConnectMcpRequest, {"server_id": server_id}, kwargs) - ) - @param_model(schema.MessageMcpRequest) async def mcp_message( - self, connection_id: str, method: str, params: dict[str, Any] | None = None, **kwargs: Any - ) -> Any: + self, server_id: str, request_id: str, method: str, params: dict[str, Any] | None = None, **kwargs: Any + ) -> schema.MessageMcpResponse: return await self._request( CLIENT_METHODS["mcp_message"], build_request( - schema.MessageMcpRequest, {"connection_id": connection_id, "method": method, "params": params}, kwargs - ), - ) - - @param_model(schema.MessageMcpNotification) - async def notify_mcp( - self, connection_id: str, method: str, params: dict[str, Any] | None = None, **kwargs: Any - ) -> None: - await self._notify( - CLIENT_METHODS["mcp_message"], - build_request( - schema.MessageMcpNotification, - {"connection_id": connection_id, "method": method, "params": params}, + schema.MessageMcpRequest, + {"server_id": server_id, "request_id": request_id, "method": method, "params": params}, kwargs, ), ) - @param_model(schema.DisconnectMcpRequest) - async def disconnect_mcp(self, connection_id: str, **kwargs: Any) -> schema.DisconnectMcpResponse: - return await self._request( - CLIENT_METHODS["mcp_disconnect"], - build_request(schema.DisconnectMcpRequest, {"connection_id": connection_id}, kwargs), - ) - @param_model(CreateElicitationRequest) async def create_elicitation( self, diff --git a/src/acp/experimental/v2/client.py b/src/acp/experimental/v2/client.py index ce82acd0..2db21aea 100644 --- a/src/acp/experimental/v2/client.py +++ b/src/acp/experimental/v2/client.py @@ -247,26 +247,15 @@ async def cancel_session(self, session_id: str, **kwargs: Any) -> None: build_request(schema.CancelSessionNotification, {"session_id": session_id}, kwargs), ) - @param_model(schema.MessageMcpRequest) - async def mcp_message( - self, connection_id: str, method: str, params: dict[str, Any] | None = None, **kwargs: Any - ) -> Any: - return await self._request( - AGENT_METHODS["mcp_message"], - build_request( - schema.MessageMcpRequest, {"connection_id": connection_id, "method": method, "params": params}, kwargs - ), - ) - @param_model(schema.MessageMcpNotification) async def notify_mcp( - self, connection_id: str, method: str, params: dict[str, Any] | None = None, **kwargs: Any + self, server_id: str, request_id: str, method: str, params: dict[str, Any] | None = None, **kwargs: Any ) -> None: await self._notify( AGENT_METHODS["mcp_message"], build_request( schema.MessageMcpNotification, - {"connection_id": connection_id, "method": method, "params": params}, + {"server_id": server_id, "request_id": request_id, "method": method, "params": params}, kwargs, ), ) diff --git a/src/acp/experimental/v2/interfaces.py b/src/acp/experimental/v2/interfaces.py index 36692e16..f5b84955 100644 --- a/src/acp/experimental/v2/interfaces.py +++ b/src/acp/experimental/v2/interfaces.py @@ -1,9 +1,11 @@ from __future__ import annotations +from functools import partial from typing import Any, Literal, Protocol -from pydantic import AnyUrl +from pydantic import AnyUrl, TypeAdapter +from acp._mcp import validate_mcp_response from acp.utils import param_model from . import schema @@ -14,7 +16,6 @@ | schema.SetSessionConfigOptionBooleanRequest | schema.SetSessionConfigOptionOtherRequest ) - CreateElicitationRequest = ( schema.CreateOtherSessionElicitationRequest | schema.CreateOtherRequestElicitationRequest @@ -23,7 +24,6 @@ | schema.CreateUrlSessionElicitationRequest | schema.CreateUrlRequestElicitationRequest ) - CreateElicitationResponse = ( schema.AcceptElicitationResponse | schema.DeclineElicitationResponse @@ -144,14 +144,9 @@ async def prompt( @param_model(schema.CancelSessionNotification, method=AGENT_METHODS["session_cancel"], kind="notification") async def cancel_session(self, session_id: str, **kwargs: Any) -> None: ... - @param_model(schema.MessageMcpRequest, method=AGENT_METHODS["mcp_message"]) - async def mcp_message( - self, connection_id: str, method: str, params: dict[str, Any] | None = None, **kwargs: Any - ) -> Any: ... - @param_model(schema.MessageMcpNotification, method=AGENT_METHODS["mcp_message"], kind="notification") async def notify_mcp( - self, connection_id: str, method: str, params: dict[str, Any] | None = None, **kwargs: Any + self, server_id: str, request_id: str, method: str, params: dict[str, Any] | None = None, **kwargs: Any ) -> None: ... @param_model(schema.StartNesRequest, method=AGENT_METHODS["nes_start"]) @@ -269,29 +264,26 @@ async def session_update( | schema.SessionNotice | schema.SessionCompactionUpdate | schema.SessionCompactionSummaryChunk + | schema.SessionSubagentUpdate + | schema.SessionMessageUpdate + | schema.SessionMessageChunkUpdate | schema.OtherSessionUpdate | schema.RunningSessionStateUpdate | schema.IdleSessionStateUpdate | schema.RequiresActionSessionStateUpdate + | schema.UnknownSessionStateUpdate | schema.OtherSessionStateUpdate, **kwargs: Any, ) -> None: ... - @param_model(schema.ConnectMcpRequest, method=CLIENT_METHODS["mcp_connect"]) - async def connect_mcp(self, server_id: str, **kwargs: Any) -> schema.ConnectMcpResponse: ... - - @param_model(schema.MessageMcpRequest, method=CLIENT_METHODS["mcp_message"]) + @param_model( + schema.MessageMcpRequest, + method=CLIENT_METHODS["mcp_message"], + adapt_result=partial(validate_mcp_response, adapter=TypeAdapter(schema.MessageMcpResponse)), + ) async def mcp_message( - self, connection_id: str, method: str, params: dict[str, Any] | None = None, **kwargs: Any - ) -> Any: ... - - @param_model(schema.MessageMcpNotification, method=CLIENT_METHODS["mcp_message"], kind="notification") - async def notify_mcp( - self, connection_id: str, method: str, params: dict[str, Any] | None = None, **kwargs: Any - ) -> None: ... - - @param_model(schema.DisconnectMcpRequest, method=CLIENT_METHODS["mcp_disconnect"], default_result={}) - async def disconnect_mcp(self, connection_id: str, **kwargs: Any) -> schema.DisconnectMcpResponse: ... + self, server_id: str, request_id: str, method: str, params: dict[str, Any] | None = None, **kwargs: Any + ) -> schema.MessageMcpResponse: ... @param_model(CreateElicitationRequest, method=CLIENT_METHODS["elicitation_create"]) async def create_elicitation( diff --git a/src/acp/experimental/v2/meta.py b/src/acp/experimental/v2/meta.py index 76bbadce..2f2ce75b 100644 --- a/src/acp/experimental/v2/meta.py +++ b/src/acp/experimental/v2/meta.py @@ -1,5 +1,5 @@ # Generated from schema/v2/meta.json. Do not edit by hand. -# Schema ref: refs/tags/schema-v2.0.0-alpha.5 +# Schema ref: refs/tags/schema-v2.0.0-alpha.7 AGENT_METHODS = { "initialize": "initialize", "auth_login": "auth/login", @@ -31,9 +31,7 @@ CLIENT_METHODS = { "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/src/acp/experimental/v2/schema.py b/src/acp/experimental/v2/schema.py index b8e5f518..801771be 100644 --- a/src/acp/experimental/v2/schema.py +++ b/src/acp/experimental/v2/schema.py @@ -1,5 +1,5 @@ # Generated from schema/v2/schema.json. Do not edit by hand. -# Schema ref: refs/tags/schema-v2.0.0-alpha.5 +# Schema ref: refs/tags/schema-v2.0.0-alpha.7 from __future__ import annotations @@ -15,6 +15,7 @@ ConfigDict, Field, RootModel, + StrictInt, ValidationInfo, ValidatorFunctionWrapHandler, field_validator, @@ -600,21 +601,6 @@ class ElicitationUrlMode(RootModel[Union[ElicitationUrlSessionMode, ElicitationU """ -class DisconnectMcpRequest(BaseModel): - connection_id: Annotated[str, Field(alias="connectionId")] - """ - The MCP-over-ACP connection to close. - """ - field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_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/v2/draft/extensibility) - """ - - class Implementation(BaseModel): name: str """ @@ -1097,6 +1083,35 @@ class SessionConfigBoolean(BaseModel): """ +class OtherAvailableCommandInput(BaseModel): + model_config = ConfigDict( + extra="allow", + ) + type: str + """ + Custom or future command input type. + + Values beginning with `_` are reserved for implementation-specific + extensions. Unknown values that do not begin with `_` are reserved for + future ACP variants. + """ + + +class TextCommandInput(BaseModel): + hint: str + """ + A hint to display when the input hasn't been provided yet + """ + field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_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/v2/draft/extensibility) + """ + + class SessionInfo(BaseModel): session_id: Annotated[str, Field(alias="sessionId")] """ @@ -1434,6 +1449,15 @@ class RequiresActionStateUpdate(BaseModel): """ +class UnknownStateUpdate(BaseModel): + field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_meta")] = None + """ + The _meta property is reserved by ACP for additional metadata. + Implementations MUST NOT make assumptions about values at these keys. + Optional; omitted and `null` mean no metadata for this state snapshot. + """ + + class RunningState(RunningStateUpdate): state: Literal["running"] = "running" @@ -1446,6 +1470,10 @@ class RequiresActionState(RequiresActionStateUpdate): state: Literal["requires_action"] = "requires_action" +class UnknownState(UnknownStateUpdate): + state: Literal["unknown"] = "unknown" + + class OtherState(BaseModel): model_config = ConfigDict( extra="allow", @@ -1460,16 +1488,6 @@ class OtherState(BaseModel): """ -class StateUpdate(RootModel[Union[RunningState, IdleState, RequiresActionState, OtherState]]): - root: Union[RunningState, IdleState, RequiresActionState, OtherState] - """ - The state of the agent's foreground work has changed. - - Background activity can continue and emit other `session/update` notifications - while `idle`. Those notifications do not change this state. - """ - - class TerminalOutput(BaseModel): data: Annotated[str, Field(json_schema_extra={"contentEncoding": "base64"})] """ @@ -1624,35 +1642,6 @@ class PlanRemoved(BaseModel): """ -class OtherAvailableCommandInput(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - type: str - """ - Custom or future command input type. - - Values beginning with `_` are reserved for implementation-specific - extensions. Unknown values that do not begin with `_` are reserved for - future ACP variants. - """ - - -class TextCommandInput(BaseModel): - hint: str - """ - A hint to display when the input hasn't been provided yet - """ - field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_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/v2/draft/extensibility) - """ - - class SessionInfoUpdateBase(BaseModel): title: Optional[str] = None """ @@ -1742,37 +1731,21 @@ def use_default_on_error_validator(cls, v: Any, handler: ValidatorFunctionWrapHa return use_default_on_error(v, handler, info) -class CompleteElicitationNotification(BaseModel): - elicitation_id: Annotated[str, Field(alias="elicitationId")] - """ - The ID of the elicitation that completed. - """ +class SessionCancelCapabilities(BaseModel): field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_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. - Optional. Omitted and `null` are equivalent and mean no metadata. - See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility) """ -class MessageMcpNotification(BaseModel): - connection_id: Annotated[str, Field(alias="connectionId")] - """ - The MCP-over-ACP connection this message is sent on. - """ - method: str - """ - The inner MCP method name. - """ - params: Optional[Dict[str, Any]] = None +class CompleteElicitationNotification(BaseModel): + elicitation_id: Annotated[str, Field(alias="elicitationId")] """ - Optional inner MCP params. - - If omitted or set to `null`, the inner MCP message has no params. + The ID of the elicitation that completed. """ field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_meta")] = None """ @@ -1780,14 +1753,11 @@ class MessageMcpNotification(BaseModel): metadata to their interactions. Implementations MUST NOT make assumptions about values at these keys. + Optional. Omitted and `null` are equivalent and mean no metadata. + See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility) """ - @field_validator("params", mode="wrap") - @classmethod - def use_default_on_error_validator(cls, v: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo) -> Any: - return use_default_on_error(v, handler, info) - class TerminalAuthCapabilities(BaseModel): field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_meta")] = None @@ -2460,29 +2430,32 @@ class ElicitationAcceptAction(BaseModel): """ -class ConnectMcpResponse(BaseModel): - connection_id: Annotated[str, Field(alias="connectionId")] +class McpResultResponse(BaseModel): + result: Any """ - The unique identifier for this MCP-over-ACP connection. + Required, even if JSON null. """ field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_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/v2/draft/extensibility) + Optional ACP carrier metadata. """ -class DisconnectMcpResponse(BaseModel): - field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_meta")] = None +class McpError(BaseModel): + model_config = ConfigDict( + extra="allow", + ) + code: StrictInt """ - 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) + Inner MCP error code; never an ACP error code. + """ + message: str + """ + Inner MCP error message. + """ + data: Optional[Any] = None + """ + Optional error data; explicit null is retained. """ @@ -2589,6 +2562,29 @@ class AcceptNesNotification(BaseModel): """ +class MessageMcpNotification(BaseModel): + server_id: Annotated[str, Field(alias="serverId")] + """ + The declared ACP MCP server handling the associated request. + """ + request_id: Annotated[str, Field(alias="requestId")] + """ + The identifier of the active inner MCP request. + """ + method: str + """ + The inner MCP method name. + """ + params: Optional[Dict[str, Any]] = None + """ + Optional inner MCP params; null is equivalent to omission. + """ + field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_meta")] = None + """ + ACP extension metadata (not inner MCP params._meta); null is equivalent to omission. + """ + + class CancelRequestNotification(BaseModel): request_id: Annotated[Optional[Union[int, str]], Field(alias="requestId")] """ @@ -3274,25 +3270,14 @@ def skip_invalid_items_validator(cls, v: Any, handler: ValidatorFunctionWrapHand return skip_invalid_items(v, handler, info) -class ConnectMcpRequest(BaseModel): +class MessageMcpRequest(BaseModel): server_id: Annotated[str, Field(alias="serverId")] """ - The ACP MCP server ID that was provided by the component declaring the MCP server. + The declared ACP MCP server receiving this request. """ - field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_meta")] = None + request_id: Annotated[str, Field(alias="requestId")] """ - 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) - """ - - -class MessageMcpRequest(BaseModel): - connection_id: Annotated[str, Field(alias="connectionId")] - """ - The MCP-over-ACP connection this message is sent on. + The caller-generated identifier for the inner MCP request. """ method: str """ @@ -3300,17 +3285,11 @@ class MessageMcpRequest(BaseModel): """ params: Optional[Dict[str, Any]] = None """ - 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. """ field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_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/v2/draft/extensibility) + ACP extension metadata (not inner MCP params._meta); null is equivalent to omission. """ @@ -3628,6 +3607,10 @@ def skip_invalid_items_validator(cls, v: Any, handler: ValidatorFunctionWrapHand return skip_invalid_items(v, handler, info) +class TextAvailableCommandInput(TextCommandInput): + type: Literal["text"] = "text" + + class ListSessionsResponse(BaseModel): sessions: List[SessionInfo] """ @@ -3773,6 +3756,10 @@ class RequiresActionSessionStateUpdateBase(RequiresActionStateUpdate): state: Literal["requires_action"] = "requires_action" +class UnknownSessionStateUpdateBase(UnknownStateUpdate): + state: Literal["unknown"] = "unknown" + + class RunningSessionStateUpdate(RunningSessionStateUpdateBase, SessionStateUpdateBase): session_update: Annotated[Literal["state_update"], Field(alias="sessionUpdate")] = "state_update" @@ -3785,6 +3772,10 @@ class RequiresActionSessionStateUpdate(RequiresActionSessionStateUpdateBase, Ses session_update: Annotated[Literal["state_update"], Field(alias="sessionUpdate")] = "state_update" +class UnknownSessionStateUpdate(UnknownSessionStateUpdateBase, SessionStateUpdateBase): + session_update: Annotated[Literal["state_update"], Field(alias="sessionUpdate")] = "state_update" + + class SessionTerminalUpdate(TerminalUpdate): session_update: Annotated[Literal["terminal_update"], Field(alias="sessionUpdate")] = "terminal_update" @@ -3896,8 +3887,73 @@ def skip_invalid_items_validator(cls, v: Any, handler: ValidatorFunctionWrapHand return skip_invalid_items(v, handler, info) -class TextAvailableCommandInput(TextCommandInput): - type: Literal["text"] = "text" +class SubagentSessionCapabilities(BaseModel): + cancel: Optional[SessionCancelCapabilities] = None + """ + Permits the client to cancel this child's current work without ending + the session. Omitted or `null` means unsupported; an object (including + `{}`) means supported. + """ + field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_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. + """ + + @field_validator("cancel", mode="wrap") + @classmethod + def use_default_on_error_validator(cls, v: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo) -> Any: + return use_default_on_error(v, handler, info) + + +class SubagentUpdate(BaseModel): + session_id: Annotated[str, Field(alias="sessionId")] + """ + The opaque session ID identifying the child in all ACP messages. + """ + title: Optional[str] = None + """ + The parent's human-readable display title for this child. It need not be unique. + + Optional and nullable. Omitted means unchanged; `null` clears it. If no + title is set, the Client chooses a fallback presentation. + """ + description: Optional[str] = None + """ + The parent's human-readable description of the child's role or purpose. + + Optional and nullable. Omitted means unchanged; `null` clears it. This is + current display metadata, not the history of instructions sent to the child. + """ + capabilities: Optional[SubagentSessionCapabilities] = None + """ + Client-initiated session mutations permitted for this subagent session. + + Read-only operations retain their normal protocol semantics and + capability requirements. + """ + state: Optional[Union[RunningState, IdleState, RequiresActionState, UnknownState, OtherState]] = None + """ + The child's current foreground state, mirrored onto its parent association. + + Optional and nullable. Omitted means unchanged; `null` removes the + current report and leaves activity unconfirmed (not idle or cancelled). + A concrete [`StateUpdate`] replaces the entire previous snapshot. + """ + field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_meta")] = None + """ + The _meta property is reserved by ACP to allow clients and agents to attach additional + metadata to their interactions. Omitted means no metadata update; `null` is an + explicit clear signal. Implementations MUST NOT make assumptions about values at these keys. + + See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility) + """ + + @field_validator("capabilities", "description", "state", "title", mode="wrap") + @classmethod + def use_default_on_error_validator(cls, v: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo) -> Any: + return use_default_on_error(v, handler, info) class AuthCapabilities(BaseModel): @@ -4174,6 +4230,17 @@ class AcceptElicitationResponse(ElicitationAcceptAction): action: Literal["accept"] = "accept" +class McpErrorResponse(BaseModel): + error: McpError + """ + Required, non-null MCP error object. + """ + field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_meta")] = None + """ + Optional ACP carrier metadata. + """ + + class TextDocumentContentChangeEvent(BaseModel): range: Optional[Range] = None """ @@ -4450,6 +4517,34 @@ class SessionConfigSelect(BaseModel): """ +class AvailableCommand(BaseModel): + name: str + """ + Command name (e.g., `create_plan`, `research_codebase`). + """ + description: str + """ + Human-readable description of what the command does. + """ + input: Optional[Union[TextAvailableCommandInput, OtherAvailableCommandInput]] = None + """ + Input for the command if required + """ + field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_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/v2/draft/extensibility) + """ + + @field_validator("input", mode="wrap") + @classmethod + def use_default_on_error_validator(cls, v: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo) -> Any: + return use_default_on_error(v, handler, info) + + class NesTextEdit(BaseModel): range: Range """ @@ -4501,6 +4596,10 @@ def use_default_on_error_validator(cls, v: Any, handler: ValidatorFunctionWrapHa return use_default_on_error(v, handler, info) +class SessionSubagentUpdate(SubagentUpdate): + session_update: Annotated[Literal["subagent_update"], Field(alias="sessionUpdate")] = "subagent_update" + + class ContentChunk(BaseModel): message_id: Annotated[str, Field(alias="messageId")] """ @@ -4654,34 +4753,6 @@ class PlanUpdate(BaseModel): """ -class AvailableCommand(BaseModel): - name: str - """ - Command name (e.g., `create_plan`, `research_codebase`). - """ - description: str - """ - Human-readable description of what the command does. - """ - input: Optional[Union[TextAvailableCommandInput, OtherAvailableCommandInput]] = None - """ - Input for the command if required - """ - field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_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/v2/draft/extensibility) - """ - - @field_validator("input", mode="wrap") - @classmethod - def use_default_on_error_validator(cls, v: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo) -> Any: - return use_default_on_error(v, handler, info) - - class AvailableCommandsUpdateBase(BaseModel): available_commands: Annotated[List[AvailableCommand], Field(alias="availableCommands")] """ @@ -4763,6 +4834,90 @@ class CompactionSummaryChunk(BaseModel): """ +class SessionMessage(BaseModel): + message_id: Annotated[str, Field(alias="messageId")] + """ + Identifier of this message within the enclosing session's transcript. + """ + sender_session_id: Annotated[Optional[str], Field(alias="senderSessionId")] = None + """ + Optional sending session identity; omission or `null` retains a known value. + """ + recipient_session_id: Annotated[Optional[str], Field(alias="recipientSessionId")] = None + """ + Optional receiving session identity; omission or `null` retains a known value. + """ + content: Optional[ + List[ + Union[ + TextContentBlock, + ImageContentBlock, + AudioContentBlock, + ResourceContentBlock, + EmbeddedResourceContentBlock, + OtherContentBlock, + ] + ] + ] = None + """ + Omitted leaves content unchanged; `null` clears it; a concrete array + replaces the whole content collection. + """ + field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_meta")] = None + """ + Omitted leaves metadata unchanged; `null` clears it. + + Implementations MUST NOT make assumptions about values in `_meta`. + """ + + @field_validator("recipient_session_id", "sender_session_id", mode="wrap") + @classmethod + def use_default_on_error_validator(cls, v: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo) -> Any: + return use_default_on_error(v, handler, info) + + @field_validator("content", mode="wrap") + @classmethod + def skip_invalid_items_validator(cls, v: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo) -> Any: + return skip_invalid_items(v, handler, info) + + +class SessionMessageChunk(BaseModel): + message_id: Annotated[str, Field(alias="messageId")] + """ + Identifier of this message within the enclosing session's transcript. + """ + sender_session_id: Annotated[Optional[str], Field(alias="senderSessionId")] = None + """ + Optional sending session identity; omission or `null` retains a known value. + """ + recipient_session_id: Annotated[Optional[str], Field(alias="recipientSessionId")] = None + """ + Optional receiving session identity; omission or `null` retains a known value. + """ + content: Union[ + TextContentBlock, + ImageContentBlock, + AudioContentBlock, + ResourceContentBlock, + EmbeddedResourceContentBlock, + OtherContentBlock, + ] + """ + A single content block appended to the message. + """ + field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_meta")] = None + """ + Optional and nullable chunk-scoped metadata; omitted or `null` means none. + + Implementations MUST NOT make assumptions about values in `_meta`. + """ + + @field_validator("recipient_session_id", "sender_session_id", mode="wrap") + @classmethod + def use_default_on_error_validator(cls, v: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo) -> Any: + return use_default_on_error(v, handler, info) + + class ClientCapabilities(BaseModel): auth: Optional[AuthCapabilities] = None """ @@ -5178,6 +5333,14 @@ class ForkSessionResponse(BaseModel): """ Initial session configuration options. """ + available_commands: Annotated[Optional[List[AvailableCommand]], Field(alias="availableCommands")] = None + """ + Initial commands the agent can execute in this session. + + Optional. Omitted or empty means no initial commands are advertised. + Senders MUST use an array, not `null`; receivers treat `null` like omission. + Later `available_commands_update` notifications replace this list. + """ field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_meta")] = None """ The _meta property is reserved by ACP to allow clients and agents to attach additional @@ -5187,7 +5350,7 @@ class ForkSessionResponse(BaseModel): See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility) """ - @field_validator("config_options", mode="wrap") + @field_validator("available_commands", "config_options", mode="wrap") @classmethod def skip_invalid_items_validator(cls, v: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo) -> Any: return skip_invalid_items(v, handler, info) @@ -5201,6 +5364,14 @@ class ResumeSessionResponse(BaseModel): """ Initial session configuration options. """ + available_commands: Annotated[Optional[List[AvailableCommand]], Field(alias="availableCommands")] = None + """ + Initial commands the agent can execute in this session. + + Optional. Omitted or empty means no initial commands are advertised. + Senders MUST use an array, not `null`; receivers treat `null` like omission. + Later `available_commands_update` notifications replace this list. + """ field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_meta")] = None """ The _meta property is reserved by ACP to allow clients and agents to attach additional @@ -5210,7 +5381,7 @@ class ResumeSessionResponse(BaseModel): See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility) """ - @field_validator("config_options", mode="wrap") + @field_validator("available_commands", "config_options", mode="wrap") @classmethod def skip_invalid_items_validator(cls, v: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo) -> Any: return skip_invalid_items(v, handler, info) @@ -5292,6 +5463,14 @@ class SessionCompactionSummaryChunk(CompactionSummaryChunk): ) +class SessionMessageUpdate(SessionMessage): + session_update: Annotated[Literal["session_message"], Field(alias="sessionUpdate")] = "session_message" + + +class SessionMessageChunkUpdate(SessionMessageChunk): + session_update: Annotated[Literal["session_message_chunk"], Field(alias="sessionUpdate")] = "session_message_chunk" + + class ToolCallContentChunk(BaseModel): tool_call_id: Annotated[str, Field(alias="toolCallId")] """ @@ -5410,11 +5589,10 @@ class ClientResponseMessage(BaseModel): """ result: Union[ RequestPermissionResponse, - ConnectMcpResponse, - DisconnectMcpResponse, Union[ AcceptElicitationResponse, DeclineElicitationResponse, CancelElicitationResponse, OtherElicitationResponse ], + Union[McpResultResponse, McpErrorResponse], Any, ] """ @@ -5637,6 +5815,14 @@ class NewSessionResponse(BaseModel): """ Initial session configuration options. """ + available_commands: Annotated[Optional[List[AvailableCommand]], Field(alias="availableCommands")] = None + """ + Initial commands the agent can execute in this session. + + Optional. Omitted or empty means no initial commands are advertised. + Senders MUST use an array, not `null`; receivers treat `null` like omission. + Later `available_commands_update` notifications replace this list. + """ field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_meta")] = None """ The _meta property is reserved by ACP to allow clients and agents to attach additional @@ -5646,7 +5832,7 @@ class NewSessionResponse(BaseModel): See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility) """ - @field_validator("config_options", mode="wrap") + @field_validator("available_commands", "config_options", mode="wrap") @classmethod def skip_invalid_items_validator(cls, v: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo) -> Any: return skip_invalid_items(v, handler, info) @@ -5721,7 +5907,6 @@ class ClientRequest(BaseModel): StartNesRequest, SuggestNesRequest, CloseNesRequest, - MessageMcpRequest, Union[ SetSessionConfigOptionIdRequest, SetSessionConfigOptionBooleanRequest, @@ -5828,9 +6013,16 @@ class UpdateSessionNotification(BaseModel): SessionNotice, SessionCompactionUpdate, SessionCompactionSummaryChunk, + SessionSubagentUpdate, + SessionMessageUpdate, + SessionMessageChunkUpdate, OtherSessionUpdate, Union[ - RunningSessionStateUpdate, IdleSessionStateUpdate, RequiresActionSessionStateUpdate, OtherSessionStateUpdate + RunningSessionStateUpdate, + IdleSessionStateUpdate, + RequiresActionSessionStateUpdate, + UnknownSessionStateUpdate, + OtherSessionStateUpdate, ], ] """ @@ -5945,9 +6137,7 @@ class AgentNotification(BaseModel): """ The notification method name. """ - params: Optional[Union[UpdateSessionNotification, CompleteElicitationNotification, MessageMcpNotification, Any]] = ( - None - ) + params: Optional[Union[UpdateSessionNotification, CompleteElicitationNotification, Any]] = None """ Method-specific notification parameters. """ @@ -5965,9 +6155,7 @@ class AgentRequest(BaseModel): params: Optional[ Union[ RequestPermissionRequest, - ConnectMcpRequest, MessageMcpRequest, - DisconnectMcpRequest, Union[ Union[CreateOtherSessionElicitationRequest, CreateOtherRequestElicitationRequest], Union[CreateFormSessionElicitationRequest, CreateFormRequestElicitationRequest], @@ -6016,3 +6204,6 @@ class AgentResponse(RootModel[Union[AgentResponseMessage, AgentErrorMessage]]): """ A JSON-RPC response object. """ + + +MessageMcpResponse = Annotated[Union[McpResultResponse, McpErrorResponse], Field(union_mode="left_to_right")] diff --git a/src/acp/interfaces.py b/src/acp/interfaces.py index 073f5166..d4641f9b 100644 --- a/src/acp/interfaces.py +++ b/src/acp/interfaces.py @@ -1,7 +1,11 @@ from __future__ import annotations +from functools import partial from typing import Any, Literal, Protocol +from pydantic import TypeAdapter + +from ._mcp import validate_mcp_response from ._protocol_adapters import ( elicitation_to_kwargs, validate_create_elicitation_request, @@ -28,8 +32,6 @@ CloseSessionResponse, CompleteElicitationNotification, ConfigOptionUpdate, - ConnectMcpRequest, - ConnectMcpResponse, CreateElicitationResponse, CreateFormRequestElicitationRequest, CreateFormSessionElicitationRequest, @@ -47,8 +49,6 @@ DidSaveDocumentNotification, DisableProviderRequest, DisableProviderResponse, - DisconnectMcpRequest, - DisconnectMcpResponse, ElicitationMode, EmbeddedResourceContentBlock, EnvVariable, @@ -72,6 +72,7 @@ McpServerStdio, MessageMcpNotification, MessageMcpRequest, + MessageMcpResponse, NesRepository, NesSuggestContext, NewSessionRequest, @@ -96,6 +97,9 @@ SessionUpdateCompactionSummaryChunk, SessionUpdateCompactionUpdate, SessionUpdateNotice, + SessionUpdateSessionMessage, + SessionUpdateSessionMessageChunk, + SessionUpdateSubagentUpdate, SetProviderRequest, SetProviderResponse, SetSessionConfigOptionBooleanRequest, @@ -129,23 +133,15 @@ class Client(Protocol): - @param_model(ConnectMcpRequest, method=CLIENT_METHODS["mcp_connect"], unstable=True) - async def connect_mcp(self, server_id: str, **kwargs: Any) -> ConnectMcpResponse: ... - @param_model( - DisconnectMcpRequest, method=CLIENT_METHODS["mcp_disconnect"], unstable=True, adapt_result=normalize_result + MessageMcpRequest, + method=CLIENT_METHODS["mcp_message"], + unstable=True, + adapt_result=partial(validate_mcp_response, adapter=TypeAdapter(MessageMcpResponse)), ) - async def disconnect_mcp(self, connection_id: str, **kwargs: Any) -> DisconnectMcpResponse: ... - - @param_model(MessageMcpRequest, method=CLIENT_METHODS["mcp_message"], unstable=True) async def mcp_message( - self, connection_id: str, method: str, params: dict[str, Any] | None = None, **kwargs: Any - ) -> Any: ... - - @param_model(MessageMcpNotification, method=CLIENT_METHODS["mcp_message"], kind="notification", unstable=True) - async def notify_mcp( - self, connection_id: str, method: str, params: dict[str, Any] | None = None, **kwargs: Any - ) -> None: ... + self, server_id: str, request_id: str, method: str, params: dict[str, Any] | None = None, **kwargs: Any + ) -> MessageMcpResponse: ... @param_model(RequestPermissionRequest, method=CLIENT_METHODS["session_request_permission"]) async def request_permission( @@ -171,7 +167,10 @@ async def session_update( | UsageUpdate | SessionUpdateNotice | SessionUpdateCompactionUpdate - | SessionUpdateCompactionSummaryChunk, + | SessionUpdateCompactionSummaryChunk + | SessionUpdateSubagentUpdate + | SessionUpdateSessionMessage + | SessionUpdateSessionMessageChunk, **kwargs: Any, ) -> None: ... @@ -289,14 +288,9 @@ async def disable_provider(self, provider_id: str, **kwargs: Any) -> DisableProv @param_model(LogoutRequest, method=AGENT_METHODS["logout"], adapt_result=normalize_result) async def logout(self, **kwargs: Any) -> LogoutResponse: ... - @param_model(MessageMcpRequest, method=AGENT_METHODS["mcp_message"], unstable=True) - async def mcp_message( - self, connection_id: str, method: str, params: dict[str, Any] | None = None, **kwargs: Any - ) -> Any: ... - @param_model(MessageMcpNotification, method=AGENT_METHODS["mcp_message"], kind="notification", unstable=True) async def notify_mcp( - self, connection_id: str, method: str, params: dict[str, Any] | None = None, **kwargs: Any + self, server_id: str, request_id: str, method: str, params: dict[str, Any] | None = None, **kwargs: Any ) -> None: ... @param_model(StartNesRequest, method=AGENT_METHODS["nes_start"], unstable=True) diff --git a/src/acp/meta.py b/src/acp/meta.py index 0c1565ae..9960944a 100644 --- a/src/acp/meta.py +++ b/src/acp/meta.py @@ -1,5 +1,5 @@ # Generated from schema/meta.json. Do not edit by hand. -# Schema ref: refs/tags/schema-v1.23.0 +# Schema ref: refs/tags/schema-v1.24.1 AGENT_METHODS = { "initialize": "initialize", "authenticate": "authenticate", @@ -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/src/acp/schema.py b/src/acp/schema.py index d5261a07..9ee274f5 100644 --- a/src/acp/schema.py +++ b/src/acp/schema.py @@ -1,5 +1,5 @@ # Generated from schema/schema.json. Do not edit by hand. -# Schema ref: refs/tags/schema-v1.23.0 +# Schema ref: refs/tags/schema-v1.24.1 from __future__ import annotations @@ -8,7 +8,16 @@ from acp._deserialize import coerce_protocol_version, skip_invalid_items, use_default_on_error from acp._schema_base import BaseModel, ElicitationContent -from pydantic import AnyUrl, ConfigDict, Field, RootModel, ValidationInfo, ValidatorFunctionWrapHandler, field_validator +from pydantic import ( + AnyUrl, + ConfigDict, + Field, + RootModel, + StrictInt, + ValidationInfo, + ValidatorFunctionWrapHandler, + field_validator, +) class ReadTextFileRequest(BaseModel): @@ -650,21 +659,6 @@ class ElicitationUrlMode(RootModel[Union[ElicitationUrlSessionMode, ElicitationU """ -class DisconnectMcpRequest(BaseModel): - connection_id: Annotated[str, Field(alias="connectionId")] - """ - The MCP-over-ACP connection to close. - """ - field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_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) - """ - - class PromptCapabilities(BaseModel): image: Optional[bool] = False """ @@ -1601,37 +1595,85 @@ def use_default_on_error_validator(cls, v: Any, handler: ValidatorFunctionWrapHa return use_default_on_error(v, handler, info) -class CompleteElicitationNotification(BaseModel): - elicitation_id: Annotated[str, Field(alias="elicitationId")] - """ - The ID of the elicitation that completed. - """ +class SessionCancelCapabilities(BaseModel): field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_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. - Optional. Omitted and `null` are equivalent and mean no metadata. - See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) """ -class MessageMcpNotification(BaseModel): - connection_id: Annotated[str, Field(alias="connectionId")] +class StateUpdate5(BaseModel): + model_config = ConfigDict( + extra="allow", + ) + state: str """ - The MCP-over-ACP connection this message is sent on. + Unrecognized state discriminator. """ - method: str + + +class RunningStateUpdate(BaseModel): + field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_meta")] = None """ - The inner MCP method name. + The _meta property is reserved by ACP for additional metadata. + Implementations MUST NOT make assumptions about values at these keys. + Optional; omitted and `null` mean no metadata for this state snapshot. """ - params: Optional[Dict[str, Any]] = None + + +class IdleStateUpdate(BaseModel): + stop_reason: Annotated[ + Optional[Literal["end_turn", "max_tokens", "max_turn_requests", "refusal", "cancelled"]], + Field(alias="stopReason"), + ] = None """ - Optional inner MCP params. + Reason foreground work stopped. Optional; omitted or `null` means not reported. + """ + usage: Optional[Usage] = None + """ + **UNSTABLE** Token usage for completed foreground work. - If omitted or set to `null`, the inner MCP message has no params. + Optional; omitted or `null` means not reported. + """ + field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_meta")] = None + """ + The _meta property is reserved by ACP for additional metadata. + Implementations MUST NOT make assumptions about values at these keys. + Optional; omitted and `null` mean no metadata for this state snapshot. + """ + + @field_validator("stop_reason", "usage", mode="wrap") + @classmethod + def use_default_on_error_validator(cls, v: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo) -> Any: + return use_default_on_error(v, handler, info) + + +class RequiresActionStateUpdate(BaseModel): + field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_meta")] = None + """ + The _meta property is reserved by ACP for additional metadata. + Implementations MUST NOT make assumptions about values at these keys. + Optional; omitted and `null` mean no metadata for this state snapshot. + """ + + +class UnknownStateUpdate(BaseModel): + field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_meta")] = None + """ + The _meta property is reserved by ACP for additional metadata. + Implementations MUST NOT make assumptions about values at these keys. + Optional; omitted and `null` mean no metadata for this state snapshot. + """ + + +class CompleteElicitationNotification(BaseModel): + elicitation_id: Annotated[str, Field(alias="elicitationId")] + """ + The ID of the elicitation that completed. """ field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_meta")] = None """ @@ -1639,14 +1681,11 @@ class MessageMcpNotification(BaseModel): metadata to their interactions. Implementations MUST NOT make assumptions about values at these keys. + Optional. Omitted and `null` are equivalent and mean no metadata. + See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) """ - @field_validator("params", mode="wrap") - @classmethod - def use_default_on_error_validator(cls, v: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo) -> Any: - return use_default_on_error(v, handler, info) - class FileSystemCapabilities(BaseModel): read_text_file: Annotated[Optional[bool], Field(alias="readTextFile")] = False @@ -1691,6 +1730,15 @@ class NoticeCapabilities(BaseModel): pass +class SubagentCapabilities(BaseModel): + field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_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. + """ + + class PlanCapabilities(BaseModel): field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_meta")] = None """ @@ -2443,29 +2491,32 @@ class ElicitationAcceptAction(BaseModel): """ -class ConnectMcpResponse(BaseModel): - connection_id: Annotated[str, Field(alias="connectionId")] +class McpResultResponse(BaseModel): + result: Any """ - The unique identifier for this MCP-over-ACP connection. + Required, even if JSON null. """ field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_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) + Optional ACP carrier metadata. """ -class DisconnectMcpResponse(BaseModel): - field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_meta")] = None +class McpError(BaseModel): + model_config = ConfigDict( + extra="allow", + ) + code: StrictInt """ - 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) + Inner MCP error code; never an ACP error code. + """ + message: str + """ + Inner MCP error message. + """ + data: Optional[Any] = None + """ + Optional error data; explicit null is retained. """ @@ -2572,6 +2623,35 @@ class AcceptNesNotification(BaseModel): """ +class MessageMcpNotification(BaseModel): + server_id: Annotated[str, Field(alias="serverId")] + """ + The declared ACP MCP server handling the associated request. + """ + request_id: Annotated[str, Field(alias="requestId")] + """ + The identifier of the active inner MCP request. + """ + method: str + """ + The inner MCP method name. + """ + params: Optional[Dict[str, Any]] = None + """ + Optional inner MCP params. + + If omitted or set to `null`, the inner MCP message has no params. + """ + field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_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) + """ + + class CancelRequestNotification(BaseModel): request_id: Annotated[Optional[Union[int, str]], Field(alias="requestId")] """ @@ -3069,25 +3149,14 @@ def skip_invalid_items_validator(cls, v: Any, handler: ValidatorFunctionWrapHand return skip_invalid_items(v, handler, info) -class ConnectMcpRequest(BaseModel): +class MessageMcpRequest(BaseModel): server_id: Annotated[str, Field(alias="serverId")] """ - The ACP MCP server ID that was provided by the component declaring the MCP server. - """ - field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_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) + The declared ACP MCP server receiving this request. """ - - -class MessageMcpRequest(BaseModel): - connection_id: Annotated[str, Field(alias="connectionId")] + request_id: Annotated[str, Field(alias="requestId")] """ - The MCP-over-ACP connection this message is sent on. + The caller-generated identifier for the inner MCP request. """ method: str """ @@ -3623,6 +3692,96 @@ class AvailableCommandInput(RootModel[UnstructuredCommandInput]): """ +class SubagentSessionCapabilities(BaseModel): + cancel: Optional[SessionCancelCapabilities] = None + """ + Permits the client to cancel this child's current work without ending + the session. Omitted or `null` means unsupported; an object (including + `{}`) means supported. + """ + field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_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. + """ + + @field_validator("cancel", mode="wrap") + @classmethod + def use_default_on_error_validator(cls, v: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo) -> Any: + return use_default_on_error(v, handler, info) + + +class StateUpdate1(RunningStateUpdate): + state: Literal["running"] = "running" + + +class StateUpdate2(IdleStateUpdate): + state: Literal["idle"] = "idle" + + +class StateUpdate3(RequiresActionStateUpdate): + state: Literal["requires_action"] = "requires_action" + + +class StateUpdate4(UnknownStateUpdate): + state: Literal["unknown"] = "unknown" + + +class SubagentUpdate(BaseModel): + session_id: Annotated[str, Field(alias="sessionId")] + """ + The opaque session ID identifying the child in all ACP messages. + """ + title: Optional[str] = None + """ + The parent's human-readable display title for this child. It need not be unique. + + Omitted means unchanged; `null` clears it. If unset, the Client chooses + a fallback presentation. + """ + description: Optional[str] = None + """ + The parent's human-readable description of the child's role or purpose. + + Omitted means unchanged; `null` clears it. If unset, the Client chooses + a fallback presentation. This is current display metadata, not the + history of instructions sent to the child. + """ + capabilities: Optional[SubagentSessionCapabilities] = None + """ + Client-initiated session mutations permitted for this subagent session. + + Omitted means unchanged; `null` clears the capability set and disables + child mutations. If never supplied, no session mutations are permitted. + Read-only operations retain their normal protocol semantics and capability + requirements. A concrete object replaces the whole capability set. + """ + state: Optional[Union[StateUpdate1, StateUpdate2, StateUpdate3, StateUpdate4, StateUpdate5]] = None + """ + Current state snapshot for the child session. + + Omitted means unchanged; `null` clears the current activity without + asserting idle or sending an `unknown` snapshot. A concrete state + replaces the previous state object wholesale. If never supplied, the + current activity is unset/unconfirmed. + """ + field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_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) + Omitted means unchanged; `null` removes the metadata. + """ + + @field_validator("capabilities", "description", "state", "title", mode="wrap") + @classmethod + def use_default_on_error_validator(cls, v: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo) -> Any: + return use_default_on_error(v, handler, info) + + class SessionConfigOptionsCapabilities(BaseModel): boolean: Optional[BooleanConfigOptionCapabilities] = None """ @@ -3999,6 +4158,17 @@ class AcceptElicitationResponse(ElicitationAcceptAction): action: Literal["accept"] = "accept" +class McpErrorResponse(BaseModel): + error: McpError + """ + Required, non-null MCP error object. + """ + field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_meta")] = None + """ + Optional ACP carrier metadata. + """ + + class TextDocumentContentChangeEvent(BaseModel): range: Optional[Range] = None """ @@ -4249,6 +4419,10 @@ class AgentPlanUpdate(Plan): session_update: Annotated[Literal["plan"], Field(alias="sessionUpdate")] = "plan" +class SessionUpdateSubagentUpdate(SubagentUpdate): + session_update: Annotated[Literal["subagent_update"], Field(alias="sessionUpdate")] = "subagent_update" + + class ContentChunk(BaseModel): content: Annotated[ Union[ @@ -4409,6 +4583,90 @@ class CompactionSummaryChunk(BaseModel): """ +class SessionMessage(BaseModel): + message_id: Annotated[str, Field(alias="messageId")] + """ + Identifier of this message within the enclosing session's transcript. + """ + sender_session_id: Annotated[Optional[str], Field(alias="senderSessionId")] = None + """ + Optional sending session identity; omission or `null` retains a known value. + """ + recipient_session_id: Annotated[Optional[str], Field(alias="recipientSessionId")] = None + """ + Optional receiving session identity; omission or `null` retains a known value. + """ + content: Optional[ + List[ + Annotated[ + Union[ + TextContentBlock, + ImageContentBlock, + AudioContentBlock, + ResourceContentBlock, + EmbeddedResourceContentBlock, + ], + Field(discriminator="type"), + ] + ] + ] = None + """ + Omitted leaves content unchanged; `null` or `[]` clears it. + A non-empty array replaces all content. + """ + field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_meta")] = None + """ + Omitted leaves metadata unchanged; `null` removes it. + + Implementations MUST NOT make assumptions about values in `_meta`. + """ + + @field_validator("recipient_session_id", "sender_session_id", mode="wrap") + @classmethod + def use_default_on_error_validator(cls, v: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo) -> Any: + return use_default_on_error(v, handler, info) + + @field_validator("content", mode="wrap") + @classmethod + def skip_invalid_items_validator(cls, v: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo) -> Any: + return skip_invalid_items(v, handler, info) + + +class SessionMessageChunk(BaseModel): + message_id: Annotated[str, Field(alias="messageId")] + """ + Identifier of this message within the enclosing session's transcript. + """ + sender_session_id: Annotated[Optional[str], Field(alias="senderSessionId")] = None + """ + Optional sending session identity; omission or `null` retains a known value. + """ + recipient_session_id: Annotated[Optional[str], Field(alias="recipientSessionId")] = None + """ + Optional receiving session identity; omission or `null` retains a known value. + """ + content: Annotated[ + Union[ + TextContentBlock, ImageContentBlock, AudioContentBlock, ResourceContentBlock, EmbeddedResourceContentBlock + ], + Field(discriminator="type"), + ] + """ + A single content block appended to the message. + """ + field_meta: Annotated[Optional[Dict[str, Any]], Field(alias="_meta")] = None + """ + Optional and nullable chunk-scoped metadata; omitted or `null` means none. + + Implementations MUST NOT make assumptions about values in `_meta`. + """ + + @field_validator("recipient_session_id", "sender_session_id", mode="wrap") + @classmethod + def use_default_on_error_validator(cls, v: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo) -> Any: + return use_default_on_error(v, handler, info) + + class ClientSessionCapabilities(BaseModel): compaction: Optional[CompactionCapabilities] = None """ @@ -4920,6 +5178,14 @@ class SessionUpdateCompactionSummaryChunk(CompactionSummaryChunk): ) +class SessionUpdateSessionMessage(SessionMessage): + session_update: Annotated[Literal["session_message"], Field(alias="sessionUpdate")] = "session_message" + + +class SessionUpdateSessionMessageChunk(SessionMessageChunk): + session_update: Annotated[Literal["session_message_chunk"], Field(alias="sessionUpdate")] = "session_message_chunk" + + class ToolCall(BaseModel): tool_call_id: Annotated[str, Field(alias="toolCallId")] """ @@ -5029,6 +5295,19 @@ class ClientCapabilities(BaseModel): Optional. Omitted or `null` both mean the client does not advertise any session-related extensions. """ + subagents: Optional[SubagentCapabilities] = None + """ + **UNSTABLE** + + This capability is not part of the spec yet, and may be removed or changed at any point. + + Whether the client understands exposed subagent sessions. + + Optional and nullable. Omitted or `null` both mean the client does not + advertise support. + Supplying `{}` means the client understands child associations, work-state + snapshots, session-directed messages, and restricted-session semantics. + """ plan: Optional[PlanCapabilities] = None """ **UNSTABLE** @@ -5084,7 +5363,7 @@ class ClientCapabilities(BaseModel): See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) """ - @field_validator("auth", "elicitation", "fs", "nes", "plan", "session", "terminal", mode="wrap") + @field_validator("auth", "elicitation", "fs", "nes", "plan", "session", "subagents", "terminal", mode="wrap") @classmethod def use_default_on_error_validator(cls, v: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo) -> Any: return use_default_on_error(v, handler, info) @@ -5148,11 +5427,10 @@ class ClientResponseMessage(BaseModel): ReleaseTerminalResponse, WaitForTerminalExitResponse, KillTerminalResponse, - ConnectMcpResponse, - DisconnectMcpResponse, Union[ AcceptElicitationResponse, DeclineElicitationResponse, CancelElicitationResponse, OtherElicitationResponse ], + Union[McpResultResponse, McpErrorResponse], Any, ] """ @@ -5593,6 +5871,9 @@ class SessionNotification(BaseModel): SessionUpdateNotice, SessionUpdateCompactionUpdate, SessionUpdateCompactionSummaryChunk, + SessionUpdateSubagentUpdate, + SessionUpdateSessionMessage, + SessionUpdateSessionMessageChunk, ], Field(discriminator="session_update"), ] @@ -5638,7 +5919,6 @@ class ClientRequest(BaseModel): StartNesRequest, SuggestNesRequest, CloseNesRequest, - MessageMcpRequest, Union[SetSessionConfigOptionBooleanRequest, SetSessionConfigOptionSelectRequest], Any, ] @@ -5667,9 +5947,7 @@ class AgentRequest(BaseModel): ReleaseTerminalRequest, WaitForTerminalExitRequest, KillTerminalRequest, - ConnectMcpRequest, MessageMcpRequest, - DisconnectMcpRequest, Union[ Union[CreateOtherSessionElicitationRequest, CreateOtherRequestElicitationRequest], Union[CreateFormSessionElicitationRequest, CreateFormRequestElicitationRequest], @@ -5740,7 +6018,7 @@ class AgentNotification(BaseModel): """ The notification method name. """ - params: Optional[Union[SessionNotification, CompleteElicitationNotification, MessageMcpNotification, Any]] = None + params: Optional[Union[SessionNotification, CompleteElicitationNotification, Any]] = None """ Method-specific notification parameters. """ @@ -5785,6 +6063,8 @@ class AgentResponse(RootModel[Union[AgentResponseMessage, AgentErrorMessage]]): """ +MessageMcpResponse = Annotated[Union[McpResultResponse, McpErrorResponse], Field(union_mode="left_to_right")] + PermissionOptionKind = Literal["allow_once", "allow_always", "reject_once", "reject_always"] PlanEntryPriority = Literal["high", "medium", "low"] PlanEntryStatus = Literal["pending", "in_progress", "completed"] diff --git a/tests/conftest.py b/tests/conftest.py index 38c501f3..61933534 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,10 +10,8 @@ AcceptElicitationResponse, AuthenticateResponse, CompleteElicitationNotification, - ConnectMcpResponse, CreateElicitationResponse, CreateTerminalResponse, - DisconnectMcpResponse, ElicitationMode, InitializeResponse, KillTerminalResponse, @@ -237,22 +235,11 @@ async def complete_elicitation(self, elicitation_id: str, **kwargs: Any) -> None CompleteElicitationNotification(elicitation_id=elicitation_id, field_meta=kwargs or None) ) - async def connect_mcp(self, server_id: str, **kwargs: Any) -> ConnectMcpResponse: - raise RequestError.method_not_found("mcp/connect") - - async def disconnect_mcp(self, connection_id: str, **kwargs: Any) -> DisconnectMcpResponse: - raise RequestError.method_not_found("mcp/disconnect") - async def mcp_message( - self, connection_id: str, method: str, params: dict[str, Any] | None = None, **kwargs: Any + self, server_id: str, request_id: str, method: str, params: dict[str, Any] | None = None, **kwargs: Any ) -> Any: raise RequestError.method_not_found("mcp/message") - async def notify_mcp( - self, connection_id: str, method: str, params: dict[str, Any] | None = None, **kwargs: Any - ) -> None: - raise RequestError.method_not_found("mcp/message") - async def ext_method(self, method: str, params: dict) -> dict: self.ext_calls.append((method, params)) if method == "example.com/ping": diff --git a/tests/test_protocol_method_coverage.py b/tests/test_protocol_method_coverage.py index 3c5b8216..8e059975 100644 --- a/tests/test_protocol_method_coverage.py +++ b/tests/test_protocol_method_coverage.py @@ -113,39 +113,32 @@ async def handler(**kwargs): @pytest.mark.asyncio -async def test_mcp_connection_lifecycle(connect, client): - client.connect_mcp = AsyncMock(return_value=schema.ConnectMcpResponse(connection_id="c")) - client.disconnect_mcp = AsyncMock(return_value=None) - connection, _ = connect(use_unstable_protocol=True) - assert (await connection.connect_mcp(server_id="m", trace="test")).connection_id == "c" - client.connect_mcp.assert_awaited_once_with(server_id="m", trace="test") - assert await connection.disconnect_mcp(connection_id="c") == schema.DisconnectMcpResponse() - client.disconnect_mcp.assert_awaited_once_with(connection_id="c") - - -@pytest.mark.asyncio -@pytest.mark.parametrize("response", [None, {}, {"tools": []}, [1, "x"], False, 42]) -@pytest.mark.parametrize("direction", ["agent", "client"]) -async def test_mcp_requests_and_notifications_share_method(connect, agent, client, direction, response): - receiver = agent if direction == "agent" else client - receiver.mcp_message = AsyncMock(return_value=response) +@pytest.mark.parametrize("result", [None, {}, {"tools": []}, [1, "x"], False, 42]) +async def test_mcp_requests_and_notifications_share_method(connect, agent, client, result): received = asyncio.Event() async def notify_mcp(**kwargs): - assert kwargs == {"connection_id": "c", "method": "notifications/initialized", "params": None, "trace": "test"} + assert kwargs == { + "server_id": "s", + "request_id": "r", + "method": "notifications/progress", + "params": None, + "trace": "test", + } received.set() - receiver.notify_mcp = notify_mcp + agent.notify_mcp = notify_mcp + client.mcp_message = AsyncMock(return_value=schema.McpResultResponse(result=result)) agent_side, client_side = connect(use_unstable_protocol=True) - sender = client_side if direction == "agent" else agent_side - assert ( - await sender.mcp_message(connection_id="c", method="tools/list", params={"cursor": "next"}, trace="test") - == response + response = await agent_side.mcp_message( + server_id="s", request_id="r", method="tools/list", params={"cursor": "next"}, trace="test" ) - receiver.mcp_message.assert_awaited_once_with( - connection_id="c", method="tools/list", params={"cursor": "next"}, trace="test" + assert isinstance(response, schema.McpResultResponse) + assert response.result == result + client.mcp_message.assert_awaited_once_with( + server_id="s", request_id="r", method="tools/list", params={"cursor": "next"}, trace="test" ) - await sender.notify_mcp(connection_id="c", method="notifications/initialized", trace="test") + await client_side.notify_mcp(server_id="s", request_id="r", method="notifications/progress", trace="test") await asyncio.wait_for(received.wait(), 2) diff --git a/tests/test_stateless_mcp.py b/tests/test_stateless_mcp.py new file mode 100644 index 00000000..4c7e6068 --- /dev/null +++ b/tests/test_stateless_mcp.py @@ -0,0 +1,247 @@ +"""Stateless MCP wire contracts and loopback behavior for both ACP versions.""" + +import asyncio +from typing import Any, cast + +import pytest +import pytest_asyncio +from pydantic import TypeAdapter, ValidationError + +from acp import schema as v1_schema +from acp._transport import memory_transport_pair +from acp.agent.connection import AgentSideConnection +from acp.client.connection import ClientSideConnection +from acp.exceptions import RequestError +from acp.experimental import v2 +from acp.interfaces import Agent as AgentProtocol +from acp.interfaces import Client as ClientProtocol + + +@pytest.fixture(params=[v1_schema, v2.schema], ids=["v1", "v2"]) +def schema(request): + return request.param + + +@pytest.mark.parametrize( + "result", [None, False, 0, "text", [None], {"resultType": "input_required", "requestState": {"opaque": None}}] +) +def test_result_carrier_preserves_opaque_json_and_prefers_result(schema, result): + carrier = TypeAdapter(schema.MessageMcpResponse).validate_python({ + "result": result, + "error": {"code": -32000, "message": "ignored"}, + "unknown": True, + "_meta": "invalid", + }) + assert isinstance(carrier, schema.McpResultResponse) + assert carrier.model_dump(mode="json", by_alias=True, exclude_none=True, exclude_unset=True) == {"result": result} + + +@pytest.mark.parametrize("data", [{}, {"data": None}, {"data": {"nested": None}}]) +def test_mcp_error_preserves_data_presence_and_extensions(schema, data): + error = {"code": -32000, "message": "MCP error, not ACP auth", "vendor": {"extra": None}, "nullable": None, **data} + carrier = TypeAdapter(schema.MessageMcpResponse).validate_python({"error": error, "_meta": {"trace": "outer"}}) + assert isinstance(carrier, schema.McpErrorResponse) + assert carrier.model_dump(mode="json", by_alias=True, exclude_none=True, exclude_unset=True) == { + "error": error, + "_meta": {"trace": "outer"}, + } + + +@pytest.mark.parametrize( + "value", + [ + None, + {}, + {"error": None}, + {"error": {}}, + {"error": {"code": True, "message": "bad"}}, + {"error": {"code": "1", "message": "bad"}}, + ], +) +def test_invalid_carriers_are_rejected(schema, value): + with pytest.raises(ValidationError): + TypeAdapter(schema.MessageMcpResponse).validate_python(value) + + +@pytest.mark.parametrize("model_name", ["MessageMcpRequest", "MessageMcpNotification"]) +@pytest.mark.parametrize( + "invalid", + [ + {"connectionId": "old", "method": "ping"}, + {"serverId": "s", "method": "ping"}, + {"serverId": "s", "requestId": 1, "method": "ping"}, + {"serverId": "s", "requestId": "r", "method": "ping", "params": []}, + ], +) +def test_envelope_requires_server_and_logical_request_ids(schema, model_name, invalid): + with pytest.raises(ValidationError): + getattr(schema, model_name).model_validate(invalid) + + +@pytest_asyncio.fixture +async def peers(schema): + class Agent: + def __init__(self): + self.notifications = asyncio.Queue() + + async def initialize(self, **kwargs): + if schema is v2.schema: + return schema.InitializeResponse( + protocol_version=2, info=schema.Implementation(name="test", version="1") + ) + return schema.InitializeResponse(protocol_version=1) + + async def notify_mcp(self, **kwargs): + await self.notifications.put(kwargs) + + class Client: + def __init__(self): + self.outcome: Any = {"result": None} + self.calls = [] + + async def mcp_message(self, **kwargs): + self.calls.append(kwargs) + if isinstance(self.outcome, Exception): + raise self.outcome + if callable(self.outcome): + return await self.outcome(**kwargs) + return self.outcome + + agent, client = Agent(), Client() + a_transport, c_transport = memory_transport_pair() + if schema is v2.schema: + a = v2.AgentSideConnection(cast(AgentProtocol, agent), a_transport) + c = v2.ClientSideConnection(cast(ClientProtocol, client), c_transport) + await c.initialize(protocol_version=2, info=schema.Implementation(name="test", version="1")) + else: + a = AgentSideConnection(cast(AgentProtocol, agent), a_transport, use_unstable_protocol=True) + c = ClientSideConnection(cast(ClientProtocol, client), c_transport, use_unstable_protocol=True) + try: + yield a, c, agent, client + finally: + await c.close() + await a.close() + + +@pytest.mark.asyncio +async def test_request_metadata_notifications_and_error_domains(peers, schema): + a, c, agent, client = peers + inner = { + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {}, + "progressToken": "p", + } + } + + async def handle(**kwargs): + await c.notify_mcp( + server_id=kwargs["server_id"], + request_id=kwargs["request_id"], + method="notifications/progress", + params={"progressToken": "p", "progress": 1}, + trace="notification", + ) + return {"error": {"code": -32000, "message": "inner", "data": None, "vendor": 1}} + + client.outcome = handle + response = await a.mcp_message( + server_id="s", request_id="logical", method="tools/call", params=inner, trace="outer" + ) + assert isinstance(response, schema.McpErrorResponse) + assert response.error.code == -32000 + assert response.error.model_dump(exclude_unset=True) == { + "code": -32000, + "message": "inner", + "data": None, + "vendor": 1, + } + assert client.calls == [ + {"server_id": "s", "request_id": "logical", "method": "tools/call", "params": inner, "trace": "outer"} + ] + notification = await asyncio.wait_for(agent.notifications.get(), 1) + assert notification == { + "server_id": "s", + "request_id": "logical", + "method": "notifications/progress", + "params": {"progressToken": "p", "progress": 1}, + "trace": "notification", + } + client.outcome = RequestError(-33001, "server unavailable") + with pytest.raises(RequestError) as error: + await a.mcp_message(server_id="s", request_id="next", method="tools/list") + assert isinstance(error.value, RequestError) + assert error.value.code == -33001 + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "outcome", [{"result": None}, {"result": {"resultType": "input_required", "requestState": {"opaque": None}}}] +) +async def test_loopback_preserves_null_and_mrtr_result(peers, outcome): + a, _, _, client = peers + client.outcome = outcome + response = await a.mcp_message(server_id="s", request_id="r", method="tools/call") + assert response.result == outcome["result"] + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "outcome", [None, {}, {"result": None, "error": {"code": 1, "message": "both"}}, {"error": None}] +) +async def test_invalid_outgoing_outcomes_are_binding_failures(peers, outcome): + a, _, _, client = peers + client.outcome = outcome + with pytest.raises(RequestError) as error: + await a.mcp_message(server_id="s", request_id="r", method="tools/call") + assert isinstance(error.value, RequestError) + assert error.value.code == -33002 + + +@pytest.mark.asyncio +async def test_cancellation_is_per_outer_request(peers): + a, _, _, client = peers + started, cleaned = asyncio.Event(), asyncio.Event() + + async def handle(request_id, **kwargs): + if request_id == "sibling": + return {"result": None} + started.set() + try: + await asyncio.Event().wait() + finally: + cleaned.set() + + client.outcome = handle + pending = asyncio.create_task(a.mcp_message(server_id="s", request_id="long", method="subscriptions/listen")) + await asyncio.wait_for(started.wait(), 1) + assert (await a.mcp_message(server_id="s", request_id="sibling", method="tools/list")).result is None + pending.cancel() + with pytest.raises(asyncio.CancelledError): + await pending + await asyncio.wait_for(cleaned.wait(), 1) + + +@pytest.mark.asyncio +async def test_removed_binding_methods_and_reverse_calls_are_not_exposed(peers): + a, c, _, _ = peers + assert not hasattr(a, "connect_mcp") + assert not hasattr(a, "disconnect_mcp") + assert not hasattr(a, "notify_mcp") + assert not hasattr(c, "mcp_message") + for method in ("mcp/connect", "mcp/disconnect"): + with pytest.raises(RequestError) as error: + await a._conn.send_request(method, {"serverId": "s"}) + assert isinstance(error.value, RequestError) + assert error.value.code == -32601 + + +@pytest.mark.asyncio +@pytest.mark.parametrize("data", [{}, {"data": None}]) +@pytest.mark.parametrize("extensions", [{}, {"vendor": None}]) +async def test_loopback_preserves_error_data_presence(peers, data, extensions): + a, _, _, client = peers + error = {"code": -32602, "message": "inner", **data, **extensions} + client.outcome = {"error": error} + response = await a.mcp_message(server_id="s", request_id="r", method="tools/call") + assert response.error.model_dump(exclude_unset=True) == error diff --git a/tests/test_v2_routing.py b/tests/test_v2_routing.py index 976d0731..af04de61 100644 --- a/tests/test_v2_routing.py +++ b/tests/test_v2_routing.py @@ -24,8 +24,8 @@ def test_protocols_cover_wire_methods_and_match_connection_signatures(protocol, assert inspect.signature(getattr(protocol, spec.handler)) == inspect.signature( getattr(connection, spec.handler) ) - assert "mcp/message" in {spec.method for spec in requests} - assert "mcp/message" in {spec.method for spec in notifications} + assert ("mcp/message" in {spec.method for spec in requests}) == (protocol is v2.Client) + assert ("mcp/message" in {spec.method for spec in notifications}) == (protocol is v2.Agent) @pytest.mark.asyncio @@ -37,6 +37,7 @@ class Agent(v2.Agent): with pytest.raises(RequestError) as error: await router("session/new", {"cwd": "/workspace"}, False) assert isinstance(error.value, RequestError) + assert isinstance(error.value, RequestError) assert error.value.code == -32601 assert await router("session/cancel", {"sessionId": "s"}, True) is None @@ -66,7 +67,10 @@ async def mcp_message(self, **kwargs: Any) -> Any: await router("session/prompt", {"sessionId": "s", "prompt": []}, False) assert agent.calls == 1 assert isinstance(await router("auth/logout", {}, False), v2.schema.LogoutAuthResponse) - assert await router("mcp/message", {"connectionId": "m", "method": "ping"}, False) is None + with pytest.raises(RequestError) as error: + await router("mcp/message", {"serverId": "s", "requestId": "r", "method": "ping"}, False) + assert isinstance(error.value, RequestError) + assert error.value.code == -32601 @pytest.mark.asyncio diff --git a/tests/test_v2_runtime.py b/tests/test_v2_runtime.py index d93da0b8..6f623367 100644 --- a/tests/test_v2_runtime.py +++ b/tests/test_v2_runtime.py @@ -144,8 +144,10 @@ async def cancel_session(self, session_id: str, **kwargs: Any) -> None: notification = v2.schema.CancelSessionNotification(session_id=session_id, **kwargs) await self.notifications.put(("cancel", notification)) - async def notify_mcp(self, connection_id: str, method: str, **kwargs: Any) -> None: - notification = v2.schema.MessageMcpNotification(connection_id=connection_id, method=method, **kwargs) + async def notify_mcp(self, server_id: str, request_id: str, method: str, **kwargs: Any) -> None: + notification = v2.schema.MessageMcpNotification( + server_id=server_id, request_id=request_id, method=method, **kwargs + ) await self.notifications.put(("mcp", notification)) @@ -403,12 +405,12 @@ async def test_extension_and_notification_names_are_explicit() -> None: await client_connection.send_extension_request("vendor/do") await client_connection.cancel_session(session_id="session-1") - await client_connection.notify_mcp(connection_id="mcp-1", method="notifications/progress") + await client_connection.notify_mcp(server_id="mcp-1", request_id="r", method="notifications/progress") cancel_kind, cancel = await asyncio.wait_for(agent.notifications.get(), timeout=1) mcp_kind, mcp = await asyncio.wait_for(agent.notifications.get(), timeout=1) assert (cancel_kind, cancel.session_id) == ("cancel", "session-1") - assert (mcp_kind, mcp.connection_id) == ("mcp", "mcp-1") + assert (mcp_kind, mcp.server_id) == ("mcp", "mcp-1") finally: await client_connection.close() await agent_connection.close()