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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 26 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ agent-client-protocol-trace-viewer = { path = "src/agent-client-protocol-trace-v
yopo = { package = "agent-client-protocol-yopo", path = "src/yopo" }

# Protocol
agent-client-protocol-schema = { version = "=1.9.1", default-features = false, features = ["tracing"] }
# Draft cross-repository validation; replace with the released schema before publishing.
agent-client-protocol-schema = { git = "https://github.com/agentclientprotocol/agent-client-protocol", rev = "e5c36d2671fd355f983533bc83b5feb7981d25a6", default-features = false, features = ["tracing"] }

# Core async runtime
tokio = { version = "1.52", default-features = false }
Expand All @@ -44,6 +45,7 @@ tokio-util = { version = "0.7", features = ["compat"] }
async-tungstenite = { version = "0.35.0", default-features = false, features = ["tokio-rustls-webpki-roots"] }

# Serialization
base64 = "0.23"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = { version = "1", features = ["preserve_order", "raw_value"] }
schemars = { version = "1.0", features = ["derive"] }
Expand Down Expand Up @@ -71,6 +73,7 @@ url = "2.5"
async-io = "2"
async-process = "2"
async-stream = "0.3.6"
async-channel = "2"
blocking = "1"
chrono = "0.4"
futures = "0.3.32"
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ attaches one while forking. Successful v2 attachments remain active for the
connection lifetime, and all three builders expose `on_proxy_session_start` to
forward proxied setup without coupling later session events to that response.

The native transport targets MCP 2026-07-28: requests carry their own metadata
and logical IDs, with request-scoped notifications and cancellation rather than
an MCP connection lifecycle. See [Native MCP-over-ACP](./md/mcp-over-acp.md)
for the direct rmcp example and current resource-limit caveats.

**Proxy orchestration**

- [`agent-client-protocol-conductor`](./src/agent-client-protocol-conductor/) – Binary and library that manages chains of proxy components.
Expand Down
9 changes: 6 additions & 3 deletions justfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# Keep file-based snapshots inside this checkout, even in nested worktrees.
export CARGO_WORKSPACE_DIR := justfile_directory()

# Build binaries needed for integration tests
prep-tests:
cargo build -p agent-client-protocol-conductor --all-features
cargo build -p agent-client-protocol-test --bin testy --all-features
cargo build -p agent-client-protocol-test --bin mcp-echo-server --example arrow_proxy --all-features

# Run all tests (requires prep-tests first)
test: prep-tests
cargo test --all --workspace --all-features
# Run all tests, or pass a test-name filter / cargo test arguments.
test *args: prep-tests
cargo test --all --workspace --all-features {{args}}
2 changes: 2 additions & 0 deletions md/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

- [Transport Architecture](./transport-architecture.md)
- [HTTP / WebSocket Transport](./http-transport.md)
- [Native MCP-over-ACP](./mcp-over-acp.md)

# Conductor (agent-client-protocol-conductor)

Expand All @@ -31,6 +32,7 @@

# Reference

- [Migrating the Native MCP Transport](./migration-stateless-mcp.md)
- [Migrating the rmcp Integration to v4](./migration-rmcp-v4.md)
- [Migrating to v2.0](./migration_v2.0.md)
- [Migrating to v0.11](./migration_v0.11.x.md)
Expand Down
124 changes: 87 additions & 37 deletions md/mcp-bridge.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
# MCP-over-ACP Compatibility Bridge
# Stateless MCP-over-ACP HTTP Adapter

`agent-client-protocol-polyfill::mcp_over_acp::McpOverAcpPolyfill` adapts the
native ACP MCP transport for a final agent that accepts HTTP MCP
servers. MCP adaptation is explicit and is not built into the conductor.
native ACP MCP transport for a final agent with an MCP 2026-07-28 HTTP client.
MCP adaptation is explicit and is not built into the conductor. There is no
fallback to older MCP revisions.

The component-facing side of the bridge always uses the opt-in native protocol:

- Servers are declared as `McpServer::Acp` with a `serverId`.
- Connections use `mcp/connect`, `mcp/message`, and `mcp/disconnect`.
- `mcp/disconnect` is a request with a response.
- Each operation uses `mcp/message` with `serverId` and a logical `requestId`.
- The provider sends notifications for that operation; the final ACP response
carries its MCP result or error.
- ACP request cancellation stops only that operation. There is no MCP
initialize/connect/disconnect or session-header exchange.

The SDK-local underscore-prefixed method family and HTTP declarations with a
special URL scheme have been retired. The polyfill now translates native
Expand Down Expand Up @@ -88,17 +92,20 @@ support and rejects any native declaration that is nevertheless supplied.
For each schema-selected `McpServer::Acp` entry in a session setup request, the
polyfill:

1. Creates or reuses a connection-scoped localhost bridge endpoint for the
`serverId` and replaces the declaration with the HTTP transport for the
final agent.
2. Retains the native `serverId` so connections can be routed back to the
component that provided the server.
3. Opens the endpoint's native connection by sending `mcp/connect` with that
server ID toward the provider.
4. Relays requests and notifications through `mcp/message`, using the returned
`connectionId` for that active MCP connection.
5. Sends an `mcp/disconnect` request when the local transport closes and removes
the connection from the bridge.
1. Creates or reuses one connection-scoped loopback listener and replaces the
declaration with an HTTP URL whose path encodes the non-secret `serverId`.
No per-server listener or route-table entry is allocated.
2. Routes each request back to the component that owns that native registration.
The provider, not possession of the URL, decides whether it still exists.
3. Adds a runtime-only bearer credential derived from the connection secret and
server ID to the HTTP declaration's headers. The endpoint authenticates and
checks supplied Origin headers before reading the request body. Credentials
never appear in URLs; an ephemeral port alone is not access control.
4. For each POST, allocates a unique logical MCP request ID and sends
`mcp/message` to the provider. Two HTTP clients may use the same external
JSON-RPC ID without sharing routing or state.
5. Relays notifications and a final result/error for that request. Closing
its HTTP response cancels the corresponding ACP request, not the listener.

Enable the polyfill crate's `unstable_session_fork` feature when adapting fork
requests. Stable v1 setup includes `session/new`, `session/load`, and
Expand All @@ -108,39 +115,82 @@ versions include `session/fork` when `unstable_session_fork` is enabled.
Declarations using another transport are left unchanged, including extension
transports represented by v2's `McpServer::Other`.

Endpoints are cached by `serverId` across session setup requests on the ACP
connection. The output declaration is rebuilt for each occurrence, preserving
that occurrence's `name`, `_meta`, and other unmodified extension fields even
when its endpoint is reused.
The same server ID derives the same route and credential on this ACP connection.
The output declaration is rebuilt for each occurrence, preserving its `name`,
`_meta`, and other unmodified extension fields. Failed setup and declaration
churn cannot accumulate per-server endpoint allocations. A server ID must never
be rebound to a different registration during the connection's lifetime.

The native wire envelopes are documented in the [SDK Protocol
Reference](./protocol.md#native-mcp-over-acp).

## HTTP Mode

`McpOverAcpPolyfill::http()` is the default compatibility shape. It replaces
the native declaration with an HTTP MCP URL at `http://127.0.0.1:PORT`. The
embedded server accepts MCP POST requests and an SSE GET stream at `/`, retaining
JSON-RPC batch frames and correlating each POST with its response.
the native declaration with an HTTP MCP URL at `http://127.0.0.1:PORT/<route>`. The
embedded server accepts a single JSON-RPC request per POST at that route, returning
JSON for a terminal-only response or SSE for a request that emits notifications.
GET and DELETE return 405. Batches and client-originated JSON-RPC responses
are rejected; there is no standalone GET event stream or MCP session ID.

```rust,ignore
let bridge = McpOverAcpPolyfill::http();
```

The listener is bound only on loopback and uses an ephemeral port. It does not
implement resumable SSE event IDs.
Clients must send the bearer header from the declaration, both JSON and SSE
Accept types, and the required MCP protocol-version, method, and applicable
name headers. Mirrored names support MCP's Base64 sentinel encoding. Missing,
duplicate, or mismatched routing headers are rejected.

## Lifecycle and Failure Behavior

Each bridge endpoint receives a unique `connectionId` from `mcp/connect`. The
polyfill keeps a connection map until the endpoint's transport task closes,
then removes the entry, sends `mcp/disconnect`, and observes its response.
Request failures use the corresponding request's error path; notifications are
never answered with synthetic errors.
The listener is bound only on loopback. Resumable SSE event IDs are not part of
the target MCP revision. Subscription IDs inside
`_meta["io.modelcontextprotocol/subscriptionId"]` are translated back to the
HTTP request's original ID in notifications and graceful completion results;
other metadata, progress tokens, and opaque retry state are not rewritten.

A reverse `mcp/message` request for an unknown `connectionId` receives
`Invalid params`. A reverse notification for an unknown connection is ignored,
as required for JSON-RPC notifications.
## Lifecycle and Failure Behavior

The polyfill does not infer or store ACP session IDs. Association is carried by
the declared `serverId` and the resulting active `connectionId`.
Each POST owns a pending native request, not an MCP session. Closing its response
stream cancels that request. A terminal outcome ends native work, but HTTP
admission remains held until the response body is consumed or dropped. The
listening endpoint remains available for later requests; releasing the native
registration makes requests through its old URL fail rather than reviving it.

The adapter limits each response's queued notifications to 16 messages and
256 KiB of serialized data, admits at most 64 HTTP responses at a time, and caps
request bodies and terminal payloads at 1 MiB. The body owns the admission permit,
including while a client is not reading. A separate terminal-response path
avoids stranding completion behind a full queue. Overflow explicitly fails and
cancels that operation without blocking the shared runner or dropping events silently.

The bridge unwraps the ACP outcome carrier before creating the HTTP JSON-RPC
response. MCP error codes/data stay MCP errors; binding failures use their
separate error codes. Queued notifications precede the terminal response.

Unknown or late provider notifications are ignored; reverse MCP requests are
not supported. The adapter does not infer ACP session IDs or maintain MCP
initialization state.

## Native-tool re-export contract

The adapter creates a **new HTTP endpoint for native tool semantics**. It does
not preserve another HTTP gateway's parameter-header routing or authorization.
It removes transport-only `x-mcp-header` annotations from actual schema positions
in `tools/list` results. Argument schemas and validation keywords, tool ordering,
pagination, metadata, and similarly named properties/example/default data remain
unchanged. Annotated native tools remain listed and callable.

Each `tools/call` issues exactly one native call, without hidden descriptor reads
or a prior client `tools/list` requirement. Native passthrough does not transform
the original descriptors. `Mcp-Param-*` headers are rejected; they confer no
authority on this endpoint. Standard MCP method/name/version header checks remain.

If a deployment depends on an existing HTTP gateway's mirrored-parameter policy,
it must implement that policy at this endpoint or decline this re-export.

## Validation scope

This does not establish every optional MCP feature or complete HTTP conformance.
In particular, HTTP response limits alone do not prove native transport bounds.
Owned operation cleanup and end-to-end bounded transport are stabilization gates;
see [Native MCP-over-ACP](./mcp-over-acp.md).
Loading
Loading