Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ All notable changes to `mcp/sdk` will be documented in this file.
0.6.0
-----

* Add `Builder::add(Tool|ResourceDefinition|ResourceTemplate|Prompt $definition, ElementHandlerInterface $handler)` for explicit registration of elements whose schema is only known at runtime.
* Add handler interfaces `ToolHandlerInterface`, `ResourceHandlerInterface`, `ResourceTemplateHandlerInterface`, `PromptHandlerInterface`, and the `ElementHandlerInterface` marker.
* [BC Break] Renamed `Mcp\Schema\Resource` to `Mcp\Schema\ResourceDefinition`. No alias.
* [BC Break] Renamed `Mcp\Capability\Registry\Loader\ArrayLoader` to `Mcp\Capability\Registry\Loader\ReflectedElementLoader`.
* [BC Break] Bump default protocol version to `2025-11-25`
* Allow overriding the default name pattern for Discovery
* Add configurable session garbage collection (`gcProbability`/`gcDivisor`)
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ $client->connect($transport);

- [api-platform/mcp](https://github.com/api-platform/mcp) — MCP integration for API Platform
- [bnomei/kirby-mcp](https://github.com/bnomei/kirby-mcp) — MCP server for the Kirby CMS
- [drupal/mcp_server](https://www.drupal.org/project/mcp_server) — MCP server for Drupal exposing configuration and entities as MCP elements
- [josbeir/cakephp-synapse](https://github.com/josbeir/cakephp-synapse) — CakePHP plugin exposing application functionality over MCP
- [nette/mcp-inspector](https://github.com/nette/mcp-inspector) — MCP server for introspecting Nette applications
- [symfony/ai-mate](https://github.com/symfony/ai-mate) — AI development assistant MCP server for Symfony projects
Expand Down
3 changes: 3 additions & 0 deletions docs/mcp-elements.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ Each capability can be registered using two methods:

For manual registration details, see [Server Builder Manual Registration](server-builder.md#manual-capability-registration).

For runtime, config-driven elements whose shape is not known at compile time, see
[Explicit element registration](server-builder.md#explicit-element-registration) in the Server Builder docs.

## Tools

Tools are callable functions that perform actions and return results.
Expand Down
56 changes: 56 additions & 0 deletions docs/server-builder.md
Comment thread
e0ipso marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,61 @@ the handler's method name and docblock.

For more details on MCP elements, handlers, and attribute-based discovery, see [MCP Elements](mcp-elements.md).

### Explicit element registration

When an element's name, schema, or description is only known at runtime, pair an `Mcp\Schema\*` value object with one of
the four handler interfaces below and register it through `Builder::add()`.

| Element kind | Handler interface |
|-------------------|-------------------------------------------------------|
| Tool | `Mcp\Server\Handler\ToolHandlerInterface` |
| Resource | `Mcp\Server\Handler\ResourceHandlerInterface` |
| Resource template | `Mcp\Server\Handler\ResourceTemplateHandlerInterface` |
| Prompt | `Mcp\Server\Handler\PromptHandlerInterface` |

Each handler interface declares a single execution method. Tool and prompt handlers receive an arguments map and a
`ClientGateway`. Resource handlers receive the requested URI; resource template handlers additionally receive the parsed
template variables.

```php
use Mcp\Schema\Tool;
use Mcp\Server;
use Mcp\Server\ClientGateway;
use Mcp\Server\Handler\ToolHandlerInterface;

final class WeatherHandler implements ToolHandlerInterface
{
public function execute(array $arguments, ClientGateway $gateway): mixed
{
return ['temperature' => 21, 'unit' => 'C'];
}
}

$tool = new Tool(
name: 'get_weather',
title: null,
inputSchema: [
'type' => 'object',
'properties' => ['city' => ['type' => 'string']],
'required' => ['city'],
],
description: 'Returns the current weather for a city.',
annotations: null,
);

$server = Server::builder()
->add($tool, new WeatherHandler())
->build();
```

`Builder::add()` validates the pairing at registration time. Pairing a `Tool` definition with, for example, a
`PromptHandlerInterface` raises `Mcp\Exception\InvalidArgumentException`. The schema value object validates its own
inputs (name pattern, schema shape, etc.), so passing an incomplete definition fails before `add()` returns.

Use `add()` when the metadata cannot be inferred from a handler class via reflection. For statically-known elements,
prefer `addTool/addResource/addResourceTemplate/addPrompt`, which can derive metadata from the handler's signature and
docblock.

## Service Dependencies

### Container
Expand Down Expand Up @@ -619,4 +674,5 @@ $server = Server::builder()
| `addResource()` | handler, uri, name?, description?, mimeType?, size?, annotations? | Register resource |
| `addResourceTemplate()` | handler, uriTemplate, name?, description?, mimeType?, annotations? | Register resource template |
| `addPrompt()` | handler, name?, description? | Register prompt |
| `add()` | definition, handler | Register an element from a schema VO + handler pair |
| `build()` | - | Create the server instance |
4 changes: 2 additions & 2 deletions src/Capability/Discovery/Discoverer.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
use Mcp\Exception\RuntimeException;
use Mcp\Schema\Prompt;
use Mcp\Schema\PromptArgument;
use Mcp\Schema\Resource;
use Mcp\Schema\ResourceDefinition;
use Mcp\Schema\ResourceTemplate;
use Mcp\Schema\Tool;
use Psr\Log\LoggerInterface;
Expand Down Expand Up @@ -249,7 +249,7 @@ private function processMethod(\ReflectionMethod $method, array &$discoveredCoun
$docBlock = $this->docBlockParser->parseDocBlock($method->getDocComment() ?? null);
$name = $instance->name ?? ('__invoke' === $methodName ? $classShortName : $methodName);
$description = $instance->description ?? $this->docBlockParser->getDescription($docBlock) ?? null;
$resource = new Resource(
$resource = new ResourceDefinition(
$instance->uri,
$name,
$instance->title,
Expand Down
4 changes: 2 additions & 2 deletions src/Capability/Registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
use Mcp\Exception\ToolNotFoundException;
use Mcp\Schema\Page;
use Mcp\Schema\Prompt;
use Mcp\Schema\Resource;
use Mcp\Schema\ResourceDefinition;
use Mcp\Schema\ResourceTemplate;
use Mcp\Schema\Tool;
use Psr\EventDispatcher\EventDispatcherInterface;
Expand Down Expand Up @@ -83,7 +83,7 @@ public function registerTool(Tool $tool, callable|array|string $handler): ToolRe
return $reference;
}

public function registerResource(Resource $resource, callable|array|string $handler): ResourceReference
public function registerResource(ResourceDefinition $resource, callable|array|string $handler): ResourceReference
{
$reference = new ResourceReference($resource, $handler);
$this->resources[$resource->uri] = $reference;
Expand Down
105 changes: 105 additions & 0 deletions src/Capability/Registry/Loader/ExplicitElementLoader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

/*
* This file is part of the official PHP MCP SDK.
*
* A collaboration between Symfony and the PHP Foundation.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Mcp\Capability\Registry\Loader;

use Mcp\Capability\Completion\ProviderInterface;
use Mcp\Capability\Registry\ReferenceHandler;
use Mcp\Capability\RegistryInterface;
use Mcp\Schema\Prompt;
use Mcp\Schema\ResourceDefinition;
use Mcp\Schema\ResourceTemplate;
use Mcp\Schema\Tool;
use Mcp\Server\ClientGateway;
use Mcp\Server\Handler\PromptHandlerInterface;
use Mcp\Server\Handler\ResourceHandlerInterface;
use Mcp\Server\Handler\ResourceTemplateHandlerInterface;
use Mcp\Server\Handler\ToolHandlerInterface;

/**
* Translates `Builder::add()` definition+handler pairs into Registry entries.
*
* Each registered closure is bound to {@see ReferenceHandler} as its scope so the
* reference handler short-circuits reflection and invokes it with the raw argument bag.
*
* @author Mateu Aguiló Bosch <mateu.aguilo.bosch@gmail.com>
*/
final class ExplicitElementLoader implements LoaderInterface
Comment thread
e0ipso marked this conversation as resolved.
{
/**
* @param list<array{definition: Tool, handler: ToolHandlerInterface}> $tools
* @param list<array{definition: ResourceDefinition, handler: ResourceHandlerInterface}> $resources
* @param list<array{definition: ResourceTemplate, handler: ResourceTemplateHandlerInterface, completionProviders: array<string, ProviderInterface>}> $resourceTemplates
* @param list<array{definition: Prompt, handler: PromptHandlerInterface, completionProviders: array<string, ProviderInterface>}> $prompts
*/
public function __construct(
private readonly array $tools = [],
private readonly array $resources = [],
private readonly array $resourceTemplates = [],
private readonly array $prompts = [],
) {
}

public function load(RegistryInterface $registry): void
{
foreach ($this->tools as $entry) {
$handler = $entry['handler'];
$registry->registerTool($entry['definition'], $this->boundClosure(
static function (array $arguments) use ($handler): mixed {
$gateway = new ClientGateway($arguments['_session']);
unset($arguments['_session'], $arguments['_request']);

return $handler->execute($arguments, $gateway);
},
));
}

foreach ($this->resources as $entry) {
$handler = $entry['handler'];
$registry->registerResource($entry['definition'], $this->boundClosure(
static fn (array $arguments): mixed => $handler->read(
$arguments['uri'],
new ClientGateway($arguments['_session']),
),
));
}

foreach ($this->resourceTemplates as $entry) {
$handler = $entry['handler'];
$registry->registerResourceTemplate($entry['definition'], $this->boundClosure(
static function (array $arguments) use ($handler): mixed {
$gateway = new ClientGateway($arguments['_session']);
$uri = $arguments['uri'];
unset($arguments['_session'], $arguments['_request'], $arguments['uri']);

return $handler->read($uri, $arguments, $gateway);
},
), $entry['completionProviders']);
}

foreach ($this->prompts as $entry) {
$handler = $entry['handler'];
$registry->registerPrompt($entry['definition'], $this->boundClosure(
static function (array $arguments) use ($handler): mixed {
$gateway = new ClientGateway($arguments['_session']);
unset($arguments['_session'], $arguments['_request']);

return $handler->get($arguments, $gateway);
},
), $entry['completionProviders']);
}
}

private function boundClosure(\Closure $closure): \Closure
{
return \Closure::bind($closure, null, ReferenceHandler::class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
use Mcp\Schema\Icon;
use Mcp\Schema\Prompt;
use Mcp\Schema\PromptArgument;
use Mcp\Schema\Resource;
use Mcp\Schema\ResourceDefinition;
use Mcp\Schema\ResourceTemplate;
use Mcp\Schema\Tool;
use Mcp\Schema\ToolAnnotations;
Expand All @@ -39,7 +39,7 @@
*
* @phpstan-import-type Handler from ElementReference
*/
final class ArrayLoader implements LoaderInterface
final class ReflectedElementLoader implements LoaderInterface
{
/**
* @param array{
Expand Down Expand Up @@ -156,7 +156,7 @@ public function load(RegistryInterface $registry): void
$description = $data['description'] ?? $docBlockParser->getDescription($docBlock) ?? null;
}

$resource = new Resource(
$resource = new ResourceDefinition(
uri: $data['uri'],
name: $name,
title: $data['title'] ?? null,
Expand Down
15 changes: 15 additions & 0 deletions src/Capability/Registry/ReferenceHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use Mcp\Exception\InvalidArgumentException;
use Mcp\Exception\RegistryException;
use Mcp\Server\ClientGateway;
use Mcp\Server\RequestContext;
use Mcp\Server\Session\SessionInterface;
use Psr\Container\ContainerInterface;
Expand All @@ -32,6 +33,15 @@ public function __construct(
*/
public function handle(ElementReference $reference, array $arguments): mixed
{
// Closures bound to this class as their scope consume the raw argument bag
// directly. Used by ExplicitElementLoader so reflection + name-based parameter
// mapping is bypassed for explicitly registered handler interfaces.
if ($reference->handler instanceof \Closure
&& self::class === (new \ReflectionFunction($reference->handler))->getClosureScopeClass()?->getName()
) {
return ($reference->handler)($arguments);
}

$session = $arguments['_session'];

if (\is_string($reference->handler)) {
Expand Down Expand Up @@ -102,6 +112,11 @@ private function prepareArguments(\ReflectionFunctionAbstract $reflection, array
$finalArgs[$paramPosition] = new RequestContext($arguments['_session'], $arguments['_request']);
continue;
}

if (ClientGateway::class === $typeName && isset($arguments['_session'])) {
$finalArgs[$paramPosition] = new ClientGateway($arguments['_session']);
continue;
}
}

if (isset($arguments[$paramName])) {
Expand Down
4 changes: 2 additions & 2 deletions src/Capability/Registry/ResourceReference.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

use Mcp\Capability\Formatter\ResourceResultFormatter;
use Mcp\Schema\Content\ResourceContents;
use Mcp\Schema\Resource;
use Mcp\Schema\ResourceDefinition;

/**
* @phpstan-import-type Handler from ElementReference
Expand All @@ -26,7 +26,7 @@ class ResourceReference extends ElementReference
* @param Handler $handler
*/
public function __construct(
public readonly Resource $resource,
public readonly ResourceDefinition $resource,
callable|array|string $handler,
) {
parent::__construct($handler);
Expand Down
4 changes: 2 additions & 2 deletions src/Capability/RegistryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
use Mcp\Exception\ToolNotFoundException;
use Mcp\Schema\Page;
use Mcp\Schema\Prompt;
use Mcp\Schema\Resource;
use Mcp\Schema\ResourceDefinition;
use Mcp\Schema\ResourceTemplate;
use Mcp\Schema\Tool;

Expand All @@ -47,7 +47,7 @@ public function registerTool(Tool $tool, callable|array|string $handler): ToolRe
*
* @param Handler $handler
*/
public function registerResource(Resource $resource, callable|array|string $handler): ResourceReference;
public function registerResource(ResourceDefinition $resource, callable|array|string $handler): ResourceReference;

/**
* Registers a resource template with its handler and completion providers.
Expand Down
4 changes: 2 additions & 2 deletions src/Schema/Page.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
namespace Mcp\Schema;

/**
* @phpstan-type PageItem Tool|Prompt|ResourceTemplate|Resource
* @phpstan-type PageItem Tool|Prompt|ResourceTemplate|ResourceDefinition
*
* @extends \ArrayObject<int|string, PageItem>
*/
final class Page extends \ArrayObject
{
/**
* @param array<int|string, PageItem> $references Items can be Tool, Prompt, ResourceTemplate, or Resource
* @param array<int|string, PageItem> $references Items can be Tool, Prompt, ResourceTemplate, or ResourceDefinition
*/
public function __construct(
public readonly array $references,
Expand Down
12 changes: 6 additions & 6 deletions src/Schema/Resource.php → src/Schema/ResourceDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
* @phpstan-import-type AnnotationsData from Annotations
* @phpstan-import-type IconData from Icon
*
* @phpstan-type ResourceData array{
* @phpstan-type ResourceDefinitionData array{
* uri: string,
* name: string,
* title?: string,
Expand All @@ -33,7 +33,7 @@
*
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
*/
class Resource implements \JsonSerializable
class ResourceDefinition implements \JsonSerializable
{
/**
* Resource name pattern regex - must contain only alphanumeric characters, underscores, and hyphens.
Expand Down Expand Up @@ -77,19 +77,19 @@ public function __construct(
}

/**
* @param ResourceData $data
* @param ResourceDefinitionData $data
*/
public static function fromArray(array $data): self
{
if (empty($data['uri']) || !\is_string($data['uri'])) {
throw new InvalidArgumentException('Invalid or missing "uri" in Resource data.');
throw new InvalidArgumentException('Invalid or missing "uri" in ResourceDefinition data.');
}
if (empty($data['name']) || !\is_string($data['name'])) {
throw new InvalidArgumentException('Invalid or missing "name" in Resource data.');
throw new InvalidArgumentException('Invalid or missing "name" in ResourceDefinition data.');
}

if (!empty($data['_meta']) && !\is_array($data['_meta'])) {
throw new InvalidArgumentException('Invalid "_meta" in Resource data.');
throw new InvalidArgumentException('Invalid "_meta" in ResourceDefinition data.');
}

return new self(
Expand Down
Loading