ADR-0002 — McpServer / McpTransport is the only coupling between protocol and transport¶
- Status: Superseded by ADR-0020
- Date: 2025-07-09
Superseded 2026-07-28 (ADR-0020): the clean break to stateless MCP 2026-07-28 collapsed this contract.
McpServernow exposes onlyhandleCall(JsonRpcCall, McpTransport)andhandleNotification(JsonRpcNotification);createContext,handleResponse, andterminate(sessionId)are gone with sessions (ADR-0020).McpTransportis now a singlesend(JsonRpcMessage)method —emit(McpEvent), theMcpEventsealed type, andSessionInitializedwere deleted along with the session lifecycle. Server-initiated requests and theirhandleResponsereturn path were removed by ADR-0021. The two-interface split below is still the only coupling between protocol and transport; only the method surface shrank. The Context/Decision text is preserved as the historical record.
Context¶
Before the protocol/transport split, a single StreamableHttpController
class mixed HTTP concerns (Accept headers, status codes, SSE emitter
management) with MCP protocol logic (session lifecycle, JSON-RPC dispatch,
elicitation/sampling correlation, tool dispatch). Every protocol bug had
to be reproduced through MockMvc; every transport bug looked like a
protocol bug; and adding a stdio transport required either copy-pasting
half the controller or breaking the controller open without a contract.
The MCP specification defines messages, methods, and capabilities — not a wire protocol. Streamable HTTP, stdio, and any future transport (WebSocket, Unix socket) all carry the same JSON-RPC payloads. Mocapi needs to implement the protocol once and let transports plug in.
Decision¶
mocapi-server exposes a two-interface contract that is the only
coupling between the protocol layer and any transport:
public interface McpServer {
McpContextResult createContext(String sessionId, String protocolVersion);
void handleCall(McpContext context, JsonRpcCall call, McpTransport transport);
void handleNotification(McpContext context, JsonRpcNotification notification);
void handleResponse(McpContext context, JsonRpcResponse response);
void terminate(String sessionId);
}
public interface McpTransport {
void send(JsonRpcMessage message);
void emit(McpEvent event);
}
public sealed interface McpEvent {
record SessionInitialized(String sessionId, String protocolVersion) implements McpEvent {}
}
Rules:
- The server never returns protocol output as a value. All outbound
messages flow through
transport.send(...). Lifecycle signals (currently onlySessionInitialized) flow throughtransport.emit(...). - The server validates sessions and protocol versions via
createContext(ADR-0009). Transports map the resultingMcpContextResultvariants to their native error format. - The server is transport-agnostic. It depends on ripcurl (JSON-RPC),
Substrate (storage SPIs — see ADR-0007),
and
mocapi-model. It does not depend on Spring MVC, Servlet API, Odyssey, or any I/O framework. - Transports are server-agnostic in the other direction: they know
nothing about sessions, registries, or tool dispatch. A transport
constructs
McpContextfrom its wire format, callscreateContextto resolve/validate it, then delegates to one of the fourhandle*methods. JsonRpcResponsefrom the client (responding to a server-initiated elicitation or sampling request) goes tohandleResponse. The server delivers it to the awaiting Mailbox internally and does not calltransport.send— there is no outgoing message. See ADR-0008.
Consequences¶
Wins:
- The server is unit-testable in complete isolation. Tests build a
capturing transport (a
List<JsonRpcMessage>+List<McpEvent>), invokehandleCall, and assert on what was sent — no MockMvc, no Tomcat, no SSE plumbing. - Adding a transport is a self-contained job.
mocapi-stdio-transportwas implemented against this contract with zero changes tomocapi-server. See ADR-0003. - Session enforcement, protocol-version negotiation, capability declaration, and error formatting all live in one place. A bug fix in the server fixes every transport.
Costs:
- Transports must accept asynchrony.
handleCallmay run synchronously (returning before the response is sent) for stdio's loop thread or asynchronously (when the server spawns a virtual thread; see ADR-0006). Transports buffer, queue, or stream as appropriate. - The contract is small but non-negotiable. Adding a per-transport hook
(e.g., "give me the HTTP request headers") is not allowed at this layer
— that data is captured by the transport before
handleCallis invoked or piped through aScopedValue.
Non-goals: the contract does not expose tool, prompt, or resource
APIs. Tool authors depend on mocapi-api (see
ADR-0001); the server resolves
those through registries built at startup.
Code anchors: mocapi-server/.../McpServer.java, mocapi-server/.../McpTransport.java. (The McpEvent.java sealed type referenced by the original decision was deleted under ADR-0020.)