[Server] Allow runtime-resolved element handlers (#294)

* [Server] feat: allow runtime handlers

Adds Mcp\Server\Handler\RunTimeHandlerInterface so integrators
(e.g. configuration-driven element sources) can register handlers
whose arguments and execution are resolved at runtime rather than
via PHP reflection of a callable.

ReferenceHandler::handle() dispatches instanceof RunTimeHandler
ahead of the callable/array/string branches, calls filterArguments
to narrow the argument map, and forwards execute() a ClientGateway
built from the active session.

The Registry-layer surface (ElementReference and the four
*Reference subclasses, RegistryInterface, Registry::register*,
ArrayLoader's handler-description helper) is widened to accept the
new type. The Builder/ArrayLoader path keeps its narrow callable
union; exposing runtime handlers there requires reworking
HandlerResolver and is left for a follow-up.

* feat(server): add runtime handler metadata API

Extend RunTimeHandlerInterface with four nullable metadata
accessors (getInputSchema, getOutputSchema, getPromptArguments,
getCompletionProviders) so the loader can obtain element
metadata without reflection.

Add RunTimeHandlerTrait providing null defaults for all four
accessors so single-purpose handlers can override only the
methods relevant to their element kind.

* feat(server): expose runtime handlers via Builder

Widen Builder::addTool, addResource, addResourceTemplate, and
addPrompt to accept RunTimeHandlerInterface instances alongside
the existing closure/array/string handler forms.

Branch ArrayLoader::load() on instanceof RunTimeHandlerInterface
in each of the four registration loops so runtime handlers skip
the reflection-based HandlerResolver/HandlerSchema path and pull
metadata from the interface accessors instead. Require explicit
name and description for runtime handlers and raise
ConfigurationException when missing or when a runtime tool has
no input schema source.

Replace direct $data['handler'] use in error log context with
getHandlerDescription() to avoid serializing large runtime
handler object graphs into log records.

Document the runtime handler API in docs/server-builder.md with a
new Runtime Handlers subsection and add a cross-reference from
docs/mcp-elements.md.

* test(server): cover runtime handler Builder path

Add ArrayLoaderRunTimeHandlerTest covering:
- successful tool registration with handler-supplied input schema,
  with kwarg input schema (precedence over the handler), and the
  same precedence rule for output schema
- ConfigurationException for tools missing name, description, or
  any input schema source
- successful resource registration via Builder kwargs and the
  matching missing-name failure mode
- successful resource template registration with completion
  providers sourced from the handler and the missing-description
  failure mode
- successful prompt registration with arguments and completion
  providers sourced from the handler, plus the missing-name
  failure mode
- a trait-only fixture verifying the four metadata accessors
  return null by default

Update existing ReferenceHandlerTest anonymous classes to use
RunTimeHandlerTrait so they satisfy the widened interface.

* refactor(server): split runtime handler interface

Splits RunTimeHandlerInterface into a minimal base contract (only
execute()) plus element-specific subtypes: RunTimeToolHandlerInterface,
RunTimePromptHandlerInterface, and
RunTimeResourceTemplateHandlerInterface. Each subtype declares only
the metadata accessors relevant to its element kind, so a tool
handler no longer has to no-op out prompt or template methods.

Drops the now-redundant RunTimeHandlerTrait and removes
filterArguments; execute() receives the full argument map directly.

Updates ArrayLoader, ReferenceHandler, and Builder type signatures
to use the per-element interfaces, and rewrites the Runtime
Handlers documentation accordingly.

* refactor(server): rename runtime handler interfaces to Runtime casing

Renames RunTimeHandlerInterface and its element-specific subtypes to
Runtime casing, adds a dedicated RuntimeResourceHandlerInterface so
addResource() rejects mismatched runtime handlers at the type level,
and strips _session and _request from arguments forwarded to runtime
handlers. Hardens ArrayLoader to re-throw ConfigurationException
without wrapping, and extracts inline test fixtures into
tests/Fixtures/Runtime/.

* chore: please the linting gods

* test(server): use Uuid::v4 for symfony/uid 5.4 compatibility

Uuid::v7() was added in symfony/uid 6.2 and breaks the Symfony 5.4
matrix job. Match the convention used elsewhere in the suite.

* refactor(server): split ArrayLoader registration

Extract per-element preparation into dedicated private prepare*
methods for tools, resources, templates, and prompts. Factor the
shared required-field assertion and reflected name/description
resolution into helpers.

* refactor(server): non-null runtime handler types

Drop nullable return types on RuntimeToolHandlerInterface::
getInputSchema(), RuntimePromptHandlerInterface::
getPromptArguments()/getCompletionProviders(), and
RuntimeResourceTemplateHandlerInterface::
getCompletionProviders() in favor of plain arrays.
ArrayLoader no longer needs the null-coalescing
fallbacks, and NullSchemaToolHandler plus its
ConfigurationException test become redundant.

* doc: update docs/server-builder.md

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>

* feat: peer revie feedback

Introduce a new `Builder::add(...)` pattern for a more generic runtime
handler.

* refactor(server): apply PR 294 review feedback

Move per-interface handler dispatch from ReferenceHandler into
ExplicitElementLoader by wrapping each ElementHandlerInterface in a
Closure that satisfies the existing callable contract, revert the
*Reference / Registry handler-type widening PR 294 introduced, throw
Mcp\Exception\InvalidArgumentException (not \TypeError) on mismatched
Builder::add() pairings, and rename two classes for clarity:

* Mcp\Capability\Registry\Loader\ArrayLoader -> ReflectedElementLoader
* Mcp\Schema\Resource -> Mcp\Schema\ResourceDefinition (no alias)

ReferenceHandler::prepareArguments() now injects ClientGateway by type
and binds an `array $arguments` / `array $variables` closure parameter
from the leftover inbound arguments. This addresses Antoine's coupling
concern (ReferenceHandler no longer knows about the four handler
interfaces) and Chris's package-exception preference, and clears the
phpdoc reserved-word collision Antoine flagged on the resource schema
class. Existing tests are re-targeted to exercise the closure path
through ExplicitElementLoader / ReferenceHandler.

* doc: address PR 294 review feedback

Apply Chris's line-level suggestions on docs/server-builder.md
(rewrap to 120 chars, smoothed Drupal mcp module name-drop in the
explicit-element registration section, exception name reads
Mcp\Exception\InvalidArgumentException), rewrap docs/mcp-elements.md
to 120 chars, and add 0.6.0 CHANGELOG entries covering the
Builder::add() entry point, the new handler interfaces, the new
ExplicitElementLoader, plus three BC breaks (Resource ->
ResourceDefinition rename, ArrayLoader -> ReflectedElementLoader
rename, mismatched-pairing exception swap).

* refactor: address self-review feedback

* CHANGELOG: drop spurious [BC Break] entry for the exception
  type Builder::add() raises (method is new in this PR).
* docs/server-builder: revert wrap-only edits to pre-existing
  main content; keep 120ch wrapping only for new sections.
* ExplicitElementLoader: trim verbose class docblock.
* ReferenceHandler: rename SPLAT_PARAMETER_NAMES constant to
  ARGUMENT_BAG_PARAMETERS.

* refactor: rename CallableHandler to Handler

* style: fix phpdoc param alignment

* refactor: inline $reference->handler access

* fix: maintainer review feedback

docs: mention ExplicitElementLoader in comment

docs: move Drupal reference to README list

docs: drop implementation-only changelog entry

docs: revert rewrap noise from mcp-elements.md

* style: fix phpdoc param alignment

---------

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
Co-authored-by: soyuka <soyuka@users.noreply.github.com>
This commit is contained in:
Mateu Aguiló Bosch 2026-05-29 07:16:12 +02:00 • committed by GitHub
commit 0347dc85b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 996 additions and 78 deletions

View file

@ -5,6 +5,10 @@ All notable changes to `mcp/sdk` will be documented in this file.
0.6.0 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` * [BC Break] Bump default protocol version to `2025-11-25`
* Allow overriding the default name pattern for Discovery * Allow overriding the default name pattern for Discovery
* Add configurable session garbage collection (`gcProbability`/`gcDivisor`) * Add configurable session garbage collection (`gcProbability`/`gcDivisor`)

View file

@ -290,6 +290,7 @@ $client->connect($transport);
- [api-platform/mcp](https://github.com/api-platform/mcp) — MCP integration for API Platform - [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 - [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 - [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 - [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 - [symfony/ai-mate](https://github.com/symfony/ai-mate) — AI development assistant MCP server for Symfony projects

View file

@ -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 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
Tools are callable functions that perform actions and return results. Tools are callable functions that perform actions and return results.

View file

@ -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). 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 ## Service Dependencies
### Container ### Container
@ -619,4 +674,5 @@ $server = Server::builder()
| `addResource()` | handler, uri, name?, description?, mimeType?, size?, annotations? | Register resource | | `addResource()` | handler, uri, name?, description?, mimeType?, size?, annotations? | Register resource |
| `addResourceTemplate()` | handler, uriTemplate, name?, description?, mimeType?, annotations? | Register resource template | | `addResourceTemplate()` | handler, uriTemplate, name?, description?, mimeType?, annotations? | Register resource template |
| `addPrompt()` | handler, name?, description? | Register prompt | | `addPrompt()` | handler, name?, description? | Register prompt |
| `add()` | definition, handler | Register an element from a schema VO + handler pair |
| `build()` | - | Create the server instance | | `build()` | - | Create the server instance |

View file

@ -27,7 +27,7 @@ use Mcp\Exception\ExceptionInterface;
use Mcp\Exception\RuntimeException; use Mcp\Exception\RuntimeException;
use Mcp\Schema\Prompt; use Mcp\Schema\Prompt;
use Mcp\Schema\PromptArgument; use Mcp\Schema\PromptArgument;
use Mcp\Schema\Resource; use Mcp\Schema\ResourceDefinition;
use Mcp\Schema\ResourceTemplate; use Mcp\Schema\ResourceTemplate;
use Mcp\Schema\Tool; use Mcp\Schema\Tool;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@ -249,7 +249,7 @@ final class Discoverer implements DiscovererInterface
$docBlock = $this->docBlockParser->parseDocBlock($method->getDocComment() ?? null); $docBlock = $this->docBlockParser->parseDocBlock($method->getDocComment() ?? null);
$name = $instance->name ?? ('__invoke' === $methodName ? $classShortName : $methodName); $name = $instance->name ?? ('__invoke' === $methodName ? $classShortName : $methodName);
$description = $instance->description ?? $this->docBlockParser->getDescription($docBlock) ?? null; $description = $instance->description ?? $this->docBlockParser->getDescription($docBlock) ?? null;
$resource = new Resource( $resource = new ResourceDefinition(
$instance->uri, $instance->uri,
$name, $name,
$instance->title, $instance->title,

View file

@ -26,7 +26,7 @@ use Mcp\Exception\ResourceNotFoundException;
use Mcp\Exception\ToolNotFoundException; use Mcp\Exception\ToolNotFoundException;
use Mcp\Schema\Page; use Mcp\Schema\Page;
use Mcp\Schema\Prompt; use Mcp\Schema\Prompt;
use Mcp\Schema\Resource; use Mcp\Schema\ResourceDefinition;
use Mcp\Schema\ResourceTemplate; use Mcp\Schema\ResourceTemplate;
use Mcp\Schema\Tool; use Mcp\Schema\Tool;
use Psr\EventDispatcher\EventDispatcherInterface; use Psr\EventDispatcher\EventDispatcherInterface;
@ -83,7 +83,7 @@ final class Registry implements RegistryInterface
return $reference; 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); $reference = new ResourceReference($resource, $handler);
$this->resources[$resource->uri] = $reference; $this->resources[$resource->uri] = $reference;

View file

@ -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
{
/**
* @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);
}
}

View file

@ -0,0 +1,337 @@
<?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\Attribute\CompletionProvider;
use Mcp\Capability\Completion\EnumCompletionProvider;
use Mcp\Capability\Completion\ListCompletionProvider;
use Mcp\Capability\Completion\ProviderInterface;
use Mcp\Capability\Discovery\DocBlockParser;
use Mcp\Capability\Discovery\HandlerResolver;
use Mcp\Capability\Discovery\SchemaGenerator;
use Mcp\Capability\Discovery\SchemaGeneratorInterface;
use Mcp\Capability\Registry\ElementReference;
use Mcp\Capability\RegistryInterface;
use Mcp\Exception\ConfigurationException;
use Mcp\Schema\Annotations;
use Mcp\Schema\Icon;
use Mcp\Schema\Prompt;
use Mcp\Schema\PromptArgument;
use Mcp\Schema\ResourceDefinition;
use Mcp\Schema\ResourceTemplate;
use Mcp\Schema\Tool;
use Mcp\Schema\ToolAnnotations;
use Mcp\Server\Handler;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
/**
* @author Antoine Bluchet <soyuka@gmail.com>
*
* @phpstan-import-type Handler from ElementReference
*/
final class ReflectedElementLoader implements LoaderInterface
{
/**
* @param array{
* handler: Handler,
* name: ?string,
* title: ?string,
* description: ?string,
* annotations: ?ToolAnnotations,
* icons: ?Icon[],
* meta: ?array<string, mixed>,
* outputSchema: ?array<string, mixed>
* }[] $tools
* @param array{
* handler: Handler,
* uri: string,
* name: ?string,
* title: ?string,
* description: ?string,
* mimeType: ?string,
* size: int|null,
* annotations: ?Annotations,
* icons: ?Icon[],
* meta: ?array<string, mixed>
* }[] $resources
* @param array{
* handler: Handler,
* uriTemplate: string,
* name: ?string,
* title: ?string,
* description: ?string,
* mimeType: ?string,
* annotations: ?Annotations,
* meta: ?array<string, mixed>
* }[] $resourceTemplates
* @param array{
* handler: Handler,
* name: ?string,
* description: ?string,
* icons: ?Icon[],
* meta: ?array<string, mixed>
* }[] $prompts
*/
public function __construct(
private readonly array $tools = [],
private readonly array $resources = [],
private readonly array $resourceTemplates = [],
private readonly array $prompts = [],
private LoggerInterface $logger = new NullLogger(),
private ?SchemaGeneratorInterface $schemaGenerator = null,
) {
}
public function load(RegistryInterface $registry): void
{
$docBlockParser = new DocBlockParser(logger: $this->logger);
$schemaGenerator = $this->schemaGenerator ?? new SchemaGenerator($docBlockParser);
// Register Tools
foreach ($this->tools as $data) {
try {
$reflection = HandlerResolver::resolve($data['handler']);
if ($reflection instanceof \ReflectionFunction) {
$name = $data['name'] ?? 'closure_tool_'.spl_object_id($data['handler']);
$description = $data['description'] ?? null;
} else {
$classShortName = $reflection->getDeclaringClass()->getShortName();
$methodName = $reflection->getName();
$docBlock = $docBlockParser->parseDocBlock($reflection->getDocComment() ?? null);
$name = $data['name'] ?? ('__invoke' === $methodName ? $classShortName : $methodName);
$description = $data['description'] ?? $docBlockParser->getDescription($docBlock) ?? null;
}
$inputSchema = $data['inputSchema'] ?? $schemaGenerator->generate($reflection);
$tool = new Tool(
name: $name,
title: $data['title'] ?? null,
inputSchema: $inputSchema,
description: $description,
annotations: $data['annotations'] ?? null,
icons: $data['icons'] ?? null,
meta: $data['meta'] ?? null,
outputSchema: $data['outputSchema'] ?? null,
);
$registry->registerTool($tool, $data['handler']);
$handlerDesc = $this->getHandlerDescription($data['handler']);
$this->logger->debug("Registered manual tool {$name} from handler {$handlerDesc}");
} catch (\Throwable $e) {
$this->logger->error(
'Failed to register manual tool',
['handler' => $data['handler'], 'name' => $data['name'], 'exception' => $e],
);
throw new ConfigurationException("Error registering manual tool '{$data['name']}': {$e->getMessage()}", 0, $e);
}
}
// Register Resources
foreach ($this->resources as $data) {
try {
$reflection = HandlerResolver::resolve($data['handler']);
if ($reflection instanceof \ReflectionFunction) {
$name = $data['name'] ?? 'closure_resource_'.spl_object_id($data['handler']);
$description = $data['description'] ?? null;
} else {
$classShortName = $reflection->getDeclaringClass()->getShortName();
$methodName = $reflection->getName();
$docBlock = $docBlockParser->parseDocBlock($reflection->getDocComment() ?? null);
$name = $data['name'] ?? ('__invoke' === $methodName ? $classShortName : $methodName);
$description = $data['description'] ?? $docBlockParser->getDescription($docBlock) ?? null;
}
$resource = new ResourceDefinition(
uri: $data['uri'],
name: $name,
title: $data['title'] ?? null,
description: $description,
mimeType: $data['mimeType'] ?? null,
annotations: $data['annotations'] ?? null,
size: $data['size'] ?? null,
icons: $data['icons'] ?? null,
meta: $data['meta'] ?? null,
);
$registry->registerResource($resource, $data['handler']);
$handlerDesc = $this->getHandlerDescription($data['handler']);
$this->logger->debug("Registered manual resource {$name} from handler {$handlerDesc}");
} catch (\Throwable $e) {
$this->logger->error(
'Failed to register manual resource',
['handler' => $data['handler'], 'uri' => $data['uri'], 'exception' => $e],
);
throw new ConfigurationException("Error registering manual resource '{$data['uri']}': {$e->getMessage()}", 0, $e);
}
}
// Register Templates
foreach ($this->resourceTemplates as $data) {
try {
$reflection = HandlerResolver::resolve($data['handler']);
if ($reflection instanceof \ReflectionFunction) {
$name = $data['name'] ?? 'closure_template_'.spl_object_id($data['handler']);
$description = $data['description'] ?? null;
} else {
$classShortName = $reflection->getDeclaringClass()->getShortName();
$methodName = $reflection->getName();
$docBlock = $docBlockParser->parseDocBlock($reflection->getDocComment() ?? null);
$name = $data['name'] ?? ('__invoke' === $methodName ? $classShortName : $methodName);
$description = $data['description'] ?? $docBlockParser->getDescription($docBlock) ?? null;
}
$template = new ResourceTemplate(
uriTemplate: $data['uriTemplate'],
name: $name,
title: $data['title'] ?? null,
description: $description,
mimeType: $data['mimeType'] ?? null,
annotations: $data['annotations'] ?? null,
meta: $data['meta'] ?? null,
);
$completionProviders = $this->getCompletionProviders($reflection);
$registry->registerResourceTemplate($template, $data['handler'], $completionProviders);
$handlerDesc = $this->getHandlerDescription($data['handler']);
$this->logger->debug("Registered manual template {$name} from handler {$handlerDesc}");
} catch (\Throwable $e) {
$this->logger->error(
'Failed to register manual template',
['handler' => $data['handler'], 'uriTemplate' => $data['uriTemplate'], 'exception' => $e],
);
throw new ConfigurationException("Error registering manual resource template '{$data['uriTemplate']}': {$e->getMessage()}", 0, $e);
}
}
// Register Prompts
foreach ($this->prompts as $data) {
try {
$reflection = HandlerResolver::resolve($data['handler']);
if ($reflection instanceof \ReflectionFunction) {
$name = $data['name'] ?? 'closure_prompt_'.spl_object_id($data['handler']);
$description = $data['description'] ?? null;
} else {
$classShortName = $reflection->getDeclaringClass()->getShortName();
$methodName = $reflection->getName();
$docBlock = $docBlockParser->parseDocBlock($reflection->getDocComment() ?? null);
$name = $data['name'] ?? ('__invoke' === $methodName ? $classShortName : $methodName);
$description = $data['description'] ?? $docBlockParser->getDescription($docBlock) ?? null;
}
$arguments = [];
$paramTags = $reflection instanceof \ReflectionMethod ? $docBlockParser->getParamTags(
$docBlockParser->parseDocBlock($reflection->getDocComment() ?? null),
) : [];
foreach ($reflection->getParameters() as $param) {
$reflectionType = $param->getType();
// Basic DI check (heuristic)
if ($reflectionType instanceof \ReflectionNamedType && !$reflectionType->isBuiltin()) {
continue;
}
$paramTag = $paramTags['$'.$param->getName()] ?? null;
$arguments[] = new PromptArgument(
$param->getName(),
$paramTag ? trim((string) $paramTag->getDescription()) : null,
!$param->isOptional() && !$param->isDefaultValueAvailable(),
);
}
$prompt = new Prompt(
name: $name,
title: $data['title'] ?? null,
description: $description,
arguments: $arguments,
icons: $data['icons'] ?? null,
meta: $data['meta'] ?? null
);
$completionProviders = $this->getCompletionProviders($reflection);
$registry->registerPrompt($prompt, $data['handler'], $completionProviders);
$handlerDesc = $this->getHandlerDescription($data['handler']);
$this->logger->debug("Registered manual prompt {$name} from handler {$handlerDesc}");
} catch (\Throwable $e) {
$this->logger->error(
'Failed to register manual prompt',
['handler' => $data['handler'], 'name' => $data['name'], 'exception' => $e],
);
throw new ConfigurationException("Error registering manual prompt '{$data['name']}': {$e->getMessage()}", 0, $e);
}
}
$this->logger->debug('Manual element registration complete.');
}
/**
* @param Handler $handler
*/
private function getHandlerDescription(\Closure|array|string $handler): string
{
if ($handler instanceof \Closure) {
return 'Closure';
}
if (\is_array($handler)) {
return \sprintf(
'%s::%s',
\is_object($handler[0]) ? $handler[0]::class : $handler[0],
$handler[1],
);
}
return (string) $handler;
}
/**
* @return array<string, ProviderInterface>
*/
private function getCompletionProviders(\ReflectionMethod|\ReflectionFunction $reflection): array
{
$completionProviders = [];
foreach ($reflection->getParameters() as $param) {
$reflectionType = $param->getType();
if ($reflectionType instanceof \ReflectionNamedType && !$reflectionType->isBuiltin()) {
continue;
}
$completionAttributes = $param->getAttributes(
CompletionProvider::class,
\ReflectionAttribute::IS_INSTANCEOF,
);
if (!empty($completionAttributes)) {
$attributeInstance = $completionAttributes[0]->newInstance();
if ($attributeInstance->provider) {
$completionProviders[$param->getName()] = $attributeInstance->provider;
} elseif ($attributeInstance->providerClass) {
$completionProviders[$param->getName()] = $attributeInstance->providerClass;
} elseif ($attributeInstance->values) {
$completionProviders[$param->getName()] = new ListCompletionProvider($attributeInstance->values);
} elseif ($attributeInstance->enum) {
$completionProviders[$param->getName()] = new EnumCompletionProvider($attributeInstance->enum);
}
}
}
return $completionProviders;
}
}

View file

@ -13,6 +13,7 @@ namespace Mcp\Capability\Registry;
use Mcp\Exception\InvalidArgumentException; use Mcp\Exception\InvalidArgumentException;
use Mcp\Exception\RegistryException; use Mcp\Exception\RegistryException;
use Mcp\Server\ClientGateway;
use Mcp\Server\RequestContext; use Mcp\Server\RequestContext;
use Mcp\Server\Session\SessionInterface; use Mcp\Server\Session\SessionInterface;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
@ -32,6 +33,15 @@ final class ReferenceHandler implements ReferenceHandlerInterface
*/ */
public function handle(ElementReference $reference, array $arguments): mixed 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']; $session = $arguments['_session'];
if (\is_string($reference->handler)) { if (\is_string($reference->handler)) {
@ -102,6 +112,11 @@ final class ReferenceHandler implements ReferenceHandlerInterface
$finalArgs[$paramPosition] = new RequestContext($arguments['_session'], $arguments['_request']); $finalArgs[$paramPosition] = new RequestContext($arguments['_session'], $arguments['_request']);
continue; continue;
} }
if (ClientGateway::class === $typeName && isset($arguments['_session'])) {
$finalArgs[$paramPosition] = new ClientGateway($arguments['_session']);
continue;
}
} }
if (isset($arguments[$paramName])) { if (isset($arguments[$paramName])) {

View file

@ -13,7 +13,7 @@ namespace Mcp\Capability\Registry;
use Mcp\Capability\Formatter\ResourceResultFormatter; use Mcp\Capability\Formatter\ResourceResultFormatter;
use Mcp\Schema\Content\ResourceContents; use Mcp\Schema\Content\ResourceContents;
use Mcp\Schema\Resource; use Mcp\Schema\ResourceDefinition;
/** /**
* @phpstan-import-type Handler from ElementReference * @phpstan-import-type Handler from ElementReference
@ -26,7 +26,7 @@ class ResourceReference extends ElementReference
* @param Handler $handler * @param Handler $handler
*/ */
public function __construct( public function __construct(
public readonly Resource $resource, public readonly ResourceDefinition $resource,
callable|array|string $handler, callable|array|string $handler,
) { ) {
parent::__construct($handler); parent::__construct($handler);

View file

@ -21,7 +21,7 @@ use Mcp\Exception\ResourceNotFoundException;
use Mcp\Exception\ToolNotFoundException; use Mcp\Exception\ToolNotFoundException;
use Mcp\Schema\Page; use Mcp\Schema\Page;
use Mcp\Schema\Prompt; use Mcp\Schema\Prompt;
use Mcp\Schema\Resource; use Mcp\Schema\ResourceDefinition;
use Mcp\Schema\ResourceTemplate; use Mcp\Schema\ResourceTemplate;
use Mcp\Schema\Tool; use Mcp\Schema\Tool;
@ -47,7 +47,7 @@ interface RegistryInterface
* *
* @param Handler $handler * @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. * Registers a resource template with its handler and completion providers.

View file

@ -12,14 +12,14 @@
namespace Mcp\Schema; namespace Mcp\Schema;
/** /**
* @phpstan-type PageItem Tool|Prompt|ResourceTemplate|Resource * @phpstan-type PageItem Tool|Prompt|ResourceTemplate|ResourceDefinition
* *
* @extends \ArrayObject<int|string, PageItem> * @extends \ArrayObject<int|string, PageItem>
*/ */
final class Page extends \ArrayObject 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 function __construct(
public readonly array $references, public readonly array $references,

View file

@ -0,0 +1,151 @@
<?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\Schema;
use Mcp\Exception\InvalidArgumentException;
/**
* A known resource that the server is capable of reading.
*
* @phpstan-import-type AnnotationsData from Annotations
* @phpstan-import-type IconData from Icon
*
* @phpstan-type ResourceDefinitionData array{
* uri: string,
* name: string,
* title?: string,
* description?: string,
* mimeType?: string,
* annotations?: AnnotationsData,
* size?: int,
* icons?: IconData[],
* _meta?: array<string, mixed>,
* }
*
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
*/
class ResourceDefinition implements \JsonSerializable
{
/**
* Resource name pattern regex - must contain only alphanumeric characters, underscores, and hyphens.
*/
private const RESOURCE_NAME_PATTERN = '/^[a-zA-Z0-9_-]+$/';
/**
* URI pattern regex - requires a valid scheme followed by colon and optional path (RFC 3986).
* Example patterns: file://path, db://table, urn:isbn:123, config:key, etc.
*/
private const URI_PATTERN = '/^[a-zA-Z][a-zA-Z0-9+.-]*:[^\s]*$/';
/**
* @param string $uri the URI of this resource
* @param string $name a short identifier for this resource
* @param ?string $title optional human-readable title for display in UI
* @param ?string $description A description of what this resource represents. This can be used by clients to improve the LLM's understanding of available resources.
* @param ?string $mimeType the MIME type of this resource, if known
* @param ?Annotations $annotations optional annotations for the client
* @param ?int $size the size of the raw resource content, in bytes (before base64 encoding or any tokenization), if known
* @param ?Icon[] $icons optional icons representing the resource
* @param ?array<string, mixed> $meta optional metadata
*/
public function __construct(
public readonly string $uri,
public readonly string $name,
public readonly ?string $title = null,
public readonly ?string $description = null,
public readonly ?string $mimeType = null,
public readonly ?Annotations $annotations = null,
public readonly ?int $size = null,
public readonly ?array $icons = null,
public readonly ?array $meta = null,
) {
if (!preg_match(self::RESOURCE_NAME_PATTERN, $name)) {
throw new InvalidArgumentException(\sprintf('Invalid resource name "%s": must contain only alphanumeric characters, underscores, and hyphens.', $name));
}
if (!preg_match(self::URI_PATTERN, $uri)) {
throw new InvalidArgumentException(\sprintf('Invalid resource URI: "%s" must be a valid URI with a scheme and optional path.', $uri));
}
}
/**
* @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 ResourceDefinition data.');
}
if (empty($data['name']) || !\is_string($data['name'])) {
throw new InvalidArgumentException('Invalid or missing "name" in ResourceDefinition data.');
}
if (!empty($data['_meta']) && !\is_array($data['_meta'])) {
throw new InvalidArgumentException('Invalid "_meta" in ResourceDefinition data.');
}
return new self(
uri: $data['uri'],
name: $data['name'],
title: isset($data['title']) && \is_string($data['title']) ? $data['title'] : null,
description: $data['description'] ?? null,
mimeType: $data['mimeType'] ?? null,
annotations: isset($data['annotations']) ? Annotations::fromArray($data['annotations']) : null,
size: isset($data['size']) ? (int) $data['size'] : null,
icons: isset($data['icons']) && \is_array($data['icons']) ? array_map(Icon::fromArray(...), $data['icons']) : null,
meta: isset($data['_meta']) ? $data['_meta'] : null
);
}
/**
* @return array{
* uri: string,
* name: string,
* title?: string,
* description?: string,
* mimeType?: string,
* annotations?: Annotations,
* size?: int,
* icons?: Icon[],
* _meta?: array<string, mixed>
* }
*/
public function jsonSerialize(): array
{
$data = [
'uri' => $this->uri,
'name' => $this->name,
];
if (null !== $this->title) {
$data['title'] = $this->title;
}
if (null !== $this->description) {
$data['description'] = $this->description;
}
if (null !== $this->mimeType) {
$data['mimeType'] = $this->mimeType;
}
if (null !== $this->annotations) {
$data['annotations'] = $this->annotations;
}
if (null !== $this->size) {
$data['size'] = $this->size;
}
if (null !== $this->icons) {
$data['icons'] = $this->icons;
}
if (null !== $this->meta) {
$data['_meta'] = $this->meta;
}
return $data;
}
}

View file

@ -14,20 +14,20 @@ namespace Mcp\Schema\Result;
use Mcp\Exception\InvalidArgumentException; use Mcp\Exception\InvalidArgumentException;
use Mcp\Schema\JsonRpc\Response; use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\JsonRpc\ResultInterface; use Mcp\Schema\JsonRpc\ResultInterface;
use Mcp\Schema\Resource as ResourceSchema; use Mcp\Schema\ResourceDefinition;
/** /**
* The server's response to a resources/list request from the client. * The server's response to a resources/list request from the client.
* *
* @phpstan-import-type ResourceData from ResourceSchema * @phpstan-import-type ResourceDefinitionData from ResourceDefinition
* *
* @author Kyrian Obikwelu <koshnawaza@gmail.com> * @author Kyrian Obikwelu <koshnawaza@gmail.com>
*/ */
class ListResourcesResult implements ResultInterface class ListResourcesResult implements ResultInterface
{ {
/** /**
* @param array<ResourceSchema> $resources the list of resource definitions * @param array<ResourceDefinition> $resources the list of resource definitions
* @param string|null $nextCursor An opaque token representing the pagination position after the last returned result. * @param string|null $nextCursor An opaque token representing the pagination position after the last returned result.
* *
* If present, there may be more results available. * If present, there may be more results available.
*/ */
@ -39,7 +39,7 @@ class ListResourcesResult implements ResultInterface
/** /**
* @param array{ * @param array{
* resources: array<ResourceData>, * resources: array<ResourceDefinitionData>,
* nextCursor?: string, * nextCursor?: string,
* } $data * } $data
*/ */
@ -50,14 +50,14 @@ class ListResourcesResult implements ResultInterface
} }
return new self( return new self(
array_map(static fn (array $resource) => ResourceSchema::fromArray($resource), $data['resources']), array_map(static fn (array $resource) => ResourceDefinition::fromArray($resource), $data['resources']),
$data['nextCursor'] ?? null $data['nextCursor'] ?? null
); );
} }
/** /**
* @return array{ * @return array{
* resources: array<ResourceSchema>, * resources: array<ResourceDefinition>,
* nextCursor?: string, * nextCursor?: string,
* } * }
*/ */

View file

@ -11,6 +11,7 @@
namespace Mcp\Server; namespace Mcp\Server;
use Mcp\Capability\Completion\ProviderInterface;
use Mcp\Capability\Discovery\CachedDiscoverer; use Mcp\Capability\Discovery\CachedDiscoverer;
use Mcp\Capability\Discovery\Discoverer; use Mcp\Capability\Discovery\Discoverer;
use Mcp\Capability\Discovery\DiscovererInterface; use Mcp\Capability\Discovery\DiscovererInterface;
@ -18,10 +19,11 @@ use Mcp\Capability\Discovery\SchemaGeneratorInterface;
use Mcp\Capability\Registry; use Mcp\Capability\Registry;
use Mcp\Capability\Registry\Container; use Mcp\Capability\Registry\Container;
use Mcp\Capability\Registry\ElementReference; use Mcp\Capability\Registry\ElementReference;
use Mcp\Capability\Registry\Loader\ArrayLoader;
use Mcp\Capability\Registry\Loader\ChainLoader; use Mcp\Capability\Registry\Loader\ChainLoader;
use Mcp\Capability\Registry\Loader\DiscoveryLoader; use Mcp\Capability\Registry\Loader\DiscoveryLoader;
use Mcp\Capability\Registry\Loader\ExplicitElementLoader;
use Mcp\Capability\Registry\Loader\LoaderInterface; use Mcp\Capability\Registry\Loader\LoaderInterface;
use Mcp\Capability\Registry\Loader\ReflectedElementLoader;
use Mcp\Capability\Registry\ReferenceHandler; use Mcp\Capability\Registry\ReferenceHandler;
use Mcp\Capability\Registry\ReferenceHandlerInterface; use Mcp\Capability\Registry\ReferenceHandlerInterface;
use Mcp\Capability\RegistryInterface; use Mcp\Capability\RegistryInterface;
@ -31,11 +33,20 @@ use Mcp\Schema\Annotations;
use Mcp\Schema\Enum\ProtocolVersion; use Mcp\Schema\Enum\ProtocolVersion;
use Mcp\Schema\Icon; use Mcp\Schema\Icon;
use Mcp\Schema\Implementation; use Mcp\Schema\Implementation;
use Mcp\Schema\Prompt;
use Mcp\Schema\ResourceDefinition;
use Mcp\Schema\ResourceTemplate;
use Mcp\Schema\ServerCapabilities; use Mcp\Schema\ServerCapabilities;
use Mcp\Schema\Tool;
use Mcp\Schema\ToolAnnotations; use Mcp\Schema\ToolAnnotations;
use Mcp\Server; use Mcp\Server;
use Mcp\Server\Handler\ElementHandlerInterface;
use Mcp\Server\Handler\Notification\NotificationHandlerInterface; use Mcp\Server\Handler\Notification\NotificationHandlerInterface;
use Mcp\Server\Handler\PromptHandlerInterface;
use Mcp\Server\Handler\Request\RequestHandlerInterface; use Mcp\Server\Handler\Request\RequestHandlerInterface;
use Mcp\Server\Handler\ResourceHandlerInterface;
use Mcp\Server\Handler\ResourceTemplateHandlerInterface;
use Mcp\Server\Handler\ToolHandlerInterface;
use Mcp\Server\Resource\SessionSubscriptionManager; use Mcp\Server\Resource\SessionSubscriptionManager;
use Mcp\Server\Resource\SubscriptionManagerInterface; use Mcp\Server\Resource\SubscriptionManagerInterface;
use Mcp\Server\Session\InMemorySessionStore; use Mcp\Server\Session\InMemorySessionStore;
@ -107,6 +118,7 @@ final class Builder
* title: ?string, * title: ?string,
* description: ?string, * description: ?string,
* annotations: ?ToolAnnotations, * annotations: ?ToolAnnotations,
* inputSchema: ?array<string, mixed>,
* icons: ?Icon[], * icons: ?Icon[],
* meta: ?array<string, mixed>, * meta: ?array<string, mixed>,
* outputSchema: ?array<string, mixed>, * outputSchema: ?array<string, mixed>,
@ -148,6 +160,7 @@ final class Builder
* @var array{ * @var array{
* handler: Handler, * handler: Handler,
* name: ?string, * name: ?string,
* title: ?string,
* description: ?string, * description: ?string,
* icons: ?Icon[], * icons: ?Icon[],
* meta: ?array<string, mixed> * meta: ?array<string, mixed>
@ -155,6 +168,26 @@ final class Builder
*/ */
private array $prompts = []; private array $prompts = [];
/**
* @var list<array{definition: Tool, handler: ToolHandlerInterface}>
*/
private array $explicitTools = [];
/**
* @var list<array{definition: ResourceDefinition, handler: ResourceHandlerInterface}>
*/
private array $explicitResources = [];
/**
* @var list<array{definition: ResourceTemplate, handler: ResourceTemplateHandlerInterface, completionProviders: array<string, ProviderInterface>}>
*/
private array $explicitResourceTemplates = [];
/**
* @var list<array{definition: Prompt, handler: PromptHandlerInterface, completionProviders: array<string, ProviderInterface>}>
*/
private array $explicitPrompts = [];
private ?string $discoveryBasePath = null; private ?string $discoveryBasePath = null;
/** /**
@ -518,6 +551,41 @@ final class Builder
return $this; return $this;
} }
/**
* Registers an element using an explicit schema value object paired with a handler interface.
*
* Use this entry point when an element's name, schema, or description is only known at
* runtime (e.g. config-driven integrations). For statically-known elements, prefer
* `addTool/addResource/addResourceTemplate/addPrompt`, which can derive metadata from
* reflection of the handler.
*
* Mismatched pairings (e.g. a `Tool` with a `PromptHandlerInterface`) raise
* `Mcp\Exception\InvalidArgumentException`. Completion providers are only supported on
* `Prompt` and `ResourceTemplate` definitions; supplying them with `Tool` or
* `ResourceDefinition` raises the same exception.
*
* @param array<string, ProviderInterface> $completionProviders Keyed by argument/variable name
*/
public function add(
Tool|ResourceDefinition|ResourceTemplate|Prompt $definition,
ElementHandlerInterface $handler,
array $completionProviders = [],
): self {
if ([] !== $completionProviders && ($definition instanceof Tool || $definition instanceof ResourceDefinition)) {
throw new InvalidArgumentException(\sprintf('Completion providers are only supported on Prompt and ResourceTemplate definitions, got %s.', $definition::class));
}
match (true) {
$definition instanceof Tool && $handler instanceof ToolHandlerInterface => $this->explicitTools[] = ['definition' => $definition, 'handler' => $handler],
$definition instanceof ResourceDefinition && $handler instanceof ResourceHandlerInterface => $this->explicitResources[] = ['definition' => $definition, 'handler' => $handler],
$definition instanceof ResourceTemplate && $handler instanceof ResourceTemplateHandlerInterface => $this->explicitResourceTemplates[] = ['definition' => $definition, 'handler' => $handler, 'completionProviders' => $completionProviders],
$definition instanceof Prompt && $handler instanceof PromptHandlerInterface => $this->explicitPrompts[] = ['definition' => $definition, 'handler' => $handler, 'completionProviders' => $completionProviders],
default => throw new InvalidArgumentException(\sprintf('%s definition cannot be paired with %s; expected the matching handler interface.', $definition::class, $handler::class)),
};
return $this;
}
/** /**
* Register a single custom loader. * Register a single custom loader.
*/ */
@ -556,11 +624,17 @@ final class Builder
$this->gcDivisor, $this->gcDivisor,
); );
// ArrayLoader runs before DiscoveryLoader so manual entries are seen first; DiscoveryLoader's // ExplicitElementLoader and ReflectedElementLoader run before DiscoveryLoader so manual entries are seen first;
// identity check then preserves them against same-name discovered entries. // DiscoveryLoader's identity check then preserves them against same-name discovered entries.
$loaders = [ $loaders = [
...$this->loaders, ...$this->loaders,
new ArrayLoader($this->tools, $this->resources, $this->resourceTemplates, $this->prompts, $logger, $this->schemaGenerator), new ExplicitElementLoader(
$this->explicitTools,
$this->explicitResources,
$this->explicitResourceTemplates,
$this->explicitPrompts,
),
new ReflectedElementLoader($this->tools, $this->resources, $this->resourceTemplates, $this->prompts, $logger, $this->schemaGenerator),
]; ];
if (null !== $this->discoveryBasePath) { if (null !== $this->discoveryBasePath) {

View file

@ -0,0 +1,23 @@
<?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\Server\Handler;
/**
* Marker contract shared by all explicit element handler interfaces (tool,
* resource, resource template, prompt). Used as a single type hint where any
* element handler is acceptable.
*
* @author Mateu Aguiló Bosch <mateu.aguilo.bosch@gmail.com>
*/
interface ElementHandlerInterface
{
}

View file

@ -0,0 +1,28 @@
<?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\Server\Handler;
use Mcp\Server\ClientGateway;
/**
* Contract for explicit prompt handlers paired with an `Mcp\Schema\Prompt` definition
* via `Mcp\Server\Builder::add()`.
*
* @author Mateu Aguiló Bosch <mateu.aguilo.bosch@gmail.com>
*/
interface PromptHandlerInterface extends ElementHandlerInterface
{
/**
* @param array<string, mixed> $arguments
*/
public function get(array $arguments, ClientGateway $gateway): mixed;
}

View file

@ -0,0 +1,25 @@
<?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\Server\Handler;
use Mcp\Server\ClientGateway;
/**
* Contract for explicit resource handlers paired with an `Mcp\Schema\ResourceDefinition`
* definition via `Mcp\Server\Builder::add()`.
*
* @author Mateu Aguiló Bosch <mateu.aguilo.bosch@gmail.com>
*/
interface ResourceHandlerInterface extends ElementHandlerInterface
{
public function read(string $uri, ClientGateway $gateway): mixed;
}

View file

@ -0,0 +1,28 @@
<?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\Server\Handler;
use Mcp\Server\ClientGateway;
/**
* Contract for explicit resource-template handlers paired with an
* `Mcp\Schema\ResourceTemplate` definition via `Mcp\Server\Builder::add()`.
*
* @author Mateu Aguiló Bosch <mateu.aguilo.bosch@gmail.com>
*/
interface ResourceTemplateHandlerInterface extends ElementHandlerInterface
{
/**
* @param array<string, string> $variables
*/
public function read(string $uri, array $variables, ClientGateway $gateway): mixed;
}

View file

@ -0,0 +1,28 @@
<?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\Server\Handler;
use Mcp\Server\ClientGateway;
/**
* Contract for explicit tool handlers paired with an `Mcp\Schema\Tool` definition
* via `Mcp\Server\Builder::add()`.
*
* @author Mateu Aguiló Bosch <mateu.aguilo.bosch@gmail.com>
*/
interface ToolHandlerInterface extends ElementHandlerInterface
{
/**
* @param array<string, mixed> $arguments
*/
public function execute(array $arguments, ClientGateway $gateway): mixed;
}

Some files were not shown because too many files have changed in this diff Show more