StatelessProtocol
Dispatches a single modern-era (SEP-2575) request.
Separate from Protocol because the modern era has no session to resolve, replay or keep a fiber against; the two eras share request handlers, not control flow.
Tags
Table of Contents
Constants
- ACKNOWLEDGED_NOTIFICATION : mixed = 'notifications/subscriptions/acknowledged'
- DISCOVER_METHOD : mixed = 'server/discover'
- LISTEN_METHOD : mixed = 'subscriptions/listen'
- REMOVED_METHODS : mixed = [ 'initialize', 'notifications/initialized', 'p...
- Methods the modern era deleted. Answered as unknown methods, which is what they are to a modern server.
- INTERNAL_ERROR_MESSAGE : mixed = 'Internal server error.'
- What a client is told when a handler fails in a way nothing anticipated.
Properties
- $codec : WireCodecInterface
- $configuration : Configuration
- $extensionMethods : array<string|int, mixed>
- $headerValidator : StandardHeaderValidator|null
- $logger : LoggerInterface
- $messageFactory : MessageFactory
- $notificationBus : NotificationBusInterface|null
- $requestHandlers : iterable<string|int, mixed>
- $requestStateCodec : RequestStateCodec|null
- $subscriptionLifetime : float
- $supportedVersions : array<string|int, mixed>
Methods
- __construct() : mixed
- handle() : StatelessResult
- Answers one JSON-RPC request read from an HTTP request body.
- supportedVersions() : array<int, ProtocolVersion>
- The modern revisions this dispatcher answers for.
- acceptsEventStream() : bool
- Whether the client will read a response stream.
- acknowledge() : StatelessResult
- Answers a notification.
- checkInputRequests() : StatelessResult|null
- Refuses to send an ask the client cannot answer.
- checkVersion() : StatelessResult|null
- Header and `_meta` must agree before the version can be judged supported: when they disagree the server cannot know which the client meant, so a mismatch outranks an unsupported version.
- discover() : DiscoverResult
- dispatch() : StatelessResult
- encode() : StatelessResult
- Runs a result through the wire codec. Passed as-is rather than via a json round trip, which would turn a nested `{}` into `[]`.
- header() : string|null
- liftInputContext() : InputContext|null
- Reads the multi round-trip material off a retry, verifying the state before any of it reaches a handler. Neither member means a first call, which is what a handler tests to decide whether it still needs to ask.
- listen() : StatelessResult
- Opens a `subscriptions/listen` stream. The subscription id is the JSON-RPC id of this request, so there is none to mint.
- readElicitation() : ElicitRequest}|null
- One fiber suspension read as an elicitation, or null when it is not one.
- readNotification() : Notification|null
- Reads one fiber suspension, or null when it carries nothing to send.
- requiresTransportHeaders() : bool
- Whether the transport carrying this dispatcher has a header layer whose required members must be present.
- run() : Generator<null, Response<string|int, ResultInterface>|Error>
- Runs a handler, yielding the notifications it emits as it emits them and returning its result.
- streamFrames() : Generator<string|int, mixed>
- The frames of a request-scoped response stream: the notifications the handler emits, then the response that ends it.
- tagWithSubscription() : array<string, mixed>
- Every message on a listen stream carries the id of the subscription it belongs to, which is how a client demultiplexes them on stdio — where they all share one channel.
- toErrorResult() : StatelessResult
- The one place a handler's exception becomes an answer, so the streaming and non-streaming paths cannot disagree about which code it earns.
- unknownMethod() : Error
- A method with no handler, said as precisely as the server can.
- withTraceContext() : array<string, mixed>
- Puts the request's trace context back onto a notification it caused, so a collector can join the two without the handler carrying it by hand.
Constants
ACKNOWLEDGED_NOTIFICATION
public
mixed
ACKNOWLEDGED_NOTIFICATION
= 'notifications/subscriptions/acknowledged'
DISCOVER_METHOD
public
mixed
DISCOVER_METHOD
= 'server/discover'
LISTEN_METHOD
public
mixed
LISTEN_METHOD
= 'subscriptions/listen'
REMOVED_METHODS
Methods the modern era deleted. Answered as unknown methods, which is what they are to a modern server.
public
mixed
REMOVED_METHODS
= [
'initialize',
'notifications/initialized',
'ping',
'logging/setLevel',
// Replaced by the `resourceSubscriptions` filter of subscriptions/listen.
'resources/subscribe',
'resources/unsubscribe',
'notifications/roots/list_changed',
]
A deny-list rather than an allow-list on purpose: extensions add methods this class has never heard of, so an unlisted method has to reach dispatch. Every removal named in the 2026-07-28 changelog belongs here — the handlers behind them stay registered for the handshake era, which is why the era guard, and not the registration, is what turns them off.
INTERNAL_ERROR_MESSAGE
What a client is told when a handler fails in a way nothing anticipated.
private
mixed
INTERNAL_ERROR_MESSAGE
= 'Internal server error.'
Deliberately generic rather than Throwable::getMessage(): the real message is logged, not returned, so an internal detail (a connection string, a file path, another library's error text) never reaches the client that triggered it.
Properties
$codec read-only
private
WireCodecInterface
$codec
$configuration read-only
private
Configuration
$configuration
$extensionMethods read-only
private
array<string|int, mixed>
$extensionMethods
= []
$headerValidator read-only
private
StandardHeaderValidator|null
$headerValidator
= null
$logger read-only
private
LoggerInterface
$logger
= new NullLogger()
$messageFactory read-only
private
MessageFactory
$messageFactory
$notificationBus read-only
private
NotificationBusInterface|null
$notificationBus
= null
$requestHandlers read-only
private
iterable<string|int, mixed>
$requestHandlers
$requestStateCodec read-only
private
RequestStateCodec|null
$requestStateCodec
= null
$subscriptionLifetime read-only
private
float
$subscriptionLifetime
= 30.0
$supportedVersions read-only
private
array<string|int, mixed>
$supportedVersions
= [ProtocolVersion::V2026_07_28]
Methods
__construct()
public
__construct(iterable<string|int, RequestHandlerInterface<string|int, ResultInterface>> $requestHandlers, MessageFactory $messageFactory, Configuration $configuration[, array<int, ProtocolVersion> $supportedVersions = [ProtocolVersion::V2026_07_28] ][, LoggerInterface $logger = new NullLogger() ][, float $subscriptionLifetime = 30.0 ][, WireCodecInterface|null $codec = null ][, StandardHeaderValidator|null $headerValidator = null ][, RequestStateCodec|null $requestStateCodec = null ][, CachePolicy|null $cachePolicy = null ][, NotificationBusInterface|null $notificationBus = null ][, array<string, string> $extensionMethods = [] ]) : mixed
Parameters
- $requestHandlers : iterable<string|int, RequestHandlerInterface<string|int, ResultInterface>>
- $messageFactory : MessageFactory
- $configuration : Configuration
- $supportedVersions : array<int, ProtocolVersion> = [ProtocolVersion::V2026_07_28]
- $logger : LoggerInterface = new NullLogger()
- $subscriptionLifetime : float = 30.0
- $codec : WireCodecInterface|null = null
- $headerValidator : StandardHeaderValidator|null = null
- $requestStateCodec : RequestStateCodec|null = null
- $cachePolicy : CachePolicy|null = null
- $notificationBus : NotificationBusInterface|null = null
- $extensionMethods : array<string, string> = []
-
RPC method to the extension identifier defining it
handle()
Answers one JSON-RPC request read from an HTTP request body.
public
handle(string $body[, array<string, string> $headers = [] ]) : StatelessResult
Parameters
- $body : string
- $headers : array<string, string> = []
-
request headers, case-insensitively matched
Return values
StatelessResultsupportedVersions()
The modern revisions this dispatcher answers for.
public
supportedVersions() : array<int, ProtocolVersion>
Return values
array<int, ProtocolVersion>acceptsEventStream()
Whether the client will read a response stream.
private
static acceptsEventStream(array<string, string> $headers) : bool
Clients MUST offer both content types, so this is normally true; a client that does not gets its notifications dropped rather than a stream it cannot parse.
Parameters
- $headers : array<string, string>
Return values
boolacknowledge()
Answers a notification.
private
acknowledge(string $method) : StatelessResult
This revision's core defines no client-to-server notification over HTTP —
notifications/cancelled is stdio-only, since closing the response
stream is the cancellation signal here — so anything arriving is either
an extension's or a client still speaking an older revision. Accepting
the former and refusing the latter both come out as a status with no
body; what must not happen is a JSON-RPC response.
Parameters
- $method : string
Return values
StatelessResultcheckInputRequests()
Refuses to send an ask the client cannot answer.
private
checkInputRequests(ResultInterface $result, RequestMeta $meta, string $method, string|int $id) : StatelessResult|null
The handler's mistake rather than the client's, but the client is the one
that has to hear about it, and -32021 is precisely the code for
"processing this needs a capability you did not declare" — so it is
reported as that, and logged as the server-side bug it is.
Parameters
- $result : ResultInterface
- $meta : RequestMeta
- $method : string
- $id : string|int
Return values
StatelessResult|nullcheckVersion()
Header and `_meta` must agree before the version can be judged supported: when they disagree the server cannot know which the client meant, so a mismatch outranks an unsupported version.
private
checkVersion(RequestMeta $meta, array<string, string> $headers, string|int|null $id) : StatelessResult|null
Parameters
- $meta : RequestMeta
- $headers : array<string, string>
- $id : string|int|null
Return values
StatelessResult|nulldiscover()
private
discover() : DiscoverResult
Return values
DiscoverResultdispatch()
private
dispatch(string $method, array<string, mixed> $decoded, RequestMeta $meta, string|int|null $id[, bool $wantsStream = false ]) : StatelessResult
Parameters
- $method : string
- $decoded : array<string, mixed>
- $meta : RequestMeta
- $id : string|int|null
- $wantsStream : bool = false
Return values
StatelessResultencode()
Runs a result through the wire codec. Passed as-is rather than via a json round trip, which would turn a nested `{}` into `[]`.
private
encode(string $method, string|int $id, ResultInterface $result[, bool $cacheable = true ]) : StatelessResult
Parameters
- $method : string
- $id : string|int
- $result : ResultInterface
- $cacheable : bool = true
Return values
StatelessResultheader()
private
header(array<string, string> $headers, string $name) : string|null
Parameters
- $headers : array<string, string>
- $name : string
Return values
string|nullliftInputContext()
Reads the multi round-trip material off a retry, verifying the state before any of it reaches a handler. Neither member means a first call, which is what a handler tests to decide whether it still needs to ask.
private
liftInputContext(array<string, mixed>|null $params) : InputContext|null
Parameters
- $params : array<string, mixed>|null
Tags
Return values
InputContext|nulllisten()
Opens a `subscriptions/listen` stream. The subscription id is the JSON-RPC id of this request, so there is none to mint.
private
listen(array<string, mixed>|null $params, string|int $id) : StatelessResult
Parameters
- $params : array<string, mixed>|null
- $id : string|int
Return values
StatelessResultreadElicitation()
One fiber suspension read as an elicitation, or null when it is not one.
private
static readElicitation(mixed $suspended) : ElicitRequest}|null
Parameters
- $suspended : mixed
-
the payload ClientGateway suspended with
Return values
ElicitRequest}|null —the name the handler gave the ask, and the ask
readNotification()
Reads one fiber suspension, or null when it carries nothing to send.
private
readNotification(mixed $suspended, RequestMeta $meta) : Notification|null
Parameters
- $suspended : mixed
-
the payload ClientGateway suspended with
- $meta : RequestMeta
Return values
Notification|nullrequiresTransportHeaders()
Whether the transport carrying this dispatcher has a header layer whose required members must be present.
private
requiresTransportHeaders() : bool
The validator's presence is the signal: it is what a header-bearing transport installs, and stdio carries its metadata inline instead (see the stdio binding's "Request Metadata").
Return values
boolrun()
Runs a handler, yielding the notifications it emits as it emits them and returning its result.
private
run(RequestHandlerInterface<string|int, ResultInterface> $handler, Request $request, Session $session, RequestMeta $meta) : Generator<null, Response<string|int, ResultInterface>|Error>
The fiber is what makes a handler's $gateway->progress(...) look
synchronous while the caller decides where the notification goes. Server
-to-client requests are never forwarded: this revision carries what it
needs in the result (MRTR), and putting a request on a response stream is
something the transport binding forbids outright.
An elicitation is answered here rather than refused. Already answered, it
resumes the fiber and the handler runs on; not yet, and the ask becomes
the result — abandoning the fiber, since this request has nothing left to
say and the client will re-send it. Abandoning unwinds it, so a handler's
finally still runs; what does not run is everything after the ask.
That is what lets one handler serve both eras through ClientGateway::elicit(); see ElicitationReplay for what it costs.
Parameters
- $handler : RequestHandlerInterface<string|int, ResultInterface>
- $request : Request
- $session : Session
- $meta : RequestMeta
Return values
Generator<null, Response<string|int, ResultInterface>|Error>streamFrames()
The frames of a request-scoped response stream: the notifications the handler emits, then the response that ends it.
private
streamFrames(Generator<null, Response<string|int, ResultInterface>|Error> $run, RequestMeta $meta, string $method, string|int $id, bool $cacheable) : Generator<string|int, mixed>
Parameters
- $run : Generator<null, Response<string|int, ResultInterface>|Error>
- $meta : RequestMeta
- $method : string
- $id : string|int
- $cacheable : bool
Return values
Generator<string|int, mixed>tagWithSubscription()
Every message on a listen stream carries the id of the subscription it belongs to, which is how a client demultiplexes them on stdio — where they all share one channel.
private
static tagWithSubscription(Notification $notification, string|int $id) : array<string, mixed>
Parameters
- $notification : Notification
- $id : string|int
Return values
array<string, mixed>toErrorResult()
The one place a handler's exception becomes an answer, so the streaming and non-streaming paths cannot disagree about which code it earns.
private
toErrorResult(string $method, string|int $id, Throwable $e) : StatelessResult
Parameters
- $method : string
- $id : string|int
- $e : Throwable
Return values
StatelessResultunknownMethod()
A method with no handler, said as precisely as the server can.
private
unknownMethod(string $method, string|int $id) : Error
An extension's method is still -32601 when the extension is off — the
server genuinely does not implement it — but naming the extension turns
an opaque refusal into something the caller can act on.
Parameters
- $method : string
- $id : string|int
Return values
ErrorwithTraceContext()
Puts the request's trace context back onto a notification it caused, so a collector can join the two without the handler carrying it by hand.
private
static withTraceContext(array<string, mixed> $frame, array<string, string> $traceContext) : array<string, mixed>
Parameters
- $frame : array<string, mixed>
- $traceContext : array<string, string>