Transports¶
A transport is the bytes-in, bytes-out layer between an MCP server and its client. The SDK ships two production
bindings, stdio and Streamable HTTP, on both the server and the client side. It also ships an in-memory pair for
tests (InMemoryTransport::createPair()).
| Transport | Side | Shape |
|---|---|---|
StdioServerTransport |
server | Long-lived, newline-delimited JSON over STDIN/STDOUT. Driven by Server::run(). |
StdioClientTransport |
client | Launches the server as a subprocess and speaks the same framing. |
StreamableHttpServerTransport |
server | Request-scoped PSR-15 handler. One POST per message. Driven by Server::listen(). |
StreamableHttpClientTransport |
client | One POST per outbound message, answered by a JSON object or an SSE stream. |
SupervisedTransport |
client | Decorator. Respawns a supervisable peer that exits unexpectedly. |
InMemoryTransport |
both | Test double pair, no I/O. |
The contract¶
Every transport implements
Nexus\Mcp\Core\Transport\TransportInterface. It is a small
synchronous-looking surface around an async event loop.
interface TransportInterface
{
public function start(): void;
public function send(JsonRpcMessage $message, ?SendContext $context = null): void;
public function close(): void;
public function onMessage(\Closure $listener): ListenerHandleInterface;
public function onError(\Closure $listener): ListenerHandleInterface;
public function onDrain(\Closure $listener): ListenerHandleInterface;
public function onClose(\Closure $listener): ListenerHandleInterface;
}
Listeners¶
The four on* methods register listeners. The Server registers them once, before it calls start():
onMessage dispatches, onError logs, onDrain awaits the in-flight coroutines, and onClose resolves the
run-future.
Closing¶
close() blocks until the close settles. A close() from another fiber waits for the close already in progress,
so no caller returns while the drain still runs. After any returned close(), a send() throws. A close that
re-enters from the closing fiber itself, such as a drain listener or a cascade peer, returns immediately. All
bundled transports honour this uniformly.
stateDiagram-v2
[*] --> Idle
Idle --> Started: start()
Started --> Draining: close(), peer EOF, or a fatal error
Idle --> Draining: cold close()
Draining --> Closed: onDrain listeners settle
Closed --> [*]: onClose fires, later send() or start() throws
Every path into Closed passes through the drain exactly once, so the dispatcher always gets to await its
in-flight coroutines. The state flips only after the drain, so a drain listener that settles its last exchange can
still send().
Inbound IDs¶
Inbound request IDs are the dispatcher's only correlation key. The dispatcher honours notifications/cancelled by
ID alone, with no connection dimension. A transport that serves several peers at once MUST namespace or rewrite
inbound IDs, so two peers' IDs can never collide. Without that, one peer can cancel or answer another peer's
request. StreamableHttpServerTransport replaces every inbound ID with an internal one for exactly this reason.
The stdio transports have a single peer, so IDs pass through.
Send context¶
SendContext carries three slots. A transport is free to ignore all of them.
relatedRequestIdties an out-of-band message, such as a progress notification, to the in-flight request that triggered it. A request-scoped transport uses it to route the message onto the right response stream.fromHandlermarks a response that a request handler produced. A request-scoped transport uses it to map the response to a transport-level status: a handler error rides HTTP 200 with the JSON-RPC error in the body, and a protocol error gets a real status.headerscarries transport headers the protocol layer computed. Today these are theMcp-Param-{Name}mirrors atools/callderives from its arguments. Stdio ignores them.
Further transport-specific fields can arrive through the same value object without a change to the interface shape.
The bindings¶
Each binding has its own reference page:
- Stdio:
StdioServerTransportandStdioClientTransport, line-framed JSON-RPC over process streams. - Streamable HTTP: the PSR-15 server handler with its middleware stack, and the one-POST-per-message client.
- SupervisedTransport: restart supervision, what survives a respawn, and the opt-in request retry.
- InMemoryTransport: the in-process test pair.
See also¶
- Getting started: a minimal server with stdio.
- Server API: the builder reference, request and notification handlers, and capability advertisement.
- Client API: the client builder and the typed request reference.
- Architecture: the dispatch kernel internals.