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
5 changes: 5 additions & 0 deletions docs/mcp-elements.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ 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
(e.g. bridging configuration entities into MCP elements), 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
62 changes: 62 additions & 0 deletions docs/server-builder.md
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,67 @@ 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
(for example, a Drupal module bridging configuration entities into MCP
tools), 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\ConfigurationException`. 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 @@ -583,4 +644,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 |
9 changes: 5 additions & 4 deletions src/Capability/Registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
use Mcp\Schema\Resource;
use Mcp\Schema\ResourceTemplate;
use Mcp\Schema\Tool;
use Mcp\Server\Handler\ElementHandlerInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
Expand Down Expand Up @@ -68,7 +69,7 @@ public function __construct(
) {
}

public function registerTool(Tool $tool, callable|array|string $handler, bool $isManual = false): void
public function registerTool(Tool $tool, callable|array|string|ElementHandlerInterface $handler, bool $isManual = false): void
{
$toolName = $tool->name;
$existing = $this->tools[$toolName] ?? null;
Expand All @@ -92,7 +93,7 @@ public function registerTool(Tool $tool, callable|array|string $handler, bool $i
$this->eventDispatcher?->dispatch(new ToolListChangedEvent());
}

public function registerResource(Resource $resource, callable|array|string $handler, bool $isManual = false): void
public function registerResource(Resource $resource, callable|array|string|ElementHandlerInterface $handler, bool $isManual = false): void
{
$uri = $resource->uri;
$existing = $this->resources[$uri] ?? null;
Expand All @@ -112,7 +113,7 @@ public function registerResource(Resource $resource, callable|array|string $hand

public function registerResourceTemplate(
ResourceTemplate $template,
callable|array|string $handler,
callable|array|string|ElementHandlerInterface $handler,
array $completionProviders = [],
bool $isManual = false,
): void {
Expand All @@ -139,7 +140,7 @@ public function registerResourceTemplate(

public function registerPrompt(
Prompt $prompt,
callable|array|string $handler,
callable|array|string|ElementHandlerInterface $handler,
array $completionProviders = [],
bool $isManual = false,
): void {
Expand Down
7 changes: 5 additions & 2 deletions src/Capability/Registry/ElementReference.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@

namespace Mcp\Capability\Registry;

use Mcp\Server\Handler\ElementHandlerInterface;

/**
* @phpstan-type Handler \Closure|array{0: object|string, 1: string}|string
* @phpstan-type CallableHandler \Closure|array{0: object|string, 1: string}|string
* @phpstan-type Handler CallableHandler|ElementHandlerInterface
Comment on lines +17 to +18
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am splitting this in two for convenience and documentation brevity.

*
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
*/
Expand All @@ -22,7 +25,7 @@ class ElementReference
* @param Handler $handler
*/
public function __construct(
public readonly \Closure|array|string $handler,
public readonly \Closure|array|string|ElementHandlerInterface $handler,
public readonly bool $isManual = false,
) {
}
Expand Down
12 changes: 6 additions & 6 deletions src/Capability/Registry/Loader/ArrayLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@
/**
* @author Antoine Bluchet <soyuka@gmail.com>
*
* @phpstan-import-type Handler from ElementReference
* @phpstan-import-type CallableHandler from ElementReference
*/
final class ArrayLoader implements LoaderInterface
{
/**
* @param array{
* handler: Handler,
* handler: CallableHandler,
* name: ?string,
* title: ?string,
* description: ?string,
Expand All @@ -53,7 +53,7 @@ final class ArrayLoader implements LoaderInterface
* outputSchema: ?array<string, mixed>
* }[] $tools
* @param array{
* handler: Handler,
* handler: CallableHandler,
* uri: string,
* name: ?string,
* description: ?string,
Expand All @@ -64,7 +64,7 @@ final class ArrayLoader implements LoaderInterface
* meta: ?array<string, mixed>
* }[] $resources
* @param array{
* handler: Handler,
* handler: CallableHandler,
* uriTemplate: string,
* name: ?string,
* description: ?string,
Expand All @@ -73,7 +73,7 @@ final class ArrayLoader implements LoaderInterface
* meta: ?array<string, mixed>
* }[] $resourceTemplates
* @param array{
* handler: Handler,
* handler: CallableHandler,
* name: ?string,
* description: ?string,
* icons: ?Icon[],
Expand Down Expand Up @@ -278,7 +278,7 @@ public function load(RegistryInterface $registry): void
}

/**
* @param Handler $handler
* @param CallableHandler $handler
*/
private function getHandlerDescription(\Closure|array|string $handler): string
{
Expand Down
65 changes: 65 additions & 0 deletions src/Capability/Registry/Loader/ExplicitElementLoader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?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\RegistryInterface;
use Mcp\Schema\Prompt;
use Mcp\Schema\ResourceTemplate;
use Mcp\Schema\Tool;
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 pair is registered with `isManual: true`, matching the precedence used
* by the `ArrayLoader` for closure/array/string handlers.
*
* @author Mateu Aguiló Bosch <mateu.aguilo.bosch@gmail.com>
*/
final class ExplicitElementLoader implements LoaderInterface
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO this feels a bit excessive indirection, but I believe this is the idiomatic way to accomplish this.

Perhaps we can avoid this if we add the elements to the registry directly in the builder. However, that approach will break the current responsibility encapsulation.

Guidance/validation here is appreciated.

{
/**
* @param list<array{definition: Tool, handler: ToolHandlerInterface}> $tools
* @param list<array{definition: \Mcp\Schema\Resource, handler: ResourceHandlerInterface}> $resources
* @param list<array{definition: ResourceTemplate, handler: ResourceTemplateHandlerInterface}> $resourceTemplates
* @param list<array{definition: Prompt, handler: PromptHandlerInterface}> $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) {
$registry->registerTool($entry['definition'], $entry['handler'], true);
}

foreach ($this->resources as $entry) {
$registry->registerResource($entry['definition'], $entry['handler'], true);
}

foreach ($this->resourceTemplates as $entry) {
$registry->registerResourceTemplate($entry['definition'], $entry['handler'], [], true);
}

foreach ($this->prompts as $entry) {
$registry->registerPrompt($entry['definition'], $entry['handler'], [], true);
}
}
}
3 changes: 2 additions & 1 deletion src/Capability/Registry/PromptReference.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Mcp\Capability\Formatter\PromptResultFormatter;
use Mcp\Schema\Content\PromptMessage;
use Mcp\Schema\Prompt;
use Mcp\Server\Handler\ElementHandlerInterface;

/**
* @phpstan-import-type Handler from ElementReference
Expand All @@ -28,7 +29,7 @@ class PromptReference extends ElementReference
*/
public function __construct(
public readonly Prompt $prompt,
\Closure|array|string $handler,
\Closure|array|string|ElementHandlerInterface $handler,
bool $isManual = false,
public readonly array $completionProviders = [],
) {
Expand Down
51 changes: 39 additions & 12 deletions src/Capability/Registry/ReferenceHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@

use Mcp\Exception\InvalidArgumentException;
use Mcp\Exception\RegistryException;
use Mcp\Server\ClientGateway;
use Mcp\Server\Handler\ElementHandlerInterface;
use Mcp\Server\Handler\PromptHandlerInterface;
use Mcp\Server\Handler\ResourceHandlerInterface;
use Mcp\Server\Handler\ResourceTemplateHandlerInterface;
use Mcp\Server\Handler\ToolHandlerInterface;
use Mcp\Server\RequestContext;
use Mcp\Server\Session\SessionInterface;
use Psr\Container\ContainerInterface;
Expand All @@ -33,33 +39,54 @@ public function __construct(
public function handle(ElementReference $reference, array $arguments): mixed
{
$session = $arguments['_session'];
$handler = $reference->handler;

if ($handler instanceof ElementHandlerInterface) {
$client = new ClientGateway($session);
$callArgs = array_diff_key($arguments, array_flip(['_session', '_request']));

return match (true) {
$handler instanceof ToolHandlerInterface => $handler->execute($callArgs, $client),
$handler instanceof PromptHandlerInterface => $handler->get($callArgs, $client),
$handler instanceof ResourceHandlerInterface => $handler->read(
$arguments['uri'] ?? throw new InvalidArgumentException('Resource dispatch requires a "uri" argument.'),
$client,
),
$handler instanceof ResourceTemplateHandlerInterface => $handler->read(
$arguments['uri'] ?? throw new InvalidArgumentException('Resource template dispatch requires a "uri" argument.'),
array_diff_key($callArgs, ['uri' => null]),
$client,
),
default => throw new InvalidArgumentException(\sprintf('Unsupported %s implementation: %s.', ElementHandlerInterface::class, $handler::class)),
};
}

if (\is_string($reference->handler)) {
if (class_exists($reference->handler) && method_exists($reference->handler, '__invoke')) {
$reflection = new \ReflectionMethod($reference->handler, '__invoke');
$instance = $this->getClassInstance($reference->handler);
if (\is_string($handler)) {
if (class_exists($handler) && method_exists($handler, '__invoke')) {
$reflection = new \ReflectionMethod($handler, '__invoke');
$instance = $this->getClassInstance($handler);
$arguments = $this->prepareArguments($reflection, $arguments);

return \call_user_func($instance, ...$arguments);
}

if (\function_exists($reference->handler)) {
$reflection = new \ReflectionFunction($reference->handler);
if (\function_exists($handler)) {
$reflection = new \ReflectionFunction($handler);
$arguments = $this->prepareArguments($reflection, $arguments);

return \call_user_func($reference->handler, ...$arguments);
return \call_user_func($handler, ...$arguments);
}
}

if (\is_callable($reference->handler)) {
$reflection = $this->getReflectionForCallable($reference->handler, $session);
if (\is_callable($handler)) {
$reflection = $this->getReflectionForCallable($handler, $session);
$arguments = $this->prepareArguments($reflection, $arguments);

return \call_user_func($reference->handler, ...$arguments);
return \call_user_func($handler, ...$arguments);
}

if (\is_array($reference->handler)) {
[$className, $methodName] = $reference->handler;
if (\is_array($handler)) {
[$className, $methodName] = $handler;
$reflection = new \ReflectionMethod($className, $methodName);
$instance = $this->getClassInstance($className);
Comment on lines -37 to 91
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks disruptive at first glance but it's just using the new variable for $reference->handler

$arguments = $this->prepareArguments($reflection, $arguments);
Expand Down
3 changes: 2 additions & 1 deletion src/Capability/Registry/ResourceReference.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Mcp\Capability\Formatter\ResourceResultFormatter;
use Mcp\Schema\Content\ResourceContents;
use Mcp\Schema\Resource;
use Mcp\Server\Handler\ElementHandlerInterface;

/**
* @phpstan-import-type Handler from ElementReference
Expand All @@ -27,7 +28,7 @@ class ResourceReference extends ElementReference
*/
public function __construct(
public readonly Resource $resource,
callable|array|string $handler,
callable|array|string|ElementHandlerInterface $handler,
bool $isManual = false,
) {
parent::__construct($handler, $isManual);
Expand Down
Loading