* [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>
678 lines
23 KiB
Markdown
678 lines
23 KiB
Markdown
# Server Builder
|
|
|
|
The server `Builder` is a fluent builder class that simplifies the creation and configuration of an MCP server instance.
|
|
It provides methods for setting server information, configuring discovery, registering capabilities, and customizing
|
|
various aspects of the server behavior.
|
|
|
|
## Table of Contents
|
|
|
|
- [Basic Usage](#basic-usage)
|
|
- [Server Configuration](#server-configuration)
|
|
- [Discovery Configuration](#discovery-configuration)
|
|
- [Session Management](#session-management)
|
|
- [Manual Capability Registration](#manual-capability-registration)
|
|
- [Service Dependencies](#service-dependencies)
|
|
- [Custom Message Handlers](#custom-message-handlers)
|
|
- [Complete Example](#complete-example)
|
|
- [Method Reference](#method-reference)
|
|
|
|
## Basic Usage
|
|
|
|
There are two ways to obtain a server builder instance:
|
|
|
|
### Method 1: Static Builder Method (Recommended)
|
|
|
|
```php
|
|
use Mcp\Server;
|
|
|
|
$server = Server::builder()
|
|
->setServerInfo('My MCP Server', '1.0.0')
|
|
->setDiscovery(__DIR__, ['.'])
|
|
->build();
|
|
```
|
|
|
|
### Method 2: Direct Instantiation
|
|
|
|
```php
|
|
use Mcp\Server\Builder;
|
|
|
|
$server = (new Builder())
|
|
->setServerInfo('My MCP Server', '1.0.0')
|
|
->setDiscovery(__DIR__, ['.'])
|
|
->build();
|
|
```
|
|
|
|
Both methods return a `Builder` instance that you can configure with fluent methods. The `build()` method returns the
|
|
final `Server` instance ready for use.
|
|
|
|
## Server Configuration
|
|
|
|
### Server Information
|
|
|
|
Set the server's identity with name, version, and optional description:
|
|
|
|
```php
|
|
use Mcp\Schema\Icon;
|
|
use Mcp\Server;
|
|
|
|
$server = Server::builder()
|
|
->setServerInfo(
|
|
name: 'Calculator Server',
|
|
version: '1.2.0',
|
|
description: 'Advanced mathematical calculations',
|
|
icons: [new Icon('https://example.com/icon.png', 'image/png', ['64x64'])],
|
|
websiteUrl: 'https://example.com'
|
|
');
|
|
```
|
|
|
|
**Parameters:**
|
|
- `$name` (string): The server name
|
|
- `$version` (string): Version string (semantic versioning recommended)
|
|
- `$description` (string|null): Optional description
|
|
- `$icons` (Icon[]|null): Optional array of server icons
|
|
- `$websiteUrl` (string|null): Optional server website URL
|
|
|
|
### Pagination Limit
|
|
|
|
Configure the maximum number of items returned in paginated responses:
|
|
|
|
```php
|
|
$server = Server::builder()
|
|
->setPaginationLimit(100); // Default: 50
|
|
```
|
|
|
|
### Instructions
|
|
|
|
Provide hints to help AI models understand how to use your server:
|
|
|
|
```php
|
|
$server = Server::builder()
|
|
->setInstructions('This calculator supports basic arithmetic operations. Use the calculate tool for math operations and check the config resource for current settings.');
|
|
```
|
|
|
|
## Discovery Configuration
|
|
|
|
**Required when using MCP attributes.** If you're using PHP attributes (`#[McpTool]`, `#[McpResource]`, `#[McpResourceTemplate]`, `#[McpPrompt]`) to define your MCP elements, you **MUST** configure discovery to tell the server where to look for these attributes.
|
|
|
|
```php
|
|
$server = Server::builder()
|
|
->setDiscovery(
|
|
basePath: __DIR__,
|
|
scanDirs: ['.', 'src', 'lib'], // Where to look for MCP attributes
|
|
excludeDirs: ['vendor', 'tests'], // Where NOT to look
|
|
cache: $cacheInstance, // Optional: cache discovered elements
|
|
namePatterns: ['*.php', '*.inc'], // Optional: list of filename patterns to match
|
|
);
|
|
```
|
|
|
|
**Parameters:**
|
|
- `$basePath` (string): Base directory for discovery (typically `__DIR__`)
|
|
- `$scanDirs` (array): Directories to recursively scan for `#[McpTool]`, `#[McpResource]`, etc. All subdirectories are included. (default: `['.', 'src']`)
|
|
- `$excludeDirs` (array): Directory names to exclude **within** the scanned directories during recursive scanning
|
|
- `$cache` (CacheInterface|null): Optional PSR-16 cache to store discovered elements for performance
|
|
- `$namePatterns` (array): Optional list of patterns (regexp, glob, or string) for file names (default: `['*.php']`)
|
|
|
|
**Basic Discovery (scans current directory and `src/`):**
|
|
```php
|
|
$server = Server::builder()
|
|
->setDiscovery(__DIR__) // Minimal setup
|
|
->build();
|
|
```
|
|
|
|
**Production Setup with Caching:**
|
|
```php
|
|
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
|
|
use Symfony\Component\Cache\Psr16Cache;
|
|
|
|
// Cache discovered elements to avoid filesystem scanning on every server start
|
|
$cache = new Psr16Cache(new FilesystemAdapter('mcp-discovery'));
|
|
|
|
$server = Server::builder()
|
|
->setDiscovery(
|
|
basePath: __DIR__,
|
|
scanDirs: ['src', 'lib'], // Scan these directories recursively
|
|
excludeDirs: ['vendor', 'tests', 'temp'], // Skip these directory names within scanned dirs
|
|
cache: $cache // Cache for performance
|
|
)
|
|
->build();
|
|
```
|
|
|
|
**How `excludeDirs` works:**
|
|
- If scanning `src/` and there's `src/vendor/`, it will be excluded
|
|
- If scanning `lib/` and there's `lib/tests/`, it will be excluded
|
|
- But if `vendor/` and `tests/` are at the same level as `src/`, they're not scanned anyway (not in `scanDirs`)
|
|
|
|
> **Performance**: Always use a cache in production. The first run scans and caches all discovered MCP elements, making
|
|
> subsequent server startups nearly instantaneous.
|
|
|
|
## Session Management
|
|
|
|
Configure session storage and lifecycle. By default, the SDK uses `InMemorySessionStore`:
|
|
|
|
```php
|
|
use Mcp\Server\Session\FileSessionStore;
|
|
use Mcp\Server\Session\InMemorySessionStore;
|
|
use Mcp\Server\Session\Psr16SessionStore;
|
|
use Symfony\Component\Cache\Psr16Cache;
|
|
use Symfony\Component\Cache\Adapter\RedisAdapter;
|
|
|
|
// Override with file-based storage
|
|
$server = Server::builder()
|
|
->setSession(new FileSessionStore(__DIR__ . '/sessions'))
|
|
->build();
|
|
|
|
// Override with in-memory storage and custom TTL
|
|
$server = Server::builder()
|
|
->setSession(new InMemorySessionStore(3600))
|
|
->build();
|
|
|
|
// Override with PSR-16 cache-based storage
|
|
// Requires psr/simple-cache and symfony/cache (or any other PSR-16 implementation)
|
|
// composer require psr/simple-cache symfony/cache
|
|
$redisAdapter = new RedisAdapter(
|
|
RedisAdapter::createConnection('redis://localhost:6379'),
|
|
'mcp_sessions'
|
|
);
|
|
|
|
$server = Server::builder()
|
|
->setSession(new Psr16SessionStore(
|
|
cache: new Psr16Cache($redisAdapter),
|
|
prefix: 'mcp-',
|
|
ttl: 3600
|
|
))
|
|
->build();
|
|
```
|
|
|
|
### Garbage Collection Configuration
|
|
|
|
The SDK periodically runs garbage collection to clean up expired sessions, similar to PHP's native
|
|
`session.gc_probability` and `session.gc_divisor` settings. The probability that GC runs on any given
|
|
request is `gcProbability / gcDivisor`.
|
|
|
|
```php
|
|
// Default: 1/100 (1% chance per request)
|
|
$server = Server::builder()
|
|
->setSession(new FileSessionStore(__DIR__ . '/sessions'))
|
|
->build();
|
|
|
|
// Higher frequency: 1/10 (10% chance per request)
|
|
$server = Server::builder()
|
|
->setSession(
|
|
new FileSessionStore(__DIR__ . '/sessions'),
|
|
gcProbability: 1,
|
|
gcDivisor: 10,
|
|
)
|
|
->build();
|
|
|
|
// Run GC on every request
|
|
$server = Server::builder()
|
|
->setSession(gcProbability: 1, gcDivisor: 1)
|
|
->build();
|
|
|
|
// Disable GC entirely (e.g. when using an external cleanup process)
|
|
$server = Server::builder()
|
|
->setSession(gcProbability: 0)
|
|
->build();
|
|
```
|
|
|
|
**Parameters:**
|
|
- `$gcProbability` (int): The numerator of the GC probability fraction (default: `1`). Set to `0` to disable GC.
|
|
- `$gcDivisor` (int): The denominator of the GC probability fraction (default: `100`). Must be >= 1.
|
|
|
|
> **Note**: When providing a custom `SessionManagerInterface` via the `$sessionManager` parameter,
|
|
> the `gcProbability` and `gcDivisor` settings are ignored — you control GC behavior in your own implementation.
|
|
|
|
**Available Session Stores:**
|
|
- `InMemorySessionStore`: Fast in-memory storage (default)
|
|
- `FileSessionStore`: Persistent file-based storage
|
|
- `Psr16StoreSession`: PSR-16 compliant cache-based storage
|
|
|
|
**Custom Session Stores:**
|
|
|
|
Implement `SessionStoreInterface` to create custom session storage:
|
|
|
|
```php
|
|
use Mcp\Server\Session\SessionStoreInterface;
|
|
use Symfony\Component\Uid\Uuid;
|
|
|
|
class RedisSessionStore implements SessionStoreInterface
|
|
{
|
|
public function __construct(private $redis, private int $ttl = 3600) {}
|
|
|
|
public function exists(Uuid $id): bool
|
|
{
|
|
return $this->redis->exists($id->toRfc4122());
|
|
}
|
|
|
|
public function read(Uuid $sessionId): string|false
|
|
{
|
|
$data = $this->redis->get($sessionId->toRfc4122());
|
|
return $data !== false ? $data : false;
|
|
}
|
|
|
|
public function write(Uuid $sessionId, string $data): bool
|
|
{
|
|
return $this->redis->setex($sessionId->toRfc4122(), $this->ttl, $data);
|
|
}
|
|
|
|
public function destroy(Uuid $sessionId): bool
|
|
{
|
|
return $this->redis->del($sessionId->toRfc4122()) > 0;
|
|
}
|
|
|
|
public function gc(): array
|
|
{
|
|
// Redis handles TTL automatically
|
|
return [];
|
|
}
|
|
}
|
|
```
|
|
|
|
## Manual Capability Registration
|
|
|
|
Register MCP elements programmatically without using attributes. The handler is the most important parameter and can be any PHP callable.
|
|
|
|
### Handler Types
|
|
|
|
**Handler** can be any PHP callable:
|
|
|
|
1. **Closure**: `function(int $a, int $b): int { return $a + $b; }`
|
|
2. **Class and method name pair**: `[ClassName::class, 'methodName']` - class must be constructable through the container
|
|
3. **Class instance and method name**: `[$instance, 'methodName']`
|
|
4. **Invokable class name**: `InvokableClass::class` - class must be constructable through the container and have `__invoke` method
|
|
|
|
### Manual Tool Registration
|
|
|
|
```php
|
|
$server = Server::builder()
|
|
// Using closure
|
|
->addTool(
|
|
handler: function(int $a, int $b): int { return $a + $b; },
|
|
name: 'add_numbers',
|
|
description: 'Adds two numbers together'
|
|
)
|
|
|
|
// Using class method pair
|
|
->addTool(
|
|
handler: [Calculator::class, 'multiply'],
|
|
name: 'multiply_numbers'
|
|
// name and description are optional - derived from method name and docblock
|
|
)
|
|
|
|
// Using instance method
|
|
->addTool(
|
|
handler: [$calculatorInstance, 'divide']
|
|
)
|
|
|
|
// Using invokable class
|
|
->addTool(
|
|
handler: InvokableCalculator::class
|
|
);
|
|
```
|
|
|
|
#### Parameters
|
|
|
|
- `handler` (callable|string): The tool handler
|
|
- `name` (string|null): Optional tool name
|
|
- `title` (string|null): Optional human-readable title for display in UI
|
|
- `description` (string|null): Optional tool description
|
|
- `annotations` (ToolAnnotations|null): Optional annotations for the tool
|
|
- `inputSchema` (array|null): Optional input schema for the tool
|
|
- `icons` (Icon[]|null): Optional array of icons for the tool
|
|
- `meta` (array|null): Optional metadata for the tool
|
|
|
|
### Manual Resource Registration
|
|
|
|
Register static resources:
|
|
|
|
```php
|
|
$server = Server::builder()
|
|
->addResource(
|
|
handler: [Config::class, 'getSettings'],
|
|
uri: 'config://app/settings',
|
|
name: 'app_config',
|
|
description: 'Application configuration',
|
|
mimeType: 'application/json'
|
|
);
|
|
```
|
|
|
|
#### Parameters
|
|
|
|
- `handler` (callable|string): The resource handler
|
|
- `uri` (string): The resource URI
|
|
- `name` (string|null): Optional resource name
|
|
- `description` (string|null): Optional resource description
|
|
- `mimeType` (string|null): Optional MIME type of the resource
|
|
- `size` (int|null): Optional size of the resource in bytes
|
|
- `annotations` (Annotations|null): Optional annotations for the resource
|
|
- `icons` (Icon[]|null): Optional array of icons for the resource
|
|
- `meta` (array|null): Optional metadata for the resource
|
|
|
|
### Manual Resource Template Registration
|
|
|
|
Register dynamic resources with URI templates:
|
|
|
|
```php
|
|
$server = Server::builder()
|
|
->addResourceTemplate(
|
|
handler: [UserService::class, 'getUserProfile'],
|
|
uriTemplate: 'user://{userId}/profile',
|
|
name: 'user_profile',
|
|
description: 'User profile by ID',
|
|
mimeType: 'application/json'
|
|
);
|
|
```
|
|
|
|
#### Parameters
|
|
|
|
- `handler` (callable|string): The resource template handler
|
|
- `uriTemplate` (string): The resource URI template
|
|
- `name` (string|null): Optional resource template name
|
|
- `description` (string|null): Optional resource template description
|
|
- `mimeType` (string|null): Optional MIME type of the resource
|
|
- `annotations` (Annotations|null): Optional annotations for the resource template
|
|
|
|
### Manual Prompt Registration
|
|
|
|
Register prompt generators:
|
|
|
|
```php
|
|
$server = Server::builder()
|
|
->addPrompt(
|
|
handler: [PromptService::class, 'generatePrompt'],
|
|
name: 'custom_prompt',
|
|
description: 'A custom prompt generator'
|
|
);
|
|
```
|
|
|
|
#### Parameters
|
|
|
|
- `handler` (callable|string): The prompt handler
|
|
- `name` (string|null): Optional prompt name
|
|
- `title` (string|null): Optional human-readable title for display in UI
|
|
- `description` (string|null): Optional prompt description
|
|
- `icons` (Icon[]|null): Optional array of icons for the prompt
|
|
|
|
**Note:** `name` and `description` are optional for all manual registrations. If not provided, they will be derived from
|
|
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
|
|
|
|
The container is used to resolve handlers and their dependencies when handlers inject dependencies in their constructors.
|
|
The SDK includes a basic container with simple auto-wiring capabilities.
|
|
|
|
```php
|
|
use Mcp\Capability\Registry\Container;
|
|
|
|
// Use the default basic container
|
|
$container = new Container();
|
|
$container->set(DatabaseService::class, new DatabaseService($pdo));
|
|
$container->set(\PDO::class, $pdo);
|
|
|
|
$server = Server::builder()
|
|
->setContainer($container)
|
|
->build();
|
|
```
|
|
|
|
**Basic Container Features:**
|
|
- Supports constructor auto-wiring for classes with parameterless constructors
|
|
- Resolves dependencies where all parameters are type-hinted classes/interfaces known to the container
|
|
- Supports parameters with default values
|
|
- Does NOT support scalar/built-in type injection without defaults
|
|
- Detects circular dependencies
|
|
|
|
You can also use any PSR-11 compatible container (Symfony DI, PHP-DI, Laravel Container, etc.).
|
|
|
|
### Logger
|
|
|
|
Provide a PSR-3 logger instance for internal server logging (request/response processing, errors, session management, transport events):
|
|
|
|
```php
|
|
use Monolog\Logger;
|
|
use Monolog\Handler\StreamHandler;
|
|
|
|
$logger = new Logger('mcp-server');
|
|
$logger->pushHandler(new StreamHandler('mcp.log', Logger::INFO));
|
|
|
|
$server = Server::builder()
|
|
->setLogger($logger);
|
|
```
|
|
|
|
### Event Dispatcher
|
|
|
|
Configure event dispatching:
|
|
|
|
```php
|
|
$server = Server::builder()
|
|
->setEventDispatcher($eventDispatcher);
|
|
```
|
|
|
|
## Custom Message Handlers
|
|
|
|
**Low-level escape hatch.** Custom message handlers run before the SDK's built-in handlers and give you total control over
|
|
individual JSON-RPC messages. They do not receive the builder's registry, container, or discovery output unless you pass
|
|
those dependencies in yourself.
|
|
|
|
> **Warning**: Custom message handlers bypass discovery, manual capability registration, and container lookups (unless
|
|
> you explicitly pass them). Tools, resources, and prompts you register elsewhere will not show up unless your handler
|
|
> loads and executes them manually. Reach for this API only when you need that level of control and are comfortable
|
|
> taking on the additional plumbing.
|
|
|
|
### Request Handlers
|
|
|
|
Handle JSON-RPC requests (messages with an `id` that expect a response). Request handlers **must** return either a
|
|
`Response` or an `Error` object.
|
|
|
|
Attach request handlers with `addRequestHandler()` (single) or `addRequestHandlers()` (multiple). You can call these
|
|
methods as many times as needed; each call prepends the handlers so they execute before the defaults:
|
|
|
|
```php
|
|
$server = Server::builder()
|
|
->addRequestHandler(new CustomListToolsHandler())
|
|
->addRequestHandlers([
|
|
new CustomCallToolHandler(),
|
|
new CustomGetPromptHandler(),
|
|
])
|
|
->build();
|
|
```
|
|
|
|
Request handlers implement `RequestHandlerInterface`:
|
|
|
|
```php
|
|
use Mcp\Schema\JsonRpc\Error;
|
|
use Mcp\Schema\JsonRpc\Request;
|
|
use Mcp\Schema\JsonRpc\Response;
|
|
use Mcp\Server\Handler\Request\RequestHandlerInterface;
|
|
use Mcp\Server\Session\SessionInterface;
|
|
|
|
interface RequestHandlerInterface
|
|
{
|
|
public function supports(Request $request): bool;
|
|
|
|
public function handle(Request $request, SessionInterface $session): Response|Error;
|
|
}
|
|
```
|
|
|
|
- `supports()` decides if the handler should process the incoming request
|
|
- `handle()` **must** return a `Response` (on success) or an `Error` (on failure)
|
|
|
|
### Notification Handlers
|
|
|
|
Handle JSON-RPC notifications (messages without an `id` that don't expect a response). Notification handlers **do not**
|
|
return anything - they perform side effects only.
|
|
|
|
Attach notification handlers with `addNotificationHandler()` (single) or `addNotificationHandlers()` (multiple):
|
|
|
|
```php
|
|
$server = Server::builder()
|
|
->addNotificationHandler(new LoggingNotificationHandler())
|
|
->addNotificationHandlers([
|
|
new InitializedNotificationHandler(),
|
|
new ProgressNotificationHandler(),
|
|
])
|
|
->build();
|
|
```
|
|
|
|
Notification handlers implement `NotificationHandlerInterface`:
|
|
|
|
```php
|
|
use Mcp\Schema\JsonRpc\Notification;
|
|
use Mcp\Server\Handler\Notification\NotificationHandlerInterface;
|
|
use Mcp\Server\Session\SessionInterface;
|
|
|
|
interface NotificationHandlerInterface
|
|
{
|
|
public function supports(Notification $notification): bool;
|
|
|
|
public function handle(Notification $notification, SessionInterface $session): void;
|
|
}
|
|
```
|
|
|
|
- `supports()` decides if the handler should process the incoming notification
|
|
- `handle()` performs side effects but **does not** return a value (notifications have no response)
|
|
|
|
### Key Differences
|
|
|
|
| Handler Type | Interface | Returns | Use Case |
|
|
|-------------|-----------|---------|----------|
|
|
| Request Handler | `RequestHandlerInterface` | `Response\|Error` | Handle requests that need responses (e.g., `tools/list`, `tools/call`) |
|
|
| Notification Handler | `NotificationHandlerInterface` | `void` | Handle fire-and-forget notifications (e.g., `notifications/initialized`, `notifications/progress`) |
|
|
|
|
### Example
|
|
|
|
Check out `examples/custom-method-handlers/server.php` for a complete example showing how to implement
|
|
custom `tools/list` and `tools/call` request handlers independently of the registry.
|
|
|
|
## Complete Example
|
|
|
|
Here's a comprehensive example showing all major configuration options:
|
|
|
|
```php
|
|
use Mcp\Server;
|
|
use Mcp\Server\Session\FileSessionStore;
|
|
use Mcp\Capability\Registry\Container;
|
|
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
|
|
use Symfony\Component\Cache\Psr16Cache;
|
|
use Monolog\Logger;
|
|
use Monolog\Handler\StreamHandler;
|
|
|
|
// Setup dependencies
|
|
$logger = new Logger('mcp-server');
|
|
$logger->pushHandler(new StreamHandler('mcp.log', Logger::INFO));
|
|
|
|
$cache = new Psr16Cache(new FilesystemAdapter('mcp-discovery'));
|
|
$sessionStore = new FileSessionStore(__DIR__ . '/sessions');
|
|
|
|
// Setup container with dependencies
|
|
$container = new Container();
|
|
$container->set(\PDO::class, new \PDO('sqlite::memory:'));
|
|
$container->set(DatabaseService::class, new DatabaseService($container->get(\PDO::class)));
|
|
|
|
// Build server
|
|
$server = Server::builder()
|
|
// Server identity
|
|
->setServerInfo('Advanced Calculator', '2.1.0')
|
|
|
|
// Performance and behavior
|
|
->setPaginationLimit(100)
|
|
->setInstructions('Use calculate tool for math operations. Check config resource for current settings.')
|
|
|
|
// Discovery with caching
|
|
->setDiscovery(__DIR__, ['src'], ['vendor', 'tests'], $cache)
|
|
|
|
// Session management
|
|
->setSession($sessionStore)
|
|
|
|
// Services
|
|
->setLogger($logger)
|
|
->setContainer($container)
|
|
|
|
// Manual capability registration
|
|
->addTool([Calculator::class, 'advancedCalculation'], 'advanced_calc')
|
|
->addResource([Config::class, 'getSettings'], 'config://app/settings', 'app_settings')
|
|
|
|
// Build the server
|
|
->build();
|
|
```
|
|
|
|
## Method Reference
|
|
|
|
| Method | Parameters | Description |
|
|
|--------|------------|-------------|
|
|
| `setServerInfo()` | name, version, description? | Set server identity |
|
|
| `setPaginationLimit()` | limit | Set max items per page |
|
|
| `setInstructions()` | instructions | Set usage instructions |
|
|
| `setDiscovery()` | basePath, scanDirs?, excludeDirs?, cache? | Configure attribute discovery |
|
|
| `setSession()` | sessionStore?, sessionManager?, gcProbability?, gcDivisor? | Configure session management |
|
|
| `setLogger()` | logger | Set PSR-3 logger |
|
|
| `setContainer()` | container | Set PSR-11 container |
|
|
| `setEventDispatcher()` | dispatcher | Set PSR-14 event dispatcher |
|
|
| `addRequestHandler()` | handler | Prepend a single custom request handler |
|
|
| `addRequestHandlers()` | handlers | Prepend multiple custom request handlers |
|
|
| `addNotificationHandler()` | handler | Prepend a single custom notification handler |
|
|
| `addNotificationHandlers()` | handlers | Prepend multiple custom notification handlers |
|
|
| `addTool()` | handler, name?, title?, description?, annotations?, inputSchema?, ... | Register tool |
|
|
| `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 |
|