mcp/sdk
The official PHP SDK for the Model Context Protocol: a framework-agnostic API for building MCP servers and clients in PHP, with tools, resources, prompts, STDIO and HTTP transports, sessions, authorization, and support for both protocol eras.
Builder::setSessionLock(), serializing the concurrent requests of one session so none of them overwrites what another saved. FileSessionLock locks with flock() next to FileSessionStore, whose gc() also removes stale lock files; SymfonyLockSessionLock uses a symfony/lock store for sessions shared across machines; SessionLockInterface takes custom ones. A request that cannot get the lock in time, or hits a failing lock backend, is answered with 503 and a JSON-RPC error (SessionLockTimeoutException and SessionLockException). Protocol::__construct() takes the lock as optional last argument.Session::save() writes only the keys it changed, onto what the store holds at that point. Writes of a handler after it resumed from an elicitation, sampling request or notification are saved too, and a session deleted while one of its requests still ran no longer comes back when that request saves it.Psr16SessionStore cache being answered as "session not found" (404): the failure is logged and answered with 503, so clients retry instead of re-initializing. SessionStoreInterface::exists() may throw the new SessionStoreException.ClientGateway::progress() takes the token from its own request.StdioTransport keeps every suspended fiber and resumes each with the answer to its own request. A call's notifications now go out before its response.Protocol::sendRequest(), Protocol::checkResponse() and Protocol::handleFiberYield() take and return a Uuid instead of an int, as does the response finder of a transport; handleFiberYield() returns the id of the request it sent. A transport passes the suspended \Fiber as last argument to the pending-requests provider and the fiber yield handler, and StreamableHttpTransport::handleFiberTermination() takes the terminated \Fiber as its first argument.TransportInterface::send() receives every response of a handler that does not suspend, and the outgoing queue only carries server-initiated requests and notifications; a custom transport must deliver each message it gets, as a batch gets several.StreamableHttpTransport bundling the session's queued messages (e.g. notifications/resources/updated) with a request's response into a JSON array, which clients on 2025-06-18 and later reject: such a request is answered as SSE, the queued messages first. A POST of only responses or notifications is accepted with 202 and no body, and the queued messages stay.Protocol::checkResponse() expires them with the Request timed out error, entries of a vanished stream are dropped a minute past their timeout, and an answer to a request the server is not waiting on is dropped instead of stored.null when the transport fails to store what it sends to the client; ClientGateway::notify() then throws a ClientException.OAuthProxyMiddleware, ClientRegistrationMiddleware, ClientRegistrarInterface, ClientRegistrationException, JwksProvider, JwksProviderInterface, OidcDiscoveryInterface and the OIDC metadata policies; OidcDiscovery is internal.OAuthRequestMetaMiddleware with RequestContext::getAccessToken(), returning the validated Server\Authorization\AccessToken; AuthorizationResult::allow() takes an AccessToken instead of request attributes. Protocol::processInput(), BaseTransport::handleMessage() and StreamableHttpTransport::handlePostRequest() take an ?AccessToken $accessToken parameter, as does the TransportInterface::onMessage() listener as fourth argument.JwtTokenValidator final with a single issuer instead of a list of issuer aliases, and a $keys set (e.g. CachedKeySet); JwtTokenValidator::fromIssuer() discovers and caches keys in a PSR-6 pool, refetching on unknown key ids. The alg allowlist is enforced, $tokenType and $leeway are added, requireScopes() is removed in favour of ScopePolicy.ScopePolicy as third argument of AuthorizationMiddleware, answering 403 insufficient_scope per method and tool, with scope hierarchies; the resource_metadata challenge URL comes from the configured resource instead of the Host header.ProtectedResourceMetadata requires $resource, serves at the path derived from it (RFC 9728 §3.1) and requires https except for loopback hosts; drops localized, policy, ToS, extra fields and $metadataPaths.401 behind AuthorizationMiddleware, which now lets OPTIONS requests pass without a token. The default CorsMiddleware allows Mcp-Method, Mcp-Name and Mcp-Param-* and exposes WWW-Authenticate; an $allowedHeaders entry ending in * allows each requested header with that prefix.DnsRebindingProtectionMiddleware always validates Host (a missing one is rejected) and checks Origin separately against the new allowedOrigins, including scheme and port and empty by default. Same-origin requests are always allowed and both lists accept ['*']. allowedOrigins is the new second constructor argument; pass the factories by name.PassthroughMiddleware to opt StreamableHttpTransport out of its default middleware without the warning an empty $middleware list logs.StdioTransport settles the era on the client's first request and serves 2026-07-28 requests, subscriptions/listen and notifications/cancelled on the one channel. StatelessAwareTransportInterface declares setHandshakeVersions(), so a server without the modern era names only the revisions it negotiates when refusing a 2026-07-28 request.initialize on a 2026-07-28-only endpoint with -32022 naming the served revisions, and a request without a session on the handshake leg with its id.Mcp-Param-* header whose mirrored argument is absent from the body with -32020, instead of accepting the request (SEP-2243).listChanged only where list changes are delivered: never in the initialize result, and on server/discover only when the registry publishes them to a notification bus. RegistryInterface declares publishesListChanges(), which Registry answers with true when its dispatcher is a PublishingEventDispatcher; a decorating registry forwards the answer of the one it wraps.structuredContent against the tool's outputSchema, which the specification requires the server to honour. A mismatch is answered with a CallToolResult carrying isError: true instead of the non-conforming value, matching the TypeScript, Python and Java SDKs. Skipped when the tool declares no outputSchema, when the result carries no structuredContent, and when the result is already an error. Return new \stdClass() for an empty object, since [] is sent as an array.Builder::setSchemaValidator() to configure the validator used for tools/call input and output, e.g. with a resolver for external $ref schemas. SchemaValidator takes an optional Opis\JsonSchema\Validator as its first constructor argument, moving $logger to second (pass logger: by name), and validateAgainstJsonSchema() no longer validates an empty array as an object (pass new \stdClass()).SchemaValidator falling back to opis' wording for camelCase keywords like minLength, multipleOf or additionalProperties, and reporting a pointer relative to the $ref target inside $defs. Clients now see the SDK's messages and the full pointer.providerClass argument of #[CompletionProvider]. Use provider:, which takes the same class-string and is now the first positional argument.ClientGateway::log() drops messages below the level set with logging/setLevel, and below warning while the client has set none, and ClientLogger honours a lower level a 2026-07-28 request asks for with io.modelcontextprotocol/logLevel. ClientLogger takes only the ClientGateway, which applies the level itself.Protocol from logging full JSON-RPC payloads (tool arguments, client replies) at info level: info records now carry only the method and id, the raw message is logged at debug level.RequestEvent, ResponseEvent and ErrorEvent not being dispatched for 2026-07-28 requests, and ResponseEvent and ErrorEvent not when a suspended fiber completes.ServerRequestEvent and ClientResponseEvent for handshake-era server-initiated requests (elicitation/create, sampling/createMessage) and the client's replies.tools/call in long-lived processes like FrankenPHP workers or stdio servers.StdioTransport answering a request on an idle connection only after ~50ms: it now waits on the input with stream_select() instead of sleeping.2026-07-28, and connect() probes with server/discover and falls back to the initialize handshake on 2025-11-25 when the server does not speak the modern era. Builder::setFallbackProtocolVersion() picks the fallback revision, or null for a modern-only client. Against a server that speaks both, a default client now settles on 2026-07-28, where a server can no longer sample or list roots through ClientGateway; return those asks as an InputRequiredResult instead, or call Builder::setProtocolVersion(ProtocolVersion::V2025_11_25) on the client.2026-07-28 connection, Client::setLoggingLevel() stamps the level on every following request, Client::ping() sends server/discover and Client::sendRootsListChanged() sends nothing.listen option to the client's HttpTransport, opening the standalone GET stream on which a 2025-era server sends requests and notifications outside of a client request, like roots/list. Needs a PSR-18 client that streams response bodies, such as symfony/http-client.Client::isConnected() turns false.RequestException: it now throws TimeoutException and sends notifications/cancelled for the abandoned request. TimeoutException extends RequestException instead of Exception, so a catch (RequestException) now also catches timeouts. ClientStateInterface::getPendingRequests() reports timestamp as a float from microtime(true) instead of an int.HttpTransport with a GET carrying Last-Event-ID, honouring the server's retry interval. Configure attempts and fallback backoff with ReconnectPolicy, the scheduling clock with any PSR-20 ClockInterface (default: a monotonic clock). Protocol version 2026-07-28 does not resume streams. A stream that ends without its response and without an event ID now fails the request instead of waiting for its timeout.onProgress callback receiving progress for an earlier request that timed out: progress now only reaches the callback of the request whose progressToken it carries.Client::getServerInfo(), getInstructions() and getProtocolVersion() reporting the previous server's values after a reconnect, whether it failed or reached a 2026-07-28 server whose server/discover leaves them out.Client::connect() rejecting an empty serverInfo.version, as the Python SDK sends, and leaving the transport open when initialization fails on a malformed result: the transport is now closed and the failure wrapped in a ConnectionException.HttpTransport failing to connect through an amphp-based PSR-18 client, like Symfony's Psr18Client(new AmpHttpClient()), where connect() failed at the init timeout with FutureIterator::consume(): Return value must be of type ?array: requests are now sent from outside the request fiber, where the event loop runs.Client::getServerCapabilities(), returning what the server declared in initialize or, from 2026-07-28 on, in server/discover. ClientStateInterface declares setServerCapabilities() and getServerCapabilities(), and its setProtocolVersion() and setServerInfo() accept null, which a custom implementation has to follow.HttpTransport::getSessionId() to read the server-minted Mcp-Session-Id: a request-scoped caller can persist it and pass it back through the constructor's $headers on a later transport. Always null on 2026-07-28, which removed protocol-level sessions.HttpTransport omitting the MCP-Protocol-Version header on requests after the initialize handshake, including the closing DELETE.Client::getPrompt() without arguments sending "arguments": [], which servers validating the spec's object type (e.g. the TypeScript SDK) reject: empty arguments are now omitted.\JsonException for request params it cannot encode (e.g. invalid UTF-8): it now throws InvalidArgumentException, and a request handler result it cannot encode is answered with an internal error instead of no answer.MessageInterface::PROTOCOL_VERSION to 2026-07-28. Use ProtocolVersion::latestHandshake() where a handshake revision is needed, e.g. in an initialize answer.Tool::$execution (ToolExecution with a TaskSupport enum) and ServerCapabilities::$tasks, which were dropped when parsing a 2025-11-25 server's tools/list and initialize results.Tool input schema whose properties is not an object or whose required is neither a list nor null, instead of silently replacing the member. Reject a completion/complete whose argument is missing name or value, instead of completing against an empty prefix.ElicitationSchema::getDefaults(), returning the declared default of each field to accept a form elicitation with. AbstractSchemaDefinition declares an abstract getDefault(), which a custom schema definition has to implement.Full Changelog: https://github.com/modelcontextprotocol/php-sdk/compare/v0.8.1...v0.9.0
Full Changelog: https://github.com/modelcontextprotocol/php-sdk/compare/v0.8.0...v0.8.1
With v0.8.0 we are shipping support for the 2026-07-28 Model Context Protocol specification, which includes a major rework on HTTP transport going stateless, while keeping older versions of the specifications alive and mostly compatible in userland code. Find a detailed CHANGELOG in the repository, and per PR below.
structuredContent on the protocol revision by @chr-hertel in https://github.com/modelcontextprotocol/php-sdk/pull/417Full Changelog: https://github.com/modelcontextprotocol/php-sdk/compare/v0.7.1...v0.8.0
Full Changelog: https://github.com/modelcontextprotocol/php-sdk/compare/v0.7.0...v0.7.1
items for array tool parameter schemas by @valeriudev in https://github.com/modelcontextprotocol/php-sdk/pull/378Full Changelog: https://github.com/modelcontextprotocol/php-sdk/compare/v0.6.0...v0.7.0
exception context when logging exceptions by @svenmuennich in https://github.com/modelcontextprotocol/php-sdk/pull/300Mcp\Schema\Resource to Mcp\Schema\ResourceDefinition. No alias.Mcp\Capability\Registry\Loader\ArrayLoader to Mcp\Capability\Registry\Loader\ReflectedElementLoader.2025-11-25ElementReference::$isManual public property and the bool $isManual parameter from all *Reference constructors. Origin tracking is no longer carried on the element; manual-over-discovered precedence is encoded by loader execution order.RegistryInterface::registerTool(), registerResource(), registerResourceTemplate(), registerPrompt() lost their trailing bool $isManual = false parameter. Callers using positional arguments must drop the flag.RegistryInterface::clear(), getDiscoveryState(), setDiscoveryState(). Rediscovery now goes through DiscoveryLoader::load() directly.Builder::addResource() signature changed — $title parameter added between $name and $description. Callers using positional arguments must switch to named arguments.Builder::addResourceTemplate() signature changed — $title parameter added between $name and $description. Callers using positional arguments must switch to named arguments.StreamableHttpTransport constructor: $corsHeaders parameter removed; CORS is now configured via CorsMiddleware. The $middleware parameter is nullable — null (or omitted) installs the default stack; [] disables all defaults. Default Access-Control-Allow-Origin is no longer set (was *).ResourceDefinition::__construct() signature changed — $title parameter added between $name and $description. Callers using positional arguments must switch to named arguments.ResourceTemplate::__construct() signature changed — $title parameter added between $name and $description. Callers using positional arguments must switch to named arguments.McpResource and McpResourceTemplate attribute signatures changed — $title parameter added between $name and $description. Callers using positional arguments must switch to named arguments.Full Changelog: https://github.com/modelcontextprotocol/php-sdk/compare/v0.5.0...v0.6.0
title field to Prompt for MCP spec compliance by @chr-hertel in https://github.com/modelcontextprotocol/php-sdk/pull/278GetPromptRequest::$arguments by @vjik in https://github.com/modelcontextprotocol/php-sdk/pull/285Full Changelog: https://github.com/modelcontextprotocol/php-sdk/compare/v0.4.0...v0.5.0
Psr16StoreSession to Psr16SessionStore for consistency by @chr-hertel in https://github.com/modelcontextprotocol/php-sdk/pull/236Full Changelog: https://github.com/modelcontextprotocol/php-sdk/compare/v0.3.0...v0.4.0
schema to resource in Capability by @luoyue712 in https://github.com/modelcontextprotocol/php-sdk/pull/217Full Changelog: https://github.com/modelcontextprotocol/php-sdk/compare/v0.2.2...v0.3.0
Full Changelog: https://github.com/modelcontextprotocol/php-sdk/compare/v0.2.1...v0.2.2
Full Changelog: https://github.com/modelcontextprotocol/php-sdk/compare/v0.2.0...v0.2.1
Full Changelog: https://github.com/modelcontextprotocol/php-sdk/compare/v0.1.0...v0.2.0
actions/checkout from v4 to v5 by @OskarStark in https://github.com/modelcontextprotocol/php-sdk/pull/24nyholm/nsa dev dependency by @OskarStark in https://github.com/modelcontextprotocol/php-sdk/pull/28composer.json file before install by @OskarStark in https://github.com/modelcontextprotocol/php-sdk/pull/29ConfigurationException by @OskarStark in https://github.com/modelcontextprotocol/php-sdk/pull/31suggest section in composer.json by @OskarStark in https://github.com/modelcontextprotocol/php-sdk/pull/34.yml to .yaml by @OskarStark in https://github.com/modelcontextprotocol/php-sdk/pull/35self to static by @markinigor in https://github.com/modelcontextprotocol/php-sdk/pull/53Server::make() to Server::builder() for better clarity by @CodeWithKyrian in https://github.com/modelcontextprotocol/php-sdk/pull/84completion/complete handler, centralize provider logic, and move completion capability by @CodeWithKyrian in https://github.com/modelcontextprotocol/php-sdk/pull/97Full Changelog: https://github.com/modelcontextprotocol/php-sdk/commits/0.1.0
How can I help you explore Laravel packages today?