Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 147 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# AGENTS.md

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.

## Commands

```bash
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 test
```

CI runs PHP 8.1–8.5 with lowest and highest dependencies; 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.

## Architecture

### Two protocol eras, one endpoint

The central design constraint (see `docs/protocol-versions.md`):

- **Handshake era** (≤ `2025-11-25`): `initialize` handshake, sessions (`Mcp-Session-Id`), server→client requests
over the open channel. Dispatched by `Server\Protocol`.
- **Modern era** (`2026-07-28`, SEP-2575): stateless, version and capabilities declared per request in
`params._meta`, discovery via `server/discover`, no server→client requests — a handler returns an
`InputRequiredResult` and the client retries (MRTR). Dispatched by `Server\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

- `Server::builder()` (`Server\Builder`) wires everything; `Server::run($transport)` connects `Protocol` (and
`StatelessProtocol` if the transport supports it) and calls `$transport->listen()`.
- Request/notification handlers live in `Server\Handler\Request|Notification` and implement
`RequestHandlerInterface`/`NotificationHandlerInterface`; custom ones go in via `Builder::addRequestHandler()` /
`addNotificationHandler()`, extensions via `Builder::enableExtension()`.
- Handlers run inside **PHP Fibers**. `ClientGateway` (from `RequestContext`) suspends the fiber with a
`Server\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 in `adr/`) — 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.

### Capabilities (tools, resources, prompts)

- Elements are declared with attributes (`Capability\Attribute\McpTool`, `McpResource`, `McpPrompt`, …) or registered
explicitly on the builder.
- `Capability\Registry\Loader\*` populate the `Registry`: `DiscoveryLoader` scans directories (needs
`symfony/finder`), `ExplicitElementLoader` handles builder registrations, chained by `ChainLoader`.
- `Capability\Discovery\*` derives JSON schemas from signatures and docblocks; `Registry\ReferenceHandler` invokes the
element, injecting `RequestContext` and container services; `Capability\Formatter\*` turns return values into
content.

### Client

`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 and JSON-RPC

`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.

## Conventions

- Throw `Mcp\Exception\*` (all implement `ExceptionInterface`), never native SPL exceptions.
- Add an `@author` tag to newly introduced classes.
- Define array shapes for parameters and return types.
- Prefer classic `if` statements over short-circuit evaluation when possible.
- Avoid `empty()`; prefer explicit checks like `[] === $array`, `'' === $string` or `null === $value`.
- Pre-1.0, BC breaks are allowed but every one needs a `[BC Break]` entry in `CHANGELOG.md` under the unreleased
version, grouped by component (`### Server`, `### Client`, …). Never add entries to an already released version.
- Spec-deprecated features get `@deprecated` plus `trigger_deprecation()` (see `docs/deprecation-policy.md`).
`@internal` code 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 inside `docs/`; 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 in `composer.json` autoload-dev)
and its integration/interop test class.

## Grounding in the Specification

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.

## Code Comments

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.

## Pull Request Comments

- 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.
Loading