Skip to content
Merged
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
/examples export-ignore
/tests export-ignore
/.php-cs-fixer.dist.php export-ignore
/deptrac.yaml export-ignore
/Makefile export-ignore
/mkdocs.yml export-ignore
/requirements-docs.txt export-ignore
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/pipeline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,9 @@ jobs:
- name: PHPStan
run: vendor/bin/phpstan analyse

- name: Package boundaries
run: vendor/bin/deptrac analyse

# Only the phpDocumentor half: this job is PHP-only, and the Zensical
# guides are built (strictly) by the Documentation workflow.
- name: Documentation
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.phpunit.cache
.php-cs-fixer.cache
.deptrac.cache
composer.lock
coverage
vendor
Expand Down
16 changes: 9 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ It is a PHP Foundation × Symfony collaboration and follows Symfony's coding sta
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 ci # composer update --prefer-stable, cs, phpstan, deptrac, 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 deptrac # package boundaries: Schema depends on nothing, Client and Server on Schema only
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
Expand Down Expand Up @@ -73,13 +74,13 @@ by issuing real `elicitation/create`/`sampling/createMessage` requests and re-ru

### Capabilities (tools, resources, prompts)

- Elements are declared with attributes (`Capability\Attribute\McpTool`, `McpResource`, `McpPrompt`, …) or registered
- Elements are declared with attributes (`Server\Capability\Attribute\McpTool`, `McpResource`, `McpPrompt`, …) or registered
explicitly on the builder.
- `Capability\Registry\Loader\*` populate the `Registry`: `DiscoveryLoader` scans directories (needs
- `Server\Capability\Registry\Loader\*` populate the `Registry`: `DiscoveryLoader` scans directories (needs
`symfony/finder`), `ReflectedElementLoader` handles `addTool()` & co, `ExplicitElementLoader` handles `Builder::add()`,
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
- `Server\Capability\Discovery\*` derives JSON schemas from signatures and docblocks; `Registry\ReferenceHandler` invokes the
element, injecting `RequestContext` and container services; `Server\Capability\Formatter\*` turns return values into
content.

### Client
Expand All @@ -92,13 +93,14 @@ 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
implement `getMethod()`, `fromParams()` and `getParams()`. `Schema\MessageFactory` parses 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.
- Throw the `Exception\*` of the package you are in (`Mcp\Schema`, `Mcp\Client` or `Mcp\Server`; all implement
`Mcp\Schema\Exception\ExceptionInterface`), never native SPL exceptions.
- No `suggest` in `composer.json`: code that needs an optional package or extension checks for it and throws an
exception that names what to install.
- Add an `@author` tag to newly introduced classes.
Expand Down
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,27 @@ All notable changes to `mcp/sdk` will be documented in this file.

* [BC Break] Require PHP 8.2 or higher, as PHP 8.1 reached its end of life.

### Namespaces

* [BC Break] Split the root namespace into `Mcp\Schema`, `Mcp\Client` and `Mcp\Server` to prepare package slicing. `Mcp\Schema` depends on nothing, `Mcp\Client` and `Mcp\Server` depend on `Mcp\Schema` only — enforced by Deptrac (`make deptrac`).
* `Mcp\Server` => `Mcp\Server\Server`
* `Mcp\Client` => `Mcp\Client\Client`
* `Mcp\Capability\*` => `Mcp\Server\Capability\*` (`Attribute`, `Completion`, `Discovery`, `Formatter`, `Logger`, `Registry`, `Tool`, plus `Registry` and `RegistryInterface`)
* `Mcp\Event\*` => `Mcp\Server\Event\*`
* `Mcp\JsonRpc\MessageFactory` => `Mcp\Schema\MessageFactory`
* `Mcp\Server\Stateless\RequestMeta` => `Mcp\Schema\RequestMeta`
* [BC Break] Distribute `Mcp\Exception\*` across the three namespaces.
* `Mcp\Exception\{ExceptionInterface,InvalidArgumentException,LogicException,RuntimeException}` => the same names under `Mcp\Schema\Exception\`, `Mcp\Client\Exception\` and `Mcp\Server\Exception\` — pick the one matching the package you catch around
* `Mcp\Exception\Exception` => `Mcp\Client\Exception\Exception` and `Mcp\Server\Exception\Exception`
* `Mcp\Exception\{InvalidInputMessageException,MissingRequestMetaException}` => `Mcp\Schema\Exception\*`
* `Mcp\Exception\{ConnectionException,ElicitationException,RequestCancelledException,RequestException,RootsException,SamplingException,TimeoutException}` => `Mcp\Client\Exception\*`
* `Mcp\Exception\{BadMethodCallException,ClientException,ConfigurationException,ContainerException,InvalidCursorException,MissingRequiredClientCapabilityException,NotFoundExceptionInterface,PromptGetException,PromptNotFoundException,RegistryException,RequestStateException,ResourceNotFoundException,ResourceReadException,ServiceNotFoundException,SessionLockException,SessionLockTimeoutException,SessionStoreException,ToolCallException,ToolNotFoundException}` => `Mcp\Server\Exception\*`
* `Mcp\Exception\HandlerNotFoundException` is removed; it was unused.
* `Mcp\Server\Exception\ExceptionInterface` and `Mcp\Client\Exception\ExceptionInterface` extend `Mcp\Schema\Exception\ExceptionInterface`, so catching the latter still catches any SDK exception.
* The named exceptions now extend their package's `RuntimeException`/`InvalidArgumentException` instead of the global ones, so `catch (Mcp\Server\Exception\RuntimeException)` reaches `ToolCallException`, `PromptGetException` and friends.

* [BC Break] Move `ExtensionInterface::getRequestHandlers()` to the new `Mcp\Server\Extension\ServerExtensionInterface`, so `Mcp\Schema` does not reference `Mcp\Server`. `AbstractExtension` no longer defines it. An extension that serves its methods must implement the new interface, or `Builder::enableExtension()` does not register its handlers.

### Server

* [BC Break] Refuse an OAuth URL whose host is `0.0.0.0`, `::`, an IPv4 address within IPv6 or an IPv4 address in a non-standard form (e.g. `127.1`), or that holds percent-encoding or non-ASCII characters (an internationalized name must be given in punycode), for the issuer, the `jwks_uri` and the resource.
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ make ci
```

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
(PHP CS Fixer, auto-fixes style), `make phpstan` (static analysis), `make deptrac` (package boundaries) 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
Expand Down
9 changes: 6 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: deps-stable deps-low cs phpstan tests unit-tests integration-tests interop-tests e2e-tests coverage ci ci-stable ci-lowest conformance-tests conformance-server conformance-client conformance-client-extensions conformance-draft conformance-draft-server conformance-draft-client docs docs-guides docs-api docs-serve
.PHONY: deps-stable deps-low cs phpstan deptrac tests unit-tests integration-tests interop-tests e2e-tests coverage ci ci-stable ci-lowest conformance-tests conformance-server conformance-client conformance-client-extensions conformance-draft conformance-draft-server conformance-draft-client docs docs-guides docs-api docs-serve

# The 2026-07-28 scenarios ship on the `alpha` dist-tag; `latest` (0.1.x) has
# none of them. Pinned to the same version CI runs (see
Expand All @@ -23,6 +23,9 @@ cs:
phpstan:
vendor/bin/phpstan --memory-limit=-1

deptrac:
vendor/bin/deptrac analyse

tests:
vendor/bin/phpunit

Expand Down Expand Up @@ -83,9 +86,9 @@ coverage:

ci: ci-stable

ci-stable: deps-stable cs phpstan tests
ci-stable: deps-stable cs phpstan deptrac tests

ci-lowest: deps-low cs phpstan tests
ci-lowest: deps-low cs phpstan deptrac tests

# The published site is the guides (Zensical) with the phpDocumentor API
# reference mounted at /api/. `zensical build` wipes site/, so it runs first.
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ A server is a plain PHP class plus a few lines of wiring:

require __DIR__.'/vendor/autoload.php';

use Mcp\Capability\Attribute\McpResource;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Server;
use Mcp\Server\Capability\Attribute\McpResource;
use Mcp\Server\Capability\Attribute\McpTool;
use Mcp\Server\Server;
Comment thread
chr-hertel marked this conversation as resolved.
Resolved
use Mcp\Server\Transport\StdioTransport;

class Calculator
Expand Down Expand Up @@ -84,7 +84,7 @@ The walkthrough in [First server](docs/get-started/first-server.md) explains eac
## Build a client

```php
use Mcp\Client;
use Mcp\Client\Client;
use Mcp\Client\Transport\StdioTransport;

$client = Client::builder()
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"amphp/amp": "^3.0",
"amphp/http-server": "^3.3",
"composer/semver": "^3.0",
"deptrac/deptrac": "^3.0",
"firebase/php-jwt": "^6.10 || ^7.0",
"laminas/laminas-httphandlerrunner": "^2.12",
"nyholm/psr7": "^1.8",
Expand Down Expand Up @@ -92,7 +93,7 @@
"Mcp\\Tests\\": "tests/"
},
"classmap": [
"tests/Unit/Capability/Discovery/Fixtures/AlternativeFileNameToolHandler.class.inc"
"tests/Unit/Server/Capability/Discovery/Fixtures/AlternativeFileNameToolHandler.class.inc"
]
},
"config": {
Expand Down
22 changes: 22 additions & 0 deletions deptrac.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
deptrac:
paths:
- ./src
layers:
- name: Schema
collectors:
- type: classLike
value: ^Mcp\\Schema\\.*
- name: Client
collectors:
- type: classLike
value: ^Mcp\\Client\\.*
- name: Server
collectors:
- type: classLike
value: ^Mcp\\Server\\.*
ruleset:
Schema: ~
Client:
- Schema
Server:
- Schema
4 changes: 2 additions & 2 deletions docs/advanced/custom-handlers.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ built-in defaults:

```php
use Mcp\Schema\ServerCapabilities;
use Mcp\Server;
use Mcp\Server\Server;

$server = Server::builder()
// the SDK detects capabilities from registered elements, so declare what your handlers serve
Expand Down Expand Up @@ -65,7 +65,7 @@ return anything - they perform side effects only.
Attach notification handlers with `addNotificationHandler()` (single) or `addNotificationHandlers()` (multiple):

```php
use Mcp\Server;
use Mcp\Server\Server;

// Handlers are your own classes implementing NotificationHandlerInterface;
// the SDK ships only its internal ones.
Expand Down
10 changes: 5 additions & 5 deletions docs/advanced/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ Configure an event dispatcher when building your server. This example uses `symf
implementation you can install with `composer require symfony/event-dispatcher`:

```php
use Mcp\Event\RequestEvent;
use Mcp\Server;
use Mcp\Server\Event\RequestEvent;
use Mcp\Server\Server;
use Symfony\Component\EventDispatcher\EventDispatcher;

$dispatcher = new EventDispatcher();
Expand Down Expand Up @@ -137,9 +137,9 @@ To change the lists at runtime and react to it, create the registry yourself and
`setEventDispatcher()` doesn't reach a registry you pass with `setRegistry()`:

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

Expand Down
17 changes: 9 additions & 8 deletions docs/advanced/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ A server opts in with `Builder::enableExtension()`. The SDK announces the extens

```php
use Mcp\Schema\Extension\Apps\McpApps;
use Mcp\Server;
use Mcp\Server\Server;

$server = Server::builder()
->setServerInfo('My Server', '1.0.0')
Expand Down Expand Up @@ -82,10 +82,11 @@ Enable it with `Builder::enableExtension(new AuditExtension())`.
A prefix whose second label is `modelcontextprotocol` or `mcp` (e.g. `io.modelcontextprotocol/`) is reserved for
official extensions. The SDK doesn't refuse such an identifier, so choose a prefix of your own.

An extension that adds JSON-RPC methods also implements `getMessages()` and
`getRequestHandlers()`. `getMessages()` returns the request and notification classes of the
new methods, so the SDK can parse them. `getRequestHandlers()` returns the
[request handlers](custom-handlers.md) that answer them. If two enabled extensions define
An extension that adds JSON-RPC methods also implements `getMessages()`, which returns the
request and notification classes of the new methods, so the SDK can parse them. To answer
those methods on a server, it implements `Mcp\Server\Extension\ServerExtensionInterface`,
whose `getRequestHandlers()` returns the [request handlers](custom-handlers.md) that answer
them. If two enabled extensions define
the same method, `enableExtension()` throws a `LogicException`. Extensions bring no
notification handlers: add those with `Builder::addNotificationHandler()`. They
[don't run on revision `2026-07-28`](custom-handlers.md#protocol-2026-07-28).
Expand All @@ -111,7 +112,7 @@ use Mcp\Schema\Extension\Apps\UiResourceContentMeta;
use Mcp\Schema\Extension\Apps\UiResourceCsp;
use Mcp\Schema\Extension\Apps\UiResourcePermissions;
use Mcp\Schema\Extension\Apps\UiToolMeta;
use Mcp\Server;
use Mcp\Server\Server;

$server = Server::builder()
->enableExtension(new McpApps())
Expand Down Expand Up @@ -157,12 +158,12 @@ descriptor marker: `McpApps::resourceMarker()` is a method call and cannot appea
in an attribute, so spell it as `new \stdClass()` there.

```php
use Mcp\Capability\Attribute\McpResource;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Content\TextResourceContents;
use Mcp\Schema\Extension\Apps\McpApps;
use Mcp\Schema\Extension\Apps\ToolVisibility;
use Mcp\Schema\Extension\Apps\UiToolMeta;
use Mcp\Server\Capability\Attribute\McpResource;
use Mcp\Server\Capability\Attribute\McpTool;

final class WeatherApp
{
Expand Down
8 changes: 4 additions & 4 deletions docs/client/authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
A remote MCP server can require an OAuth access token. It answers a request without a token with `401 Unauthorized`. Pass an authenticator to the HTTP transport and the client gets a token and sends the request again:

```php
use Mcp\Client;
use Mcp\Client\Client;
use Mcp\Client\Auth\OAuth;
use Mcp\Client\Transport\HttpTransport;

Expand Down Expand Up @@ -120,7 +120,7 @@ When the client needs a token, the handler throws an `AuthorizationPendingExcept

```php
use Mcp\Client\Transport\HttpTransport;
use Mcp\Exception\AuthorizationPendingException;
use Mcp\Client\Exception\AuthorizationPendingException;

try {
$client->connect(new HttpTransport('https://mcp.example.com/mcp', auth: createAuthenticator()));
Expand Down Expand Up @@ -307,8 +307,8 @@ A `resource` can name a parent path of the MCP server. All paths below it then b
`connect()` reports a failed authorization as a `ConnectionException`. The cause is an `AuthorizationException`:

```php
use Mcp\Exception\AuthorizationException;
use Mcp\Exception\ConnectionException;
use Mcp\Client\Exception\AuthorizationException;
use Mcp\Client\Exception\ConnectionException;

try {
$client->connect($transport);
Expand Down
2 changes: 1 addition & 1 deletion docs/client/connecting.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The `Client\Builder` provides fluent configuration of client instances.
### Basic Configuration

```php
use Mcp\Client;
use Mcp\Client\Client;

$client = Client::builder()
->setClientInfo('My Application', '1.0.0', 'Description of my client')
Expand Down
18 changes: 9 additions & 9 deletions docs/client/errors.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Error handling

Every exception `Client` throws implements `Mcp\Exception\ExceptionInterface`. Catch it to handle
Every exception `Client` throws implements `Mcp\Schema\Exception\ExceptionInterface`. Catch it to handle
client errors in one place. Creating an `HttpTransport` is different: if no PSR-18 client or PSR-17
factory can be found, the constructor throws an exception from `php-http/discovery`. All classes below
live in the `Mcp\Exception` namespace:
live in the `Mcp\Client\Exception` namespace:

| Exception | Thrown when |
| --- | --- |
Expand All @@ -21,7 +21,7 @@ wait for the request timeout.
## ConnectionException

```php
use Mcp\Exception\ConnectionException;
use Mcp\Client\Exception\ConnectionException;

try {
$client->connect($transport);
Expand All @@ -31,14 +31,14 @@ try {
```

A malformed result during `connect()` also throws a `ConnectionException`. In this case, `$e->getPrevious()`
returns the `InvalidArgumentException`.
returns the `Mcp\Schema\Exception\InvalidArgumentException`.

## AuthorizationException

This exception also reports a failed [authorization](authorization.md) flow. The message of a refused request contains the `WWW-Authenticate` header of the response, if the server sends one:

```php
use Mcp\Exception\AuthorizationException;
use Mcp\Client\Exception\AuthorizationException;

try {
$result = $client->callTool('get_weather', ['city' => 'Paris']);
Expand All @@ -52,7 +52,7 @@ During `connect()`, the client doesn't retry the request. It throws a `Connectio
## RequestException

```php
use Mcp\Exception\RequestException;
use Mcp\Client\Exception\RequestException;

try {
$result = $client->callTool('unknown_tool', []);
Expand All @@ -68,8 +68,8 @@ Thrown when the request timeout or the `timeoutSeconds` of a `callTool()` call r
`RequestException`, so catch it first to tell a timeout from an error response:

```php
use Mcp\Exception\RequestException;
use Mcp\Exception\TimeoutException;
use Mcp\Client\Exception\RequestException;
use Mcp\Client\Exception\TimeoutException;

try {
$result = $client->callTool('slow_tool', []);
Expand Down Expand Up @@ -102,7 +102,7 @@ This script connects to a `server.php` next to it, which provides a `process_dat

require __DIR__.'/vendor/autoload.php';

use Mcp\Client;
use Mcp\Client\Client;
use Mcp\Client\Handler\Request\ElicitationCallbackInterface;
use Mcp\Client\Handler\Request\ElicitationRequestHandler;
use Mcp\Client\Transport\StdioTransport;
Expand Down
Loading
Loading