- Author(s): alexhancock, jh-block
- Champion: anna239
Elevator pitch
What are you proposing to change?ACP needs a standard remote transport. We propose adopting MCP Streamable HTTP (2025-11-25) with ACP-specific headers, and extending it with a WebSocket upgrade on the same endpoint. A single
/acp endpoint supports two connectivity profiles:
- Streamable HTTP (POST/GET/DELETE) — stateless-friendly, SSE-based streaming, aligned with MCP Streamable HTTP.
- WebSocket upgrade (GET with
Upgrade: websocket) — persistent, full-duplex, low-latency bidirectional messaging.
Status quo
How do things work today and what problems does this cause? Why would we change things?ACP only has stdio (inherited from MCP). There is no standard remote transport, which causes:
- Fragmentation — implementers invent their own HTTP layers, leading to incompatible SDKs and deployments.
- Missed alignment — MCP Streamable HTTP is well-designed; ACP should adopt it rather than diverge.
What we propose to do about it
What are you proposing to improve the situation?
1. Mostly adopts MCP Streamable HTTP semantics with ACP-specific headers
Follows the MCP 2025-11-25 Streamable HTTP spec with these adaptations:- Connection header:
Acp-Connection-Id(similar toMcp-Session-Id) - Session header:
Acp-Session-Id(new, no MCP equivalent) - Protocol version header:
Acp-Protocol-Version - Endpoint path: conventionally
/acp
2. Separates connection identity from session identity with two headers
ACP introduces two HTTP headers that together identify the full request context:Acp-Connection-Id(HTTP header) — A transport-level identifier returned by the server in theinitializeresponse. It binds all subsequent HTTP requests to the initialized connection and its negotiated capabilities. This is analogous toMcp-Session-Idin MCP Streamable HTTP. Required on all requests afterinitialize.Acp-Session-Id(HTTP header) — A session-level identifier returned by the server in thesession/newresponse (both in theAcp-Session-Idresponse header and the JSON-RPC result body). It identifies a specific conversation. Clients MUST include it on all subsequent requests that operate within a session (e.g.,session/prompt,session/cancel). The same value appears in JSON-RPC params assessionIdfor methods that require it.
Acp-Connection-Id may span multiple ACP sessions. Before session/new is called, only Acp-Connection-Id is present. After session/new, both headers are included.
3. Adds WebSocket as a first-class upgrade on the same endpoint
A GET withUpgrade: websocket upgrades to a persistent bidirectional channel — same endpoint, same lifecycle model.
This is important for ACP, as it is more bidirectional in its nature as a protocol.
4. Requires cookie support on HTTP transports
Clients MUST accept, store, and return cookies set by the server on all HTTP-based transports (Streamable HTTP and WebSocket). Cookies MUST be sent on subsequent requests to the server for the duration of the connection. Clients MAY discard all cookies when a connection is terminated. This allows servers to rely on cookies for session affinity (e.g., sticky sessions behind a load balancer) and other small amounts of per-connection state.5. Defines a unified routing model
| Method | Upgrade Header? | Behavior |
|---|---|---|
POST | — | Send JSON-RPC request/notification/response (Streamable HTTP) |
GET | No | Open session-scoped SSE stream for server-initiated messages (Streamable HTTP). Requires Acp-Connection-Id and Acp-Session-Id. |
GET | Upgrade: websocket | Upgrade to WebSocket for full-duplex messaging |
DELETE | — | Terminate the connection |
6. Preserves the full ACP lifecycle
Theinitialize → initialized → messages → close lifecycle is identical regardless of transport. The Acp-Connection-Id header binds requests to the initialized connection and its negotiated capabilities. The Acp-Session-Id header (introduced after session/new) identifies an active ACP session.
Shiny future
How will things play out once this feature exists?
- SDK implementers get a clear, testable transport spec — Rust, TypeScript, and Python SDKs can all interoperate.
- Desktop clients use WebSocket for low-latency streaming; all clients support it as a baseline.
- Cloud deployments expose agents behind standard HTTP load balancers using the stateless-friendly HTTP mode, with cookie-based sticky sessions guaranteed by client support.
- Proxy chains can route ACP traffic over HTTP for multi-hop agent topologies.
Implementation details and plan
Tell me more about your implementation. What is your detailed implementation plan?
Transport Architecture
Identity Model
ACP over Streamable HTTP uses two HTTP headers that together identify the full request context:Acp-Connection-Id | Acp-Session-Id | |
|---|---|---|
| Purpose | Binds HTTP requests to an initialized connection | Identifies an ACP conversation/session |
| Created by | Server, on initialize response | Server, on session/new response |
| Location | HTTP header (all requests after initialize) | HTTP header (all requests after session/new) + JSON-RPC body as sessionId |
| Scope | One per initialize handshake | One per session/new call |
| Multiplicity | May span multiple sessions | Belongs to exactly one connection |
| Used for | Request routing, capability lookup | Session-scoped request routing, session-scoped GET listener filtering |
| Required | After initialize | After session/new |
Streamable HTTP Message Flow
Content Negotiation and Validation
Content-TypeMUST beapplication/json(415 otherwise).AcceptMUST include bothapplication/jsonandtext/event-stream(406 otherwise).- Batch JSON-RPC requests return 501.
WebSocket Request Flow
Connection Establishment (GET with Upgrade)
Acp-Connection-Id is returned in the upgrade response headers. The client must still send initialize as the first JSON-RPC message over the WebSocket to negotiate capabilities before creating sessions.
Bidirectional Messaging
All messages are WebSocket text frames containing JSON-RPC. Binary frames are ignored. On disconnect, the server cleans up the connection and any associated sessions.Unified Endpoint Routing
Session Model
session/new, which returns the Acp-Session-Id in both the HTTP response header and the JSON-RPC result body. The transport layer adapts channels to the wire format (SSE events for HTTP, text frames for WebSocket). GET listeners are always session-scoped — both Acp-Connection-Id and Acp-Session-Id are required, and the server delivers only events belonging to that session.
MCP Streamable HTTP Compliance
| MCP Requirement | ACP Implementation | Status |
|---|---|---|
| POST for all client→server messages | ✅ | Compliant |
| Accept header validation (406) | ✅ | Compliant |
| Notifications/responses return 202 | ✅ | Compliant |
| Requests return SSE stream | ✅ | Compliant |
| Session ID on initialize response | ✅ (Acp-Connection-Id) | Compliant (renamed) |
| Session ID required on subsequent requests | ✅ (Acp-Connection-Id required; Acp-Session-Id required after session/new) | Compliant (extended) |
| GET opens SSE stream | ✅ | Compliant |
| DELETE terminates session | ✅ (terminates connection) | Compliant |
| 404 for unknown sessions | ✅ (unknown connection IDs) | Compliant |
| Batch requests | ❌ (returns 501) | Documented deviation |
| Resumability (Last-Event-ID) | ❌ | Future work |
| Protocol version header | ❌ | Future work |
Deviations from MCP Streamable HTTP
- Header naming:
Acp-Connection-Id/Acp-Session-Id/Acp-Protocol-Versioninstead of MCP equivalents.Acp-Connection-Idmaps to MCP’sMcp-Session-Idbut is deliberately renamed to avoid confusion with ACP’s session concept (see Identity Model). - Two-header model: MCP uses a single
Mcp-Session-Idfor both transport binding and session identity. ACP separates these intoAcp-Connection-Id(connection-scoped, frominitialize) andAcp-Session-Id(session-scoped, fromsession/new), because ACP sessions are an explicit protocol concept with their own lifecycle (session/new,session/load,session/cancel). TheAcp-Session-Idalso enables session-scoped GET listener streams. - Session-scoped GET streams: GET listeners require both
Acp-Connection-IdandAcp-Session-Id. The server MUST only deliver events belonging to that session. There is no connection-scoped GET stream. MCP has no equivalent concept. - WebSocket extension: MCP doesn’t define WebSocket. ACP adds it as a required client capability. Clients MUST support WebSocket, and servers MAY choose to only support WebSocket connections.
- Cookie support required: Clients MUST handle cookies on HTTP transports for the duration of the connection, enabling sticky sessions and per-connection server state.
- No batch requests: Returns 501. May be added later.
- No resumability yet in reference implementation: SSE event IDs and
Last-Event-IDresumption planned as follow-up.
Implementation Plan
- Phase 1 — Specification (this RFD): Define the transport spec and align terminology.
- Phase 2 — Reference Implementation (in progress): Working implementation in Goose (
block/goose) atcrates/goose-acp/src/transport/(transport.rs,http.rs,websocket.rs). - Phase 3 — SDK Support: Add Streamable HTTP and WebSocket client support to Rust SDK (
sacp), then TypeScript SDK. - Phase 4 — Hardening: Origin validation,
Acp-Protocol-Version, SSE resumability, batch requests, security audit.
Frequently asked questions
What questions have arisen over the course of authoring this document or during subsequent discussions?
Why not just use MCP Streamable HTTP as-is?
We largely do. The differences are: header naming (Acp-Connection-Id + Acp-Session-Id vs MCP’s single Mcp-Session-Id), the WebSocket extension for long-running agent sessions, the two-header model that separates connection identity from session identity, and session-scoped GET listener streams.
Why two headers (Acp-Connection-Id and Acp-Session-Id) instead of one?
MCP uses a single Mcp-Session-Id because MCP has no protocol-level concept of sessions. ACP does — session/new creates a conversation with its own lifecycle. Using one header for both would conflate the initialized connection (capabilities, protocol version, auth state) with the active session (conversation context, history). Two headers let the server immediately distinguish which connection and which session a request belongs to, enable session-scoped GET listener streams, and support multiple concurrent sessions within a single connection.
Why add WebSocket support?
A singleprompt can generate dozens of streaming updates and ACP is more bidirectional in nature than MCP. With Streamable HTTP, the server can only push via SSE on POST responses or a separate GET stream. WebSocket provides true bidirectional messaging, lower per-message overhead, and connection persistence. Clients MUST support WebSocket so that servers can choose to only support WebSocket connections, simplifying deployment. Streamable HTTP remains available as an additional option for environments where WebSocket is not viable on the server side (e.g., serverless).
How does the server distinguish WebSocket from SSE on GET?
By inspecting theUpgrade: websocket header. This is standard HTTP behavior.
Can a client have multiple sessions on one connection?
Yes. A client may callsession/new multiple times within a single Acp-Connection-Id. Each returns a distinct Acp-Session-Id. The client includes the appropriate Acp-Session-Id header (and sessionId in the JSON-RPC params) on subsequent requests. The Acp-Connection-Id header remains the same across all of them. The client may also open separate GET listener streams per session, each requiring both Acp-Connection-Id and Acp-Session-Id.
What alternative approaches did you consider, and why did you settle on this one?
- Separate endpoints (
/acp/http,/acp/ws): Rejected — single endpoint is simpler; WebSocket upgrade is natural HTTP. - WebSocket only: Rejected — doesn’t work through all proxies; Streamable HTTP is better for stateless/serverless.
- Single header for both connection and session: Rejected — conflates connection lifecycle with session lifecycle, prevents session-scoped GET streams, and makes multi-session connections ambiguous.
How does this interact with authentication?
Authentication (see auth-methods RFD) is orthogonal and layered on top via HTTP headers, query parameters, or WebSocket subprotocols.Acp-Connection-Id and Acp-Session-Id are transport-level identifiers, not auth tokens.
What about the Acp-Protocol-Version header?
Clients SHOULD include it on all requests after initialization. Not yet implemented; part of Phase 4 hardening.
Revision history
- 2025-03-10: Initial draft based on the RFC template and goose reference implementation.
- 2026-04-01: Introduced a two-header identity model:
Acp-Connection-Id(returned atinitialize, binds to the connection) andAcp-Session-Id(returned atsession/new, scopes to a session). This addresses feedback that the original singleAcp-Session-Idconflated transport binding with ACP session identity, and enables session-scoped GET listener streams for targeted server-to-client event delivery. Removed connection-scoped GET streams — all GET SSE listeners now require bothAcp-Connection-IdandAcp-Session-Id. - 2026-04-15: Minor edits