Skip to content
Open
Show file tree
Hide file tree
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
20 changes: 18 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,22 @@ All notable changes to `mcp/sdk` will be documented in this file.
* Fix lost responses on concurrent requests of one session over Streamable HTTP: a POST is answered with its own responses instead of taking them from the session's outgoing queue.
* [BC Break] `TransportInterface::send()` receives every response of a handler that does not suspend, not only errors raised before a session exists; the session's outgoing queue only carries server-initiated requests and notifications. A custom transport must deliver each message it gets, as a batch gets several.
* Serve both protocol eras over stdio: `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.
* Fix `ClientGateway::log()` sending messages below the level the client set with `logging/setLevel`; `ClientLogger` already dropped them.
* [BC Break] `ClientLogger` takes only the `ClientGateway`, which now applies the client-set level itself.
* [BC Break] `StatelessAwareTransportInterface` declares `setHandshakeVersions()`, so a server without the modern era names only the revisions it negotiates when refusing a `2026-07-28` request, e.g. the one set with `Builder::setProtocolVersion()`.
* Answer a bare `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.
* Speak both protocol eras from a client configured with `2026-07-28`: `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.
* [BC Break] Bump the client's default protocol version to `2026-07-28`, so a client speaks both protocol eras unless told otherwise. 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.
* On a `2026-07-28` connection, `Client::setLoggingLevel()` stamps the level on every following request, `Client::ping()` sends `server/discover` and `Client::sendRootsListChanged()` sends nothing.
* Fail a client request at once when the HTTP server refuses it with an error status or the stdio server process exits, instead of waiting out the timeout.
* [BC Break] Bump `MessageInterface::PROTOCOL_VERSION` to `2026-07-28`. Use `ProtocolVersion::latestHandshake()` where a handshake revision is needed, e.g. in an `initialize` answer.
* Fix `ClientLogger` dropping messages below `warning` on `2026-07-28` requests that ask for a lower level with `io.modelcontextprotocol/logLevel`.

0.8.1
-----

* Fix variadic tool parameters (e.g. `int ...$scores`) failing with a cast error: each element of the array a client sends is now cast on its own.
* Fix `Builder::build()` with a registry passed to `setRegistry()` publishing one `list_changed` notification per loaded element. Adds `Registry::loadFrom()`.

0.8.0
-----
Expand Down Expand Up @@ -78,6 +87,13 @@ All notable changes to `mcp/sdk` will be documented in this file.
* Fix empty tool/resource schemas serializing as `[]` instead of `{}`.
* Fix `PromptResultFormatter` dropping `annotations`, `_meta` and `mimeType` for plain-array content.

0.7.1
-----

* Cap the line length the server `StdioTransport` reads (`$maxLineBytes`, default 4 MiB) and discard longer lines, so a peer that never sends a newline cannot exhaust memory (GHSA-vm75-qjc7-xh6w).
* Cap the output buffer of the client `StdioTransport` (`$maxBufferSize`, default 4 MiB) and fail the pending request when it is reached (GHSA-6vrr-g4hw-f3hj).
* Cap the SSE buffer of the client `HttpTransport` (`$maxSseBufferBytes`, default 8 MiB) and fail the pending request when it is reached (GHSA-7m52-jw36-44r3).

0.7.0
-----

Expand Down Expand Up @@ -117,7 +133,7 @@ All notable changes to `mcp/sdk` will be documented in this file.
* [BC Break] `Builder::addResource()` signature changed — `$title` parameter added between `$name` and `$description`. Callers using positional arguments must switch to named arguments.
* [BC Break] `Builder::addResourceTemplate()` signature changed — `$title` parameter added between `$name` and `$description`. Callers using positional arguments must switch to named arguments.
* Add `CorsMiddleware`, `DnsRebindingProtectionMiddleware`, and `ProtocolVersionMiddleware` for `StreamableHttpTransport`, composed automatically as the default stack via `StreamableHttpTransport::defaultMiddleware()`
* [BC BREAK] `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 `*`).
* [BC Break] `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 `*`).
* [BC Break] `ResourceDefinition::__construct()` signature changed — `$title` parameter added between `$name` and `$description`. Callers using positional arguments must switch to named arguments.
* [BC Break] `ResourceTemplate::__construct()` signature changed — `$title` parameter added between `$name` and `$description`. Callers using positional arguments must switch to named arguments.
* [BC Break] `McpResource` and `McpResourceTemplate` attribute signatures changed — `$title` parameter added between `$name` and `$description`. Callers using positional arguments must switch to named arguments.
Expand All @@ -129,7 +145,7 @@ All notable changes to `mcp/sdk` will be documented in this file.
* Add client component for building MCP clients
* Add `Builder::setReferenceHandler()` to allow custom `ReferenceHandlerInterface` implementations (e.g. authorization decorators)
* Add elicitation enum schema types per SEP-1330: `TitledEnumSchemaDefinition`, `MultiSelectEnumSchemaDefinition`, `TitledMultiSelectEnumSchemaDefinition`
* [BC break] Make Symfony Finder component optional. Users would need to install `symfony/finder` now themselves
* [BC Break] Make Symfony Finder component optional. Users would need to install `symfony/finder` now themselves
* Add `LenientOidcDiscoveryMetadataPolicy` for identity providers that omit `code_challenge_methods_supported` (e.g. FusionAuth, Microsoft Entra ID)
* Add OAuth 2.0 Dynamic Client Registration middleware (RFC 7591)
* Add optional `title` field to `Prompt` and `McpPrompt` for MCP spec compliance
Expand Down
37 changes: 26 additions & 11 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,34 @@ Run the full CI suite locally:
make ci
```

This runs, in order: `make cs` (PHP CS Fixer, auto-fixes style), `make phpstan` (static analysis),
and `make tests` (all PHPUnit suites; the interop suite needs Node). All three must pass. If your
change touches protocol-observable behavior, also run:
This runs, in order: `make deps-stable` (`composer update --prefer-stable`), `make cs`
(PHP CS Fixer, auto-fixes style), `make phpstan` (static analysis) and `make tests` (all PHPUnit
suites). All of them must pass.

The interop suite runs against Node packages. Install them once before you run `make ci` or
`make tests`:

```bash
npm ci --prefix tests/Interop
```

If your change touches protocol-observable behavior, also run:

```bash
make conformance-tests # requires Docker
make conformance-tests # requires Docker and Node (npx)
```

## Coding standards

This project follows [Symfony's coding standards](https://symfony.com/doc/current/contributing/code/standards.html)
and [backward compatibility promise](https://symfony.com/doc/current/contributing/code/bc.html). In short:

See [CLAUDE.md](CLAUDE.md) for a fuller tour of the codebase's architecture and layout.
- Until 1.0, a release can break backward compatibility. Add a `[BC Break]` entry to
`CHANGELOG.md` for every break.
- Before you remove a public class, method or option, mark it with a `@deprecated` tag and call
`trigger_deprecation()` when it's used. The
[deprecation policy](docs/deprecation-policy.md) describes the tag and the removal window.
- Code marked `@internal` is not covered by the promise.

## Tests

Expand All @@ -55,9 +69,9 @@ reachable over the wire — end-to-end coverage. The suites are split by what th
`examples/client/`, one test class per example, piping "user input" to STDIN where an example
asks for it, and snapshots what it prints.
- **Interop** (`tests/Interop/`, `make interop-tests`, needs Node): each half works with an
implementation we did not write. `Server/` drives the example servers with the MCP Inspector,
one test class per example; `Client/` runs our client against the TypeScript SDK's reference
server, `@modelcontextprotocol/server-everything`, over stdio and HTTP. Both are installed from
implementation we did not write. `Server/` drives a selection of the example servers with the
MCP Inspector, one test class per example; `Client/` runs our client against the TypeScript
SDK's reference server, `@modelcontextprotocol/server-everything`, over stdio and HTTP. Both are installed from
`tests/Interop/package-lock.json` (`npm ci --prefix tests/Interop`), so a release of one of
their dependencies cannot change what the snapshots see.
- **Conformance** (`tests/Conformance/`, `make conformance-tests`): each half passes the
Expand All @@ -69,15 +83,16 @@ pattern, consider adding or updating an example under `examples/` along with its
## Documentation

The guides under `docs/` are built with [Zensical](https://zensical.org/) in `--strict` mode,
which fails the build on a broken internal link:
which fails the build on a broken internal link. The build runs Zensical through
[uv](https://docs.astral.sh/uv/getting-started/installation/), so install uv first:

```bash
make docs-guides
```

Links between guide pages must be relative paths that resolve within `docs/` (e.g.
`protocol-versions.md`, `../CLAUDE.md` will *not* resolve — Zensical only follows the `docs/` tree).
For anything at the repo root (`CLAUDE.md`, `ROADMAP.md`, `CHANGELOG.md`), link to it by its GitHub
`protocol-versions.md`, `../README.md` will *not* resolve — Zensical only follows the `docs/` tree).
For anything at the repo root (`README.md`, `ROADMAP.md`, `CHANGELOG.md`), link to it by its GitHub
URL instead, matching the pattern already used across `docs/`. The class-level API reference is
generated separately by phpDocumentor (`make docs-api`) and isn't hand-written.

Expand Down
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ exit(Server::builder()
->run(new StdioTransport()));
```

The `setDiscovery()` call needs `symfony/finder`:

```bash
composer require symfony/finder
```

The walkthrough in [First server](docs/get-started/first-server.md) explains each piece, and
[Try it with the Inspector](docs/get-started/inspector.md) shows it running.

Expand All @@ -89,8 +95,9 @@ $result = $client->callTool('add', ['a' => 5, 'b' => 3]);
$client->disconnect();
```

See [Connecting to a server](docs/client/connecting.md) for transports, timeouts, and the
handlers that answer server-initiated requests.
See [Transports](docs/client/transports.md) for STDIO and HTTP, [Connecting to a server](docs/client/connecting.md)
for timeouts, and [Server-initiated requests](docs/client/server-requests.md) for the handlers that
answer requests from the server.

## Documentation

Expand All @@ -103,6 +110,8 @@ The full documentation is published at **[php.sdk.modelcontextprotocol.io](https
- **[Clients](docs/client/index.md)** — Client SDK for connecting to and communicating with MCP servers
- **[Protocol versions](docs/protocol-versions.md)** — The two protocol eras, and what revision `2026-07-28` changed
- **[Advanced](docs/advanced/index.md)** — Events, protocol extensions (including MCP Apps), and custom message handlers
- **[Deprecation policy](docs/deprecation-policy.md)** — How deprecated features are handled
- **[SDK tier target](docs/sdk-tier.md)** — Where the SDK stands on the MCP SDK tiers
- **[Examples](docs/examples.md)** — Runnable server and client examples
- **[API Reference](https://php.sdk.modelcontextprotocol.io/api/)** — Generated class reference

Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This roadmap is a living document that outlines the planned features and improve

- **Server**
- [x] Implement full support for elicitations
- [ ] Implement OAuth2 authentication for server
- [x] Implement OAuth2 authorization for server as a resource server. The authorization server role is out of scope, see [ADR 0001](adr/0001-oauth-authorization-server-out-of-scope.md) and [ADR 0002](adr/0002-resource-server-only.md).
- **Client**
- [x] Implement client-side support
- [x] Implement client examples and documentation
Expand Down
14 changes: 14 additions & 0 deletions docs/advanced/custom-handlers.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ those dependencies in yourself.
> loads and executes them manually. Reach for this API only when you need that level of control and are comfortable
> taking on the additional plumbing.

!!! note
A custom handler can only serve a method the SDK knows. The SDK rejects any other method as unknown before
a handler runs. To add a new method, [write an extension](extensions.md#writing-your-own-extension) that
registers its message classes.

## Request Handlers

Handle JSON-RPC requests (messages with an `id` that expect a response). Request handlers **must** return either a
Expand Down Expand Up @@ -85,6 +90,15 @@ interface NotificationHandlerInterface
- `supports()` decides if the handler should process the incoming notification
- `handle()` performs side effects but **does not** return a value (notifications have no response)

Request handlers stop at the first match. Notification handlers don't: every handler whose `supports()` returns
`true` runs, the built-in ones included.

## Protocol `2026-07-28`

Custom request handlers also serve requests on protocol version `2026-07-28`. Each of these requests gets a new
in-memory session, so nothing you store in `$session` reaches the next request. Notification handlers never run
on this protocol version.

## Key Differences

| Handler Type | Interface | Returns | Use Case |
Expand Down
32 changes: 27 additions & 5 deletions docs/advanced/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ The SDK dispatches 4 broad event types at the protocol level, allowing you to ob
### ResponseEvent

**Dispatched**: When a successful response is ready to be sent to the client, after handler execution.
See [When response and error events are skipped](#when-response-and-error-events-are-skipped) for the exceptions.

**Properties**:

Expand All @@ -55,7 +56,7 @@ The SDK dispatches 4 broad event types at the protocol level, allowing you to ob

### ErrorEvent

**Dispatched**: When an error occurs during request processing.
**Dispatched**: When an error occurs during request processing. The same exceptions as for `ResponseEvent` apply.

**Properties**:

Expand All @@ -76,14 +77,23 @@ The SDK dispatches 4 broad event types at the protocol level, allowing you to ob
- `getSession(): SessionInterface` - The current session
- `getMethod(): string` - Convenience method to get the notification method

### When response and error events are skipped

In the handshake era, a handler that suspends gets no `ResponseEvent` and no `ErrorEvent`. A
handler suspends whenever it calls the `ClientGateway`: `progress()`, `log()`, `notify()`,
`elicit()` or `sample()`, for example. A handler that returns an `InputRequiredResult` suspends
too, because the SDK sends its asks to the client. The transport sends the final result of
such a handler without dispatching an event.

### Protocol `2026-07-28`

Requests on protocol version `2026-07-28` dispatch the same request, response and error events, with a few differences:

- `ResponseEvent` fires on every `InputRequiredResult` round, not only on the final result. Listeners that only care about completed calls need to check the result type.
- `getSession()` returns a new in-memory session for each request. Anything a listener stores there is gone by the next request.
- `NotificationEvent` is not dispatched, since this protocol version runs no notification handlers.
- `NotificationEvent` is not dispatched, since this protocol version runs no notification handlers. This includes `notifications/cancelled` on stdio.
- `server/discover` and `subscriptions/listen` dispatch no events.
- Requests the SDK rejects before a handler runs dispatch no events. Examples are parse errors, a missing `_meta`, a protocol version mismatch, a removed method or a `requestState` that fails verification.

## List Change Events

Expand All @@ -96,12 +106,24 @@ These events are dispatched when the lists of available capabilities change:
| `ResourceTemplateListChangedEvent` | Dispatched when the list of available resource templates changes |
| `PromptListChangedEvent` | Dispatched when the list of available prompts changes |

These events carry no data and are used to notify clients that they should refresh their capability lists.
These events carry no data. The registry dispatches them when you add or remove a tool, resource, resource
template or prompt at runtime. They are not dispatched while the registry loads its initial elements.

The SDK does not send these changes to handshake-era clients. To send them to clients, configure a notification
bus: the SDK then publishes each change to clients listening with `subscriptions/listen`. See
[Subscriptions](../run/subscriptions.md).

To react to a change yourself, add a listener. This example uses `symfony/event-dispatcher`, one PSR-14
implementation you can install with `composer require symfony/event-dispatcher`:

```php
use Mcp\Event\ToolListChangedEvent;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcher;

$dispatcher->addListener(ToolListChangedEvent::class, function (ToolListChangedEvent $event) {
$logger->info('Tool list has changed, clients should refresh');
/** @var LoggerInterface $logger */
$dispatcher = new EventDispatcher();
$dispatcher->addListener(ToolListChangedEvent::class, function (ToolListChangedEvent $event) use ($logger) {
$logger->info('The tool list has changed.');
});
```
Loading
Loading