This file provides guidance to AI coding agents working with code in this repository.
mcp/sdk is the official PHP SDK for the Model Context Protocol — server and client, framework-agnostic, PHP ^8.1.
It is a PHP Foundation × Symfony collaboration and follows Symfony's coding standards and BC promise.
composer install
npm ci --include=dev --prefix tests/Interop # once; without it the interop suite fails with setup noise
make ci # composer update --prefer-stable, cs, phpstan, all PHPUnit suites
make cs # php-cs-fixer (auto-fixes; @Symfony + @Symfony:risky + file header)
make phpstan # static analysis (phpstan.dist.neon + phpstan-baseline.neon)
make unit-tests # tests/Unit
make integration-tests # tests/Integration — our client against our server
make interop-tests # tests/Interop — needs Node; MCP Inspector + TS reference server
make conformance-tests # official conformance suite, 2025-11-25 (Docker + npx)
make conformance-draft # same for 2026-07-28
make docs-guides # Zensical build of docs/ in --strict mode (via uv)
vendor/bin/phpunit tests/Unit/Server/ProtocolTest.php # single file
vendor/bin/phpunit --filter testMethodName # single testCI runs PHP 8.1–8.6 with lowest and highest dependencies (8.6 is non-blocking while in pre-release); code must stay valid on PHP 8.1. PHPUnit fails on warnings, risky tests and unexpected output.
Integration and interop tests snapshot their output; snapshots are written on first run — delete one to re-record it.
tests/Integration/ClientExamples/ and tests/Interop/Server/ have one test class per script in examples/, so
changing an example usually means updating its snapshot.
The central design constraint (see docs/protocol-versions.md):
- Handshake era (≤
2025-11-25):initializehandshake, sessions (Mcp-Session-Id), server→client requests over the open channel. Dispatched byServer\Protocol. - Modern era (
2026-07-28, SEP-2575): stateless, version and capabilities declared per request inparams._meta, discovery viaserver/discover, no server→client requests — a handler returns anInputRequiredResultand the client retries (MRTR). Dispatched byServer\Stateless\StatelessProtocol.
Both share the request handlers, not control flow. StreamableHttpTransport and StdioTransport serve both eras
(StatelessAwareTransportInterface); Server\Wire\InboundClassifier decides the era once per inbound request.
Server\Wire\Rev2025Codec/Rev2026Codec adapt results to each era's wire shape. The modern codec supplies default
resultType and caching hints where applicable; InputRequiredResult declares its own type, and
ReadResourceResult can carry explicit caching hints.
InputRequiredShim lets a modern-style handler that returns an input-required result run on a handshake connection
by issuing real elicitation/create/sampling/createMessage requests and re-running the handler.
ProtocolVersion::isModern() / FIRST_MODERN_VERSION mark the boundary.
Server::builder()(Server\Builder) wires everything;Server::run($transport)connectsProtocol(andStatelessProtocolif the transport supports it) and calls$transport->listen().- Request/notification handlers live in
Server\Handler\Request|Notificationand implementRequestHandlerInterface/NotificationHandlerInterface; custom ones go in viaBuilder::addRequestHandler()/addNotificationHandler(), extensions viaBuilder::enableExtension(). - Handlers run inside PHP Fibers.
ClientGateway(fromRequestContext) suspends the fiber with aServer\Suspension\RequestSuspension/NotificationSuspension; the transport loop sends the message and resumes the fiber with the client's answer. Request ids to the client are UUIDs, matched per fiber, so concurrent requests of one session don't cross. Server\Session\*: session stores (in-memory, file, PSR-16) and optional per-session locks.Server\Transport\Http\OAuth\*: resource-server-only OAuth (ADR 0001/0002 inadr/) — token validation, scope policy, protected resource metadata. Being an authorization server or proxying the OAuth flow is out of scope.Server\Transport\Http\*: PSR-15 middleware (CORS, DNS rebinding, authorization) in front of the HTTP transport.
- Elements are declared with attributes (
Capability\Attribute\McpTool,McpResource,McpPrompt, …) or registered explicitly on the builder. Capability\Registry\Loader\*populate theRegistry:DiscoveryLoaderscans directories (needssymfony/finder),ExplicitElementLoaderhandles builder registrations, chained byChainLoader.Capability\Discovery\*derives JSON schemas from signatures and docblocks;Registry\ReferenceHandlerinvokes the element, injectingRequestContextand container services;Capability\Formatter\*turns return values into content.
Client::builder() → Client\Protocol handles correlation and the handshake; Client\Stateless\* builds modern-era
request envelopes/headers and resolves input-required results. Transports in Client\Transport (stdio, HTTP with SSE
and reconnect policy).
Schema\* holds the MCP types (requests, results, notifications, content, enums) as immutable value objects
serialized via jsonSerialize(); requests and notifications extend Schema\JsonRpc\Request/Notification and
implement getMethod(), fromParams() and getParams(). JsonRpc\* parses and builds raw messages. Schema\Extension\* is the generic seam
for protocol extensions (ExtensionInterface, e.g. Apps); core handlers, RequestContext and Client must not
reference concrete extension types.
- Throw
Mcp\Exception\*(all implementExceptionInterface), never native SPL exceptions. - Add an
@authortag to newly introduced classes. - Define array shapes for parameters and return types.
- Prefer classic
ifstatements over short-circuit evaluation when possible. - Avoid
empty(); prefer explicit checks like[] === $array,'' === $stringornull === $value. - Pre-1.0, BC breaks are allowed but every one needs a
[BC Break]entry inCHANGELOG.mdunder the unreleased version, grouped by component (### Server,### Client, …). Never add entries to an already released version. - Spec-deprecated features get
@deprecatedplustrigger_deprecation()(seedocs/deprecation-policy.md).@internalcode is outside the BC promise. - Every PHP file carries the license header enforced by php-cs-fixer.
- Docs under
docs/link to each other with relative paths insidedocs/; link repo-root files (README.md,CHANGELOG.md,ROADMAP.md) by their GitHub URL, or the strict Zensical build fails. - New documented patterns usually come with an example under
examples/(registered incomposer.jsonautoload-dev) and its integration/interop test class.
Ground protocol work in the MCP specification instead of assumptions: before implementing or changing protocol behaviour — message shapes, capabilities, error codes, transport semantics — read the specification text of the revision in question, or the SEP that introduces the change. Don't infer behaviour from another SDK (TypeScript, Python, Java) or from a different protocol revision.
Comments are the exception, not the default. Well-named classes, methods, and variables should carry the meaning; add a comment only when the code cannot explain itself.
- Comment the why, never the what: a non-obvious constraint, a spec quirk, a workaround for a client interop issue. Don't narrate what the next line does.
- Describe the implementation as it is, not how it came to be. No references to the change, the ticket, the PR, the review, or a previous version ("fix for #123", "previously we…", "now also handles…", "as requested") — that context belongs in the commit message or PR description.
- Keep comments to a single short line. No multi-line rationale blocks; if a longer explanation is needed, it goes into the PR or the docs.
- Don't add PHPDoc that only repeats the native signature; use PHPDoc for what types can't express (array shapes,
generics,
@throws,@author). - Be especially restrained in central, frequently read classes (
Protocol,StatelessProtocol, the transports,RequestContext,ClientGateway,Schema\*value objects): every comment there is read by every contributor and must keep earning its place. - When editing existing code, don't add comments to explain your own change, and remove comments your change made obsolete.
- Keep comments and PR descriptions short: one to three sentences.
- After a push, say what changed and why. Do not list the checks, tests, assertions or PHP versions you ran, CI shows that.
- Do not report that nothing happened, such as "no outstanding review conversations".
- Reply to a review comment only to answer it or to say how it was resolved.
- Do not post a comment for a rebase or force-push unless it changes the code under review.