StreamableHttpTransport extends BaseTransport implements StatelessAwareTransportInterface uses ReadsBoundedBody
Carries MCP over HTTP, in either protocol era.
Every request is classified once, before anything else looks at it, and
routed to the lifecycle it belongs to: a per-request envelope claiming a
modern revision goes to StatelessProtocol, everything else — the
initialize handshake, its session's later requests, its DELETE teardown —
goes to the session machinery below. One endpoint, both eras, nothing for the
client to pick.
A server run without a modern-era dispatcher (see Builder::withoutModernEra()) serves the handshake era alone and refuses modern claims, naming the revisions it does serve.
Tags
Table of Contents
Interfaces
- StatelessAwareTransportInterface
- A transport that can carry the modern (SEP-2575) lifecycle as well as the handshake one.
Constants
- DEFAULT_MAX_BODY_BYTES : mixed = 4 * 1024 * 1024
- Upper bound on the request body read for a POST, guarding against memory exhaustion from an oversized (or unbounded chunked) payload.
- PROTOCOL_VERSION_HEADER : mixed = 'Mcp-Protocol-Version'
- SESSION_HEADER : mixed = 'Mcp-Session-Id'
Properties
- $fiberYieldHandler : callable(Array, Array): void
- $logger : LoggerInterface
- $messageListener : callable(TransportInterface<string|int, mixed>, string, Array): void
-
$outgoingMessagesProvider
: callable(Uuid): array<int, array{message: string, context: array
}> - $pendingRequestsProvider : callable(Uuid): array<int, array<string, mixed>>
- $responseFinder : callable(int, Uuid): Response<string|int, array<string, mixed>>|Error|null
- $sessionEndListener : callable(Uuid): void
- $sessionFiber : McpFiber|null
- $sessionId : Uuid|null
- $classifier : InboundClassifier
- $immediateResponse : string|null
- $immediateStatusCode : int|null
- $maxBodyBytes : int
- $middleware : array<int, MiddlewareInterface>|null
- $request : ServerRequestInterface
- $responder : StatelessResponder
- $responseFactory : ResponseFactoryInterface
- $stateless : StatelessProtocol|null
- $streamFactory : StreamFactoryInterface
Methods
- __construct() : mixed
- attachFiberToSession() : void
- close() : void
- Closes the transport and cleans up any resources.
- connectStateless() : void
- defaultMiddleware() : array<int, MiddlewareInterface>
- Secure default middleware stack applied when no `$middleware` is provided to the constructor.
- handshakeMiddleware() : array<int, MiddlewareInterface>
- Middleware applied only to requests classified as handshake-era traffic.
- initialize() : void
- Initializes the transport.
- listen() : TResult
- Starts the transport's execution process.
- onMessage() : void
- onSessionEnd() : void
- send() : void
- Send a message to the client immediately (bypassing session queue).
- setFiberYieldHandler() : void
- setOutgoingMessagesProvider() : void
- setPendingRequestsProvider() : void
- setResponseFinder() : void
- setSessionId() : void
- Set the session ID for the current transport context.
- checkForResponse() : Response|Error|null
- createErrorResponse() : ResponseInterface
- createJsonResponse() : ResponseInterface
- createStreamedResponse() : ResponseInterface
- flushOutgoingMessages() : void
-
getOutgoingMessages()
: array<int, array{message: string, context: array
}> - getPendingRequests() : array<int, array<string, mixed>>
- handleDeleteRequest() : ResponseInterface
- handleFiberTermination() : void
- handleFiberYield() : void
- handleMessage() : void
- handleOptionsRequest() : ResponseInterface
- handlePostRequest() : ResponseInterface
- handleSessionEnd() : void
- handleHandshakeRequest() : ResponseInterface
- handleModernRequest() : ResponseInterface
- Answers a request that claimed the modern era's per-request envelope.
- handleRequest() : ResponseInterface
- headers() : array<string, string>
- normalizeMiddleware() : array<int, MiddlewareInterface>
- readBody() : string|null
- Reads the request body, bounded by {@see self::$maxBodyBytes}.
- readBoundedBody() : string|null
- Returns the body contents, or `null` when the payload exceeds $maxBytes.
Constants
DEFAULT_MAX_BODY_BYTES
Upper bound on the request body read for a POST, guarding against memory exhaustion from an oversized (or unbounded chunked) payload.
public
mixed
DEFAULT_MAX_BODY_BYTES
= 4 * 1024 * 1024
PROTOCOL_VERSION_HEADER
public
mixed
PROTOCOL_VERSION_HEADER
= 'Mcp-Protocol-Version'
SESSION_HEADER
public
mixed
SESSION_HEADER
= 'Mcp-Session-Id'
Properties
$fiberYieldHandler
protected
callable(Array, Array): void
$fiberYieldHandler
$logger
protected
LoggerInterface
$logger
$messageListener
protected
callable(TransportInterface<string|int, mixed>, string, Array): void
$messageListener
$outgoingMessagesProvider
protected
callable(Uuid): array<int, array{message: string, context: array}>
$outgoingMessagesProvider
$pendingRequestsProvider
protected
callable(Uuid): array<int, array<string, mixed>>
$pendingRequestsProvider
$responseFinder
protected
callable(int, Uuid): Response<string|int, array<string, mixed>>|Error|null
$responseFinder
$sessionEndListener
protected
callable(Uuid): void
$sessionEndListener
$sessionFiber
protected
McpFiber|null
$sessionFiber
= null
$sessionId
protected
Uuid|null
$sessionId
= null
$classifier
private
InboundClassifier
$classifier
$immediateResponse
private
string|null
$immediateResponse
= null
$immediateStatusCode
private
int|null
$immediateStatusCode
= null
$maxBodyBytes read-only
private
int
$maxBodyBytes
= self::DEFAULT_MAX_BODY_BYTES
$middleware
private
array<int, MiddlewareInterface>|null
$middleware
null until self::listen() resolves the defaults
$request
private
ServerRequestInterface
$request
$responder
private
StatelessResponder
$responder
$responseFactory
private
ResponseFactoryInterface
$responseFactory
$stateless
private
StatelessProtocol|null
$stateless
= null
$streamFactory
private
StreamFactoryInterface
$streamFactory
Methods
__construct()
public
__construct(ServerRequestInterface $request[, ResponseFactoryInterface|null $responseFactory = null ][, StreamFactoryInterface|null $streamFactory = null ][, LoggerInterface|null $logger = null ][, iterable<string|int, MiddlewareInterface>|null $middleware = null ][, int $maxBodyBytes = self::DEFAULT_MAX_BODY_BYTES ]) : mixed
Parameters
- $request : ServerRequestInterface
- $responseFactory : ResponseFactoryInterface|null = null
- $streamFactory : StreamFactoryInterface|null = null
- $logger : LoggerInterface|null = null
- $middleware : iterable<string|int, MiddlewareInterface>|null = null
-
nullinstalls self::defaultMiddleware();[]disables all middleware - $maxBodyBytes : int = self::DEFAULT_MAX_BODY_BYTES
attachFiberToSession()
public
attachFiberToSession(McpFiber $fiber, Uuid $sessionId) : void
Parameters
- $fiber : McpFiber
- $sessionId : Uuid
close()
Closes the transport and cleans up any resources.
public
close() : void
connectStateless()
public
connectStateless(StatelessProtocol $protocol) : void
Parameters
- $protocol : StatelessProtocol
defaultMiddleware()
Secure default middleware stack applied when no `$middleware` is provided to the constructor.
public
static defaultMiddleware() : array<int, MiddlewareInterface>
These run at the edge, before the request's era is known, because what
they enforce — origin policy, DNS rebinding — is true of both eras. The
MCP-Protocol-Version header rule is not: it belongs to the handshake
era, so self::handshakeMiddleware() carries it instead and the
modern leg answers for its own revisions.
Return values
array<int, MiddlewareInterface>handshakeMiddleware()
Middleware applied only to requests classified as handshake-era traffic.
public
static handshakeMiddleware() : array<int, MiddlewareInterface>
Return values
array<int, MiddlewareInterface>initialize()
Initializes the transport.
public
initialize() : void
listen()
Starts the transport's execution process.
public
listen() : TResult
- For a blocking transport like STDIO, this method will run a continuous loop.
- For a single-request transport like HTTP, this will process the request and return a result (e.g., a PSR-7 Response) to be sent to the client.
Return values
TResult —the result of the transport's execution, if any
onMessage()
public
onMessage(callable $listener) : void
Parameters
- $listener : callable
onSessionEnd()
public
onSessionEnd(callable $listener) : void
Parameters
- $listener : callable
send()
Send a message to the client immediately (bypassing session queue).
public
send(string $data, array<string|int, mixed> $context) : void
Used for session resolution errors when no session is available. The transport decides HOW to send based on context.
Parameters
- $data : string
- $context : array<string|int, mixed>
-
Context about this message:
- 'session_id': Uuid|null
- 'type': 'response'|'request'|'notification'
- 'status_code': int (HTTP status code for errors)
setFiberYieldHandler()
public
setFiberYieldHandler(callable(Array, Array): void $handler) : void
Parameters
- $handler : callable(Array, Array): void
setOutgoingMessagesProvider()
public
setOutgoingMessagesProvider(callable $provider) : void
Parameters
- $provider : callable
setPendingRequestsProvider()
public
setPendingRequestsProvider(callable $provider) : void
Parameters
- $provider : callable
setResponseFinder()
public
setResponseFinder(callable(int, Uuid): Array $finder) : void
Parameters
- $finder : callable(int, Uuid): Array
setSessionId()
Set the session ID for the current transport context.
public
setSessionId(Uuid|null $sessionId) : void
Parameters
- $sessionId : Uuid|null
-
The session ID, or null to clear
checkForResponse()
protected
checkForResponse(int $requestId, Uuid|null $sessionId) : Response|Error|null
Parameters
- $requestId : int
- $sessionId : Uuid|null
Tags
Return values
Response|Error|nullcreateErrorResponse()
protected
createErrorResponse(Error $jsonRpcError, int $statusCode) : ResponseInterface
Parameters
- $jsonRpcError : Error
- $statusCode : int
Return values
ResponseInterfacecreateJsonResponse()
protected
createJsonResponse() : ResponseInterface
Return values
ResponseInterfacecreateStreamedResponse()
protected
createStreamedResponse() : ResponseInterface
Return values
ResponseInterfaceflushOutgoingMessages()
protected
flushOutgoingMessages(Uuid|null $sessionId) : void
Parameters
- $sessionId : Uuid|null
getOutgoingMessages()
protected
getOutgoingMessages(Uuid|null $sessionId) : array<int, array{message: string, context: array}>
Parameters
- $sessionId : Uuid|null
Return values
array<int, array{message: string, context: arraygetPendingRequests()
protected
getPendingRequests(Uuid|null $sessionId) : array<int, array<string, mixed>>
Parameters
- $sessionId : Uuid|null
Return values
array<int, array<string, mixed>>handleDeleteRequest()
protected
handleDeleteRequest() : ResponseInterface
Return values
ResponseInterfacehandleFiberTermination()
protected
handleFiberTermination() : void
handleFiberYield()
protected
handleFiberYield(FiberSuspend|null $yielded, Uuid|null $sessionId) : void
Parameters
- $yielded : FiberSuspend|null
- $sessionId : Uuid|null
handleMessage()
protected
handleMessage(string $payload, Uuid|null $sessionId) : void
Parameters
- $payload : string
- $sessionId : Uuid|null
handleOptionsRequest()
protected
handleOptionsRequest() : ResponseInterface
Return values
ResponseInterfacehandlePostRequest()
protected
handlePostRequest(string $body) : ResponseInterface
Parameters
- $body : string
-
the request body, already read and bounded by self::handleRequest()
Return values
ResponseInterfacehandleSessionEnd()
protected
handleSessionEnd(Uuid|null $sessionId) : void
Parameters
- $sessionId : Uuid|null
handleHandshakeRequest()
private
handleHandshakeRequest(ServerRequestInterface $request, string|null $body) : ResponseInterface
Parameters
- $request : ServerRequestInterface
- $body : string|null
Return values
ResponseInterfacehandleModernRequest()
Answers a request that claimed the modern era's per-request envelope.
private
handleModernRequest(string $body, string $claimedVersion) : ResponseInterface
Parameters
- $body : string
- $claimedVersion : string
Return values
ResponseInterfacehandleRequest()
private
handleRequest(ServerRequestInterface $request) : ResponseInterface
Parameters
- $request : ServerRequestInterface
Return values
ResponseInterfaceheaders()
private
static headers(ServerRequestInterface $request) : array<string, string>
Parameters
- $request : ServerRequestInterface
Return values
array<string, string>normalizeMiddleware()
private
static normalizeMiddleware(iterable<string|int, MiddlewareInterface> $middleware) : array<int, MiddlewareInterface>
Parameters
- $middleware : iterable<string|int, MiddlewareInterface>
Return values
array<int, MiddlewareInterface>readBody()
Reads the request body, bounded by {@see self::$maxBodyBytes}.
private
readBody(StreamInterface $body) : string|null
Parameters
- $body : StreamInterface
Return values
string|nullreadBoundedBody()
Returns the body contents, or `null` when the payload exceeds $maxBytes.
private
readBoundedBody(StreamInterface $body, int $maxBytes) : string|null
A stream advertising its size is rejected up front; otherwise the read is incremental and stops at the cap, so an unbounded stream cannot exhaust memory.
Parameters
- $body : StreamInterface
- $maxBytes : int