MCP PHP SDK

Protocol

Client protocol handler for MCP communication.

Handles message routing, request/response correlation, and the initialization handshake. All blocking operations are delegated to the transport.

Tags
phpstan-import-type

FiberSuspend from TransportInterface

author

Kyrian Obikwelu koshnawaza@gmail.com

Table of Contents

Constants

MAX_ROUND_TRIPS  : mixed = 10
How many times a request may be re-sent before the client gives up.

Properties

$envelope  : RequestEnvelope|null
Set only when the configured revision has no handshake.
$headers  : HeaderFactory|null
$inputRequests  : InputRequestResolver
$logger  : LoggerInterface
$messageFactory  : MessageFactory
$notificationHandlers  : array<string|int, NotificationHandlerInterface>
$progressTokens  : int
Progress tokens are only required to be unique within a connection, and a retry keeps the caller's one — the work being reported on is the same.
$requestHandlers  : array<string|int, mixed>
$state  : ClientStateInterface
$tools  : ToolCatalog
$transport  : TransportInterface|null

Methods

__construct()  : mixed
connect()  : void
Connect this protocol to a transport.
getState()  : ClientStateInterface
getToolCatalog()  : ToolCatalog
What the client knows about the server's tools, from `tools/list`.
initialize()  : Response<string|int, array<string, mixed>>|Error
Ready the connection for use.
processMessage()  : void
Process an incoming message from the server.
request()  : Response<string|int, array<string, mixed>>|Error
Send a request to the server and wait for response.
sendNotification()  : void
Send a notification to the server (fire and forget).
discover()  : Response<string|int, array<string, mixed>>
Stand in for the handshake in the modern era.
exchange()  : Response<string|int, array<string, mixed>>|Error
One request on the wire: assign an id, send, and wait for its answer.
handleNotification()  : void
Handle a notification from the server.
handleRequest()  : void
Handle a request from the server (e.g., sampling request).
handleResponse()  : void
Handle a response from the server.
headersFor()  : array<string, string>
The headers belonging to an encoded message, for a transport that has any.
readDiscovery()  : void
Read defensively: `server/discover` is optional, so a server may answer with something that is not a DiscoverResult at all, and none of it is load-bearing for the requests that follow.
reconcileVersion()  : void
Move to a revision the server actually speaks, if it said which.
send()  : void
Encode and hand a message to the transport, stamping the per-request envelope on the way out when the revision calls for one.
sendResponse()  : void
Send a response back to the server (for server-initiated requests).
withAcceptedVersion()  : ProtocolVersion|null
Switches the offered revision when the server refuses the current one, or null when there is nothing to retry with.
withMeta()  : array<string, mixed>

Constants

MAX_ROUND_TRIPS

How many times a request may be re-sent before the client gives up.

private mixed MAX_ROUND_TRIPS = 10

Both loops that re-send are bounded by it: a server that keeps asking for input, and one that keeps rejecting the offered revision. Neither is expected to run more than a round or two, so the cap is only there to stop a broken or hostile server from spinning the client forever.

Properties

$progressTokens

Progress tokens are only required to be unique within a connection, and a retry keeps the caller's one — the work being reported on is the same.

private int $progressTokens = 0

$requestHandlers read-only

private array<string|int, mixed> $requestHandlers = []

Methods

__construct()

public __construct([array<string|int, RequestHandlerInterface<string|int, mixed>> $requestHandlers = [] ][, array<string|int, NotificationHandlerInterface$notificationHandlers = [] ][, MessageFactory|null $messageFactory = null ][, LoggerInterface|null $logger = null ]) : mixed
Parameters
$requestHandlers : array<string|int, RequestHandlerInterface<string|int, mixed>> = []
$notificationHandlers : array<string|int, NotificationHandlerInterface> = []
$messageFactory : MessageFactory|null = null
$logger : LoggerInterface|null = null

getToolCatalog()

What the client knows about the server's tools, from `tools/list`.

public getToolCatalog() : ToolCatalog

Kept on the protocol rather than the facade because it is what makes the SEP-2243 headers derivable at send time.

Return values
ToolCatalog

initialize()

Ready the connection for use.

public initialize(Configuration $config) : Response<string|int, array<string, mixed>>|Error

Up to 2025-11-25 that means the initialize handshake: offer a revision, take the server's answer, confirm with notifications/initialized. From 2026-07-28 there is no handshake at all — see self::discover().

Parameters
$config : Configuration

The client configuration

Return values
Response<string|int, array<string, mixed>>|Error

processMessage()

Process an incoming message from the server.

public processMessage(string $input) : void

Routes to appropriate handler based on message type.

Parameters
$input : string

request()

Send a request to the server and wait for response.

public request(Request $request, int $timeout[, bool $withProgress = false ]) : Response<string|int, array<string, mixed>>|Error

If a response is immediately available (sync HTTP), returns it. Otherwise, suspends the Fiber and waits for the transport to resume it.

In the modern era this is also where the two loops that re-send live: answering a server's request for input (SEP-2322), and retrying under a revision the server accepts (SEP-2575). Both re-send the same call, so they belong together and above the single exchange.

Parameters
$request : Request

The request to send

$timeout : int

The timeout in seconds

$withProgress : bool = false

Whether to attach a progress token to the request

Return values
Response<string|int, array<string, mixed>>|Error

sendNotification()

Send a notification to the server (fire and forget).

public sendNotification(Notification $notification) : void
Parameters
$notification : Notification

discover()

Stand in for the handshake in the modern era.

private discover(Configuration $config) : Response<string|int, array<string, mixed>>

There is nothing to negotiate: the revision travels on every request, so the connection is usable the moment the transport is. server/discover is only asked because the facade exposes getServerInfo(), and a server that will not answer it still serves every other method — so a failure here is logged and the connection proceeds.

Parameters
$config : Configuration
Return values
Response<string|int, array<string, mixed>>

exchange()

One request on the wire: assign an id, send, and wait for its answer.

private exchange(array<string, mixed> $payload, int $timeout) : Response<string|int, array<string, mixed>>|Error

A retry gets a new id, because the previous one is spent — the server has already answered it, and reusing it would make the two indistinguishable.

Parameters
$payload : array<string, mixed>
$timeout : int
Return values
Response<string|int, array<string, mixed>>|Error

handleNotification()

Handle a notification from the server.

private handleNotification(Notification $notification) : void
Parameters
$notification : Notification

handleRequest()

Handle a request from the server (e.g., sampling request).

private handleRequest(Request $request) : void
Parameters
$request : Request

handleResponse()

Handle a response from the server.

private handleResponse(Response<string|int, mixed>|Error $response) : void

This stores it in session. The transport will pick it up and resume the Fiber.

Parameters
$response : Response<string|int, mixed>|Error

headersFor()

The headers belonging to an encoded message, for a transport that has any.

private headersFor(string $payload) : array<string, string>
Parameters
$payload : string
Return values
array<string, string>

readDiscovery()

Read defensively: `server/discover` is optional, so a server may answer with something that is not a DiscoverResult at all, and none of it is load-bearing for the requests that follow.

private readDiscovery(array<string, mixed> $result) : void
Parameters
$result : array<string, mixed>

reconcileVersion()

Move to a revision the server actually speaks, if it said which.

private reconcileVersion(mixed $supportedVersions) : void

server/discover reports rather than negotiates, so a client that asked for something the server does not list learns it here — and learning it now is far better than a stream of refusals later. A server that stays silent about its versions is left alone; the method is optional and saying nothing is not the same as saying no.

Parameters
$supportedVersions : mixed

send()

Encode and hand a message to the transport, stamping the per-request envelope on the way out when the revision calls for one.

private send(array<string, mixed> $payload, string $kind) : void
Parameters
$payload : array<string, mixed>
$kind : string

sendResponse()

Send a response back to the server (for server-initiated requests).

private sendResponse(Response<string|int, mixed>|Error $response) : void
Parameters
$response : Response<string|int, mixed>|Error

withAcceptedVersion()

Switches the offered revision when the server refuses the current one, or null when there is nothing to retry with.

private withAcceptedVersion(Error $error) : ProtocolVersion|null
Parameters
$error : Error

the server's refusal

Return values
ProtocolVersion|null

withMeta()

private static withMeta(array<string, mixed> $payload, array<string, mixed> $meta) : array<string, mixed>
Parameters
$payload : array<string, mixed>
$meta : array<string, mixed>
Return values
array<string, mixed>
On this page

Search results