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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ 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.
* Add `ExplicitElementLoader` that wraps explicit-handler instances in closures before registration.
* [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
49 changes: 35 additions & 14 deletions docs/mcp-elements.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,21 @@ Each capability can be registered using two methods:
1. **Attribute-Based Discovery**: Use PHP attributes (`#[McpTool]`, `#[McpResource]`, etc.) on methods or classes. The
server automatically discovers and registers them.

2. **Manual Registration**: Explicitly register capabilities using `ServerBuilder` methods (`addTool()`, `addResource()`, etc.).
2. **Manual Registration**: Explicitly register capabilities using `ServerBuilder` methods (`addTool()`,
`addResource()`, etc.).

**Priority**: Manual registrations **always override** discovered elements with the same identifier:
- **Tools**: Same `name`
- **Resources**: Same `uri`
- **Resource Templates**: Same `uriTemplate`
- **Prompts**: Same `name`

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 (e.g. bridging configuration entities into
MCP elements), see [Explicit element registration](server-builder.md#explicit-element-registration) in the Server
Builder docs.

## Tools

Expand Down Expand Up @@ -71,14 +77,16 @@ class Calculator
### Parameters

- **`name`** (optional): Tool identifier. Defaults to method name if not provided.
- **`description`** (optional): Tool description. Defaults to docblock summary if not provided, otherwise uses method name.
- **`description`** (optional): Tool description. Defaults to docblock summary if not provided, otherwise uses method
name.
- **`annotations`** (optional): `ToolAnnotations` object for additional metadata.
- **`icons`** (optional): Array of `Icon` objects for visual representation.
- **`meta`** (optional): Arbitrary key-value pairs for custom metadata.

**Priority for name/description**: Attribute parameters → DocBlock content → Method name

For tool parameter validation and JSON schema generation, see [Schema Generation and Validation](#schema-generation-and-validation).
For tool parameter validation and JSON schema generation, see
[Schema Generation and Validation](#schema-generation-and-validation).

### Tool Return Values

Expand Down Expand Up @@ -159,7 +167,8 @@ public function getMultipleContent(): array

Tool handlers can throw any exception, but the type determines how it's handled:

- **`ToolCallException`**: Converted to JSON-RPC response with `CallToolResult` where `isError: true`, allowing the LLM to see the error message and self-correct
- **`ToolCallException`**: Converted to JSON-RPC response with `CallToolResult` where `isError: true`, allowing the LLM
to see the error message and self-correct
- **Any other exception**: Converted to JSON-RPC error response, but with a generic error message

```php
Expand All @@ -186,7 +195,8 @@ public function processFile(string $filename): string
}
```

**Recommendation**: Use `ToolCallException` when you want to communicate specific errors to clients. Any other exception will still be converted to JSON-RPC compliant errors but with generic error messages.
**Recommendation**: Use `ToolCallException` when you want to communicate specific errors to clients. Any other
exception will still be converted to JSON-RPC compliant errors but with generic error messages.


## Resources
Expand Down Expand Up @@ -215,7 +225,8 @@ class ConfigProvider

### Parameters

- **`uri`** (required): Unique resource identifier. Must comply with [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986).
- **`uri`** (required): Unique resource identifier. Must comply with
[RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986).
- **`name`** (optional): Human-readable name. Defaults to method name if not provided.
- **`description`** (optional): Resource description. Defaults to docblock summary if not provided.
- **`mimeType`** (optional): MIME type of the resource content.
Expand All @@ -229,7 +240,8 @@ class ConfigProvider

### Resource Return Values

Resource handlers can return various data types that are automatically formatted into appropriate MCP resource content types.
Resource handlers can return various data types that are automatically formatted into appropriate MCP resource content
types.

#### Supported Return Types

Expand Down Expand Up @@ -333,12 +345,14 @@ public function getFile(string $path): string
}
```

**Recommendation**: Use `ResourceReadException` when you want to communicate specific errors to clients. Any other exception will still be converted to JSON-RPC compliant errors but with generic error messages.
**Recommendation**: Use `ResourceReadException` when you want to communicate specific errors to clients. Any other
exception will still be converted to JSON-RPC compliant errors but with generic error messages.

## Resource Templates

Resource templates are **dynamic resources** that use parameterized URIs with variables. They follow all the same rules
as static resources (URI schemas, return values, MIME types, etc.) but accept variables using [RFC 6570 URI template syntax](https://datatracker.ietf.org/doc/html/rfc6570).
as static resources (URI schemas, return values, MIME types, etc.) but accept variables using
[RFC 6570 URI template syntax](https://datatracker.ietf.org/doc/html/rfc6570).

```php
use Mcp\Capability\Attribute\McpResourceTemplate;
Expand Down Expand Up @@ -395,7 +409,10 @@ class PromptGenerator
{
return [
['role' => 'system', 'content' => 'You are an expert code reviewer.'],
['role' => 'user', 'content' => "Review this {$language} code focusing on {$focus}:\n\n```{$language}\n{$code}\n```"]
[
'role' => 'user',
'content' => "Review this {$language} code focusing on {$focus}:\n\n```{$language}\n{$code}\n```",
],
];
}
}
Expand Down Expand Up @@ -470,7 +487,8 @@ public function explicitMessages(): array
}
```

The SDK automatically validates that all messages have valid roles and converts the result into the appropriate MCP prompt message format.
The SDK automatically validates that all messages have valid roles and converts the result into the appropriate MCP
prompt message format.

#### Valid Message Roles

Expand Down Expand Up @@ -503,7 +521,8 @@ public function generatePrompt(string $topic, string $style): array
}
```

**Recommendation**: Use `PromptGetException` when you want to communicate specific errors to clients. Any other exception will still be converted to JSON-RPC compliant errors but with generic error messages.
**Recommendation**: Use `PromptGetException` when you want to communicate specific errors to clients. Any other
exception will still be converted to JSON-RPC compliant errors but with generic error messages.

## Logging

Expand Down Expand Up @@ -534,7 +553,9 @@ public function processData(string $input, RequestContext $context): array {

## Completion Providers

Completion providers help MCP clients offer auto-completion suggestions for Resource Templates and Prompts. Unlike Tools and static Resources (which can be listed via `tools/list` and `resources/list`), Resource Templates and Prompts have dynamic parameters that benefit from completion hints.
Completion providers help MCP clients offer auto-completion suggestions for Resource Templates and Prompts. Unlike
Tools and static Resources (which can be listed via `tools/list` and `resources/list`), Resource Templates and Prompts
have dynamic parameters that benefit from completion hints.

### Completion Provider Types

Expand Down
57 changes: 57 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,62 @@ 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, the Drupal `mcp` module exposing
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's move the Drupal reference to README section "PHP Libraries Using the MCP SDK"

configuration or database entities as MCP elements), 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 +675,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
80 changes: 80 additions & 0 deletions src/Capability/Registry/Loader/ExplicitElementLoader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?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\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.
*
* @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}> $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) {
$handler = $entry['handler'];
$registry->registerTool(
$entry['definition'],
static fn (array $arguments, ClientGateway $client) => $handler->execute($arguments, $client),
);
}

foreach ($this->resources as $entry) {
$handler = $entry['handler'];
$registry->registerResource(
$entry['definition'],
static fn (string $uri, ClientGateway $client) => $handler->read($uri, $client),
);
}

foreach ($this->resourceTemplates as $entry) {
$handler = $entry['handler'];
$registry->registerResourceTemplate(
$entry['definition'],
static fn (string $uri, array $variables, ClientGateway $client) => $handler->read($uri, $variables, $client),
);
Comment on lines +66 to +69
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Third argument completionProviders basically lost, not sure how to tackle that - would accept that for now. Same for prompts below.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can't we just add the completionProvider? there'd be a feature gap between the two loaders if not?

Copy link
Copy Markdown
Member

@chr-hertel chr-hertel May 21, 2026

Choose a reason for hiding this comment

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

was my thought as well, but we only have add(Prompt, PromptHandler) at the moment.

i don't think it should be part of the Mcp\Schema\Prompt instance, so it could be an optional third argument?
add(Prompt, PromptHandler, ?CompletionProvider)

}

foreach ($this->prompts as $entry) {
$handler = $entry['handler'];
$registry->registerPrompt(
$entry['definition'],
static fn (array $arguments, ClientGateway $client) => $handler->get($arguments, $client),
);
}
}
}
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
Loading