Skip to content

feat(acp)!: own stateless MCP operations and bound transports - #376

Draft
benbrandt wants to merge 5 commits into
mainfrom
work/mcp-over-acp-stateless
Draft

benbrandt wants to merge 5 commits into
mainfrom
work/mcp-over-acp-stateless

Conversation

@benbrandt

@benbrandt benbrandt commented Sep 24, 2026 •

Copy link
Copy Markdown
Member

Draft: MCP 2026-07-28 only, with major-version API changes

Companion schema/RFD: agentclientprotocol/agent-client-protocol#2223

Builds on merged rmcp upgrade #372 and supersedes the connection-oriented checkpoint #375. This transport has no legacy MCP compatibility mode.

Implementation

  • Server-addressed, request-scoped MCP calls and notifications; remove the MCP connect/disconnect lifecycle and reverse requests.
  • Explicit inner MCP result/error carriers, distinct from outer ACP binding failures. Preserve MCP codes, null data, and extensions without interpreting them as ACP authentication errors. Traces retain error-domain provenance.
  • Reusable request-native services. The rmcp application service is initialized lazily once; each request owns execution, cancellation, notifications, and supervised cleanup.
  • Scoped mutable and concurrent tools acknowledge actual closure cleanup before the logical request ID and admission are released. Queued cancellation does not poison the runner.
  • Clone-safe bounded frame/application queues, shared byte charges through forwarding and deferred work, retained response and pending-request metadata charges, and cancellation-safe capacity waits. Imported frames must meet destination limits.
  • Explicit owned versus passive connection drivers preserve half-close response draining without waiting for an unrelated remote sender. Cancellation that overtakes an unpublished request is settled locally rather than followed by publishing uncancelled work.
  • One loopback HTTP listener per ACP connection with server-bound HMAC bearer credentials, no per-server route allocations, validation before bounded body reads, and admission owned through response-body consumption/drop.
  • Native-tool HTTP re-export strips only schema-position transport header annotations. Direct calls make no hidden tools/list requests; standard routing/version headers and endpoint authentication remain enforced.

Latest review follow-ups

  • Updated native and HTTP adapters to use independent v1/v2 outcome types. The schema refactor preserves the generated wire representation.
  • Added MCP message deserialization regressions: identical method/parameters are classified by the outer JSON-RPC ID, v1/v2 request and notification mappings stay separate, and invalid IDs do not fall back to notification parsing. All three tests pass.
  • Updated the protocol guide to describe cancellation as best effort, with no stronger support requirement implied by the advertised transport. The SDK's existing owned-cleanup behavior is unchanged.
  • Strict workspace Clippy and the companion protocol checks pass for this follow-up.

Review order

  1. Migration guide and companion wire-schema PR.
  2. Native service contract and cleanup regression.
  3. Transport ownership, queue admission, and transport-close tests.
  4. HTTP re-export contract and real rmcp HTTP coverage.

Verification

All Cargo validation used disabled incremental caching and stripped dev/test debug info.

  • Full just test passed, including doctests.
  • Strict cargo clippy --workspace --all-targets --all-features --locked -- -D warnings passed, with no temporary lint allowances.
  • Rust 1.88 cargo check --workspace --all-targets --all-features --locked passed.
  • Core and rmcp no-default-feature checks passed.
  • Native real-rmcp example ran successfully.
  • Formatting, mdbook build, and git diff --check passed.
  • Focused regressions cover both tool runner cleanup lifetimes, capacity recovery, sender cloning, cross-budget forwarding, retained replies, cancellation during readiness, EOF draining, real HTTP MRTR/subscriptions, and unread-body limits.

Release gates — keep draft

  • The RFD and unstable wire contract still require protocol review. Replace Git-pinned schema revision e5c36d2671fd355f983533bc83b5feb7981d25a6 with the released matching schema before package publication.
  • Coordinate dependent SDK major releases. The commit and PR are marked breaking; this PR does not publish packages or manually finalize release versions.
  • Limits account for SDK-owned serialized payloads and admitted work, not every allocation inside user code or the network stack. The SDK supervises supported cancellation and waits for owned cleanup; detached application work cannot be forcibly terminated. These safeguards do not add a cancellation support requirement to the transport capability.
  • This is native-tool re-export, not preservation of another HTTP gateway's parameter-header authorization policy. It is not a claim of complete MCP/HTTP conformance for every optional feature.

stream.write_all(request.as_bytes()).await.unwrap();
let mut response = String::new();
stream.read_to_string(&mut response).await.unwrap();
assert!(response.starts_with("HTTP/1.1 200"), "{response}");
stream.write_all(request.as_bytes()).await.unwrap();
let mut response = String::new();
stream.read_to_string(&mut response).await.unwrap();
assert!(response.starts_with("HTTP/1.1 200"), "{response}");
Comment thread src/agent-client-protocol-polyfill/src/mcp_over_acp/http.rs Fixed
Comment thread src/agent-client-protocol-polyfill/src/mcp_over_acp/http.rs Fixed
Comment thread src/agent-client-protocol-polyfill/src/mcp_over_acp/http.rs Fixed
Comment thread src/agent-client-protocol-polyfill/src/mcp_over_acp/http.rs Fixed
.to_string();
let headers = "Authorization: Bearer secret\r\nAccept: application/json, text/event-stream\r\nContent-Type: application/json\r\nMCP-Protocol-Version: 2026-07-28\r\nMcp-Method: wrong/method\r\n";
let mismatch = exchange(address, "POST", headers, &body).await;
assert!(mismatch.starts_with("HTTP/1.1 400"), "{mismatch}");
Comment thread src/agent-client-protocol-polyfill/src/mcp_over_acp/http.rs Fixed
assert!(mismatch.starts_with("HTTP/1.1 400"), "{mismatch}");
assert!(mismatch.contains("-32020"), "{mismatch}");
let batch = exchange(address, "POST", headers, "[]").await;
assert!(batch.starts_with("HTTP/1.1 400"), "{batch}");
Separate MCP outcomes from ACP failures, reuse request-native services, join tool cleanup before releasing admission, and bound retained frames, replies and HTTP response bodies. Preserve passive transport half-close semantics and test real rmcp HTTP workflows.

BREAKING CHANGE: Channel now carries budgeted frames, ConnectTo returns ConnectionDriver, and native MCP uses request-scoped services with explicit outcome carriers. Coordinate the schema and dependent SDK major releases before publishing.
@benbrandt benbrandt changed the title feat(acp): implement stateless MCP-over-ACP feat(acp)!: own stateless MCP operations and bound transports Sep 25, 2026
let task = tokio::spawn(run_http_listener(listener, state));
let auth = format!("Authorization: Bearer {token}\r\n");
let legacy = exchange(address, route, "GET", &auth, "").await;
assert!(legacy.starts_with("HTTP/1.1 405"), "{legacy}");
let legacy = exchange(address, route, "GET", &auth, "").await;
assert!(legacy.starts_with("HTTP/1.1 405"), "{legacy}");
let delete = exchange(address, route, "DELETE", &auth, "").await;
assert!(delete.starts_with("HTTP/1.1 405"), "{delete}");
exchange(address, route, "POST", "Origin: http://evil.test\r\n", "{}").await;
assert!(
invalid_origin.starts_with("HTTP/1.1 403"),
"{invalid_origin}"
exchange(address, route, "GET", "Origin: http://evil.test\r\n", "").await;
assert!(
invalid_get_origin.starts_with("HTTP/1.1 403"),
"{invalid_get_origin}"
"{invalid_get_origin}"
);
let invalid_auth = exchange(address, route, "POST", "", "{}").await;
assert!(invalid_auth.starts_with("HTTP/1.1 401"), "{invalid_auth}");
invalid_auth
.to_ascii_lowercase()
.contains("www-authenticate: bearer"),
"{invalid_auth}"
);
let mismatch = exchange(address, route, "POST", &headers, &body).await;
assert!(mismatch.starts_with("HTTP/1.1 400"), "{mismatch}");
assert!(mismatch.contains("-32020"), "{mismatch}");
Cover outer-id discrimination and separate v1/v2 method enums, and clarify that the binding adds no stronger cancellation support requirement.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants