ADR-009: Migrate MCP Transport to rmcp SDK
- Status: Accepted
- Date: 2026-05-14
- Issue: #985
Context
dcc-mcp-core maintains ~4k lines of MCP protocol framing (session lifecycle, SSE streaming, JSON-RPC method dispatch, protocol version negotiation) across dcc-mcp-jsonrpc, dcc-mcp-http-server, and dcc-mcp-http. Each MCP spec revision (2025-03-26, 2025-06-18, 2025-11-25) requires manual updates to types, dispatch logic, and transport headers.
The official Rust MCP SDK rmcp now provides a production-quality implementation of Streamable HTTP transport via its transport-streamable-http-server feature. It tracks spec versions, handles session lifecycle, and exposes a ServerHandler trait for method dispatch.
Decision
Replace the MCP transport layer with rmcp's StreamableHttpService behind the rmcp-transport Cargo feature flag. DCC-specific business logic remains in our crates; only the wire-level concerns move to rmcp.
Boundary Definition
| Concern | Owner after migration |
|---|---|
HTTP transport, SSE framing, Mcp-Session-Id header | rmcp |
| Session lifecycle (create / resume / evict) | rmcp SessionManager |
| JSON-RPC frame parsing and method routing | rmcp |
| Protocol version negotiation | rmcp (ProtocolVersion::KNOWN_VERSIONS) |
| Tool registry, dispatch, readiness gating | dcc-mcp-http-server (unchanged) |
| DCC executor, thread-affinity routing | dcc-mcp-http-server (unchanged) |
| Skill catalog, lazy actions, progressive disclosure | dcc-mcp-http-server (unchanged) |
| Vendor extensions (deltaToolsUpdate, dynamic tools) | Adapter layer (new) |
Multi-DCC gateway routing, /v1/* REST, admin, audit | dcc-mcp-http (unchanged) |
| Python/PyO3 bindings | dcc-mcp-core (unchanged, public API stable) |
Approach
- Feature-flagged:
rmcp-transportfeature indcc-mcp-httpanddcc-mcp-http-server. Off by default initially; default-on after parity is proven. - Adapter pattern:
DccMcpHandlerimplements rmcp'sServerHandlertrait and delegates to our existingServerState(registry, dispatcher, catalog). - Type conversion: Thin
rmcp_adaptermodule converts between our types (McpTool,CallToolResult,ToolContent) and rmcp's (Tool,CallToolResult,Content). - Parallel mount (spike):
/mcp-nextendpoint runs rmcp alongside the existing/mcp; once stable, the paths merge. - Version routing (later): Middleware routes
2025-11-25sessions to rmcp and2025-06-18/2025-03-26sessions to the legacy stack until legacy is removed.
Consequences
Positive
- Spec protocol updates are handled by upgrading the
rmcpdependency version. - Session management, SSE keep-alive, and MCP-Protocol-Version header validation are maintained upstream.
- Reduced internal code surface to maintain and test.
Negative
- Vendor extensions (
dcc_mcp_core/deltaToolsUpdate, elicitation, dynamic tools) require adapter hooks outside rmcp's standard trait methods. - An additional dependency (~20 transitive crates) in the compile graph when the feature is enabled.
- Breaking rmcp major version bumps require adapter updates.
Neutral
- Tool authors' workflow is unchanged: register tools via
ToolRegistry/SKILL.mdas before. - Python binding consumers see no change (PyO3 surface uses public Rust API).
Alternatives Considered
- Fork rmcp: Full control but doubles maintenance burden.
- Partial adoption (types only, no transport): Saves less code, still need to maintain SSE / session logic.
- Stay on in-house stack: Higher maintenance cost per spec revision, but zero new dependencies.
Option 2 was rejected because the transport layer is the primary maintenance cost. Option 1 is a fallback if rmcp's extension points prove insufficient.