MCP PHP SDK

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
author

Kyrian Obikwelu koshnawaza@gmail.com

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

Properties

$outgoingMessagesProvider

protected callable(Uuid): array<int, array{message: string, context: array}> $outgoingMessagesProvider

$pendingRequestsProvider

protected callable(Uuid): array<int, array<string, mixed>> $pendingRequestsProvider

$middleware

private array<int, MiddlewareInterface>|null $middleware

null until self::listen() resolves the defaults

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

null installs 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

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

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
phpstan-return

FiberResume

Return values
Response|Error|null

createErrorResponse()

protected createErrorResponse(Error $jsonRpcError, int $statusCode) : ResponseInterface
Parameters
$jsonRpcError : Error
$statusCode : int
Return values
ResponseInterface

createJsonResponse()

protected createJsonResponse() : ResponseInterface
Return values
ResponseInterface

createStreamedResponse()

protected createStreamedResponse() : ResponseInterface
Return values
ResponseInterface

flushOutgoingMessages()

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: array}>

getPendingRequests()

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
ResponseInterface

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
ResponseInterface

handlePostRequest()

protected handlePostRequest(string $body) : ResponseInterface
Parameters
$body : string

the request body, already read and bounded by self::handleRequest()

Return values
ResponseInterface

handleSessionEnd()

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
ResponseInterface

handleModernRequest()

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
ResponseInterface

handleRequest()

private handleRequest(ServerRequestInterface $request) : ResponseInterface
Parameters
$request : ServerRequestInterface
Return values
ResponseInterface

headers()

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|null

readBoundedBody()

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
Return values
string|null
On this page

Search results