Skip to content
Merged
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
115 changes: 31 additions & 84 deletions docs/server-builder.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ various aspects of the server behavior.
- [Session Management](#session-management)
- [Manual Capability Registration](#manual-capability-registration)
- [Service Dependencies](#service-dependencies)
- [Custom Capability Handlers](#custom-capability-handlers)
- [Custom Method Handlers](#custom-method-handlers)
- [Complete Example](#complete-example)
- [Method Reference](#method-reference)

Expand Down Expand Up @@ -344,102 +344,50 @@ $server = Server::builder()
->setEventDispatcher($eventDispatcher);
```

## Custom Capability Handlers
## Custom Method Handlers

**Advanced customization for specific use cases.** Override the default capability handlers when you need completely custom
behavior for how tools are executed, resources are read, or prompts are generated. Most users should stick with the default implementations.
**Low-level escape hatch.** Custom method handlers run before the SDK’s built-in handlers and give you total control over
individual JSON-RPC methods. They do not receive the builder’s registry, container, or discovery output unless you pass
those dependencies in yourself.

The default handlers work by:
1. Looking up registered tools/resources/prompts by name/URI
2. Resolving the handler from the container
3. Executing the handler with the provided arguments
4. Formatting the result and handling errors

### Custom Tool Caller

Replace how tool execution requests are processed. Your custom `ToolCallerInterface` receives a `CallToolRequest` (with
tool name and arguments) and must return a `CallToolResult`.
Attach handlers with `addMethodHandler()` (single) or `addMethodHandlers()` (multiple). You can call these methods as
many times as needed; each call prepends the handlers so they execute before the defaults:

```php
use Mcp\Capability\Tool\ToolCallerInterface;
use Mcp\Schema\Request\CallToolRequest;
use Mcp\Schema\Result\CallToolResult;

class CustomToolCaller implements ToolCallerInterface
{
public function call(CallToolRequest $request): CallToolResult
{
// Custom tool routing, execution, authentication, caching, etc.
// You handle finding the tool, executing it, and formatting results
$toolName = $request->name;
$arguments = $request->arguments ?? [];

// Your custom logic here
return new CallToolResult([/* content */]);
}
}

$server = Server::builder()
->setToolCaller(new CustomToolCaller());
->addMethodHandler(new AuditHandler())
->addMethodHandlers([
new CustomListToolsHandler(),
new CustomCallToolHandler(),
])
->build();
```

### Custom Resource Reader

Replace how resource reading requests are processed. Your custom `ResourceReaderInterface` receives a `ReadResourceRequest`
(with URI) and must return a `ReadResourceResult`.
Custom handlers implement `MethodHandlerInterface`:

```php
use Mcp\Capability\Resource\ResourceReaderInterface;
use Mcp\Schema\Request\ReadResourceRequest;
use Mcp\Schema\Result\ReadResourceResult;
use Mcp\Schema\JsonRpc\HasMethodInterface;
use Mcp\Server\Handler\MethodHandlerInterface;
use Mcp\Server\Session\SessionInterface;

class CustomResourceReader implements ResourceReaderInterface
interface MethodHandlerInterface
{
public function read(ReadResourceRequest $request): ReadResourceResult
{
// Custom resource resolution, caching, access control, etc.
$uri = $request->uri;

// Your custom logic here
return new ReadResourceResult([/* content */]);
}
}
public function supports(HasMethodInterface $message): bool;

$server = Server::builder()
->setResourceReader(new CustomResourceReader());
public function handle(HasMethodInterface $message, SessionInterface $session);
}
```

### Custom Prompt Getter

Replace how prompt generation requests are processed. Your custom `PromptGetterInterface` receives a `GetPromptRequest`
(with prompt name and arguments) and must return a `GetPromptResult`.

```php
use Mcp\Capability\Prompt\PromptGetterInterface;
use Mcp\Schema\Request\GetPromptRequest;
use Mcp\Schema\Result\GetPromptResult;

class CustomPromptGetter implements PromptGetterInterface
{
public function get(GetPromptRequest $request): GetPromptResult
{
// Custom prompt generation, template engines, dynamic content, etc.
$promptName = $request->name;
$arguments = $request->arguments ?? [];

// Your custom logic here
return new GetPromptResult([/* messages */]);
}
}
- `supports()` decides if the handler should look at the incoming message.
- `handle()` must return a JSON-RPC `Response`, an `Error`, or `null`.

$server = Server::builder()
->setPromptGetter(new CustomPromptGetter());
```
Check out `examples/custom-method-handlers/server.php` for a complete example showing how to implement
custom `tool/list` and `tool/call` methods independently of the registry.

> **Warning**: Custom capability handlers bypass the entire default registration system (discovered attributes, manual
> registration, container resolution, etc.). You become responsible for all aspect of execution, including error handling,
> logging, and result formatting. Only use this for very specific advanced use cases like custom authentication, complex
> routing, or integration with external systems.
> **Warning**: Custom method handlers bypass discovery, manual capability registration, and container lookups (unlesss
> 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.

## Complete Example

Expand Down Expand Up @@ -505,9 +453,8 @@ $server = Server::builder()
| `setLogger()` | logger | Set PSR-3 logger |
| `setContainer()` | container | Set PSR-11 container |
| `setEventDispatcher()` | dispatcher | Set PSR-14 event dispatcher |
| `setToolCaller()` | caller | Set custom tool caller |
| `setResourceReader()` | reader | Set custom resource reader |
| `setPromptGetter()` | getter | Set custom prompt getter |
| `addMethodHandler()` | handler | Prepend a single custom method handler |
| `addMethodHandlers()` | handlers | Prepend multiple custom method handlers |
| `addTool()` | handler, name?, 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 |
Expand Down
144 changes: 144 additions & 0 deletions examples/custom-method-handlers/server.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#!/usr/bin/env php
<?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.
*/

require_once dirname(__DIR__).'/bootstrap.php';
chdir(__DIR__);

use Mcp\Schema\Content\TextContent;
use Mcp\Schema\JsonRpc\Error;
use Mcp\Schema\JsonRpc\HasMethodInterface;
use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Request\CallToolRequest;
use Mcp\Schema\Request\ListToolsRequest;
use Mcp\Schema\Result\CallToolResult;
use Mcp\Schema\Result\ListToolsResult;
use Mcp\Schema\ServerCapabilities;
use Mcp\Schema\Tool;
use Mcp\Server;
use Mcp\Server\Handler\MethodHandlerInterface;
use Mcp\Server\Session\SessionInterface;
use Mcp\Server\Transport\StdioTransport;

logger()->info('Starting MCP Custom Method Handlers (Stdio) Server...');

$toolDefinitions = [
'say_hello' => new Tool(
name: 'say_hello',
inputSchema: [
'type' => 'object',
'properties' => [
'name' => ['type' => 'string', 'description' => 'Name to greet'],
],
'required' => ['name'],
],
description: 'Greets a user by name.',
annotations: null,
),
'sum' => new Tool(
name: 'sum',
inputSchema: [
'type' => 'object',
'properties' => [
'a' => ['type' => 'number'],
'b' => ['type' => 'number'],
],
'required' => ['a', 'b'],
],
description: 'Returns a+b.',
annotations: null,
),
];

$listToolsHandler = new class($toolDefinitions) implements MethodHandlerInterface {
/**
* @param array<string, Tool> $toolDefinitions
*/
public function __construct(private array $toolDefinitions)
{
}

public function supports(HasMethodInterface $message): bool
{
return $message instanceof ListToolsRequest;
}

public function handle(ListToolsRequest|HasMethodInterface $message, SessionInterface $session): Response
{
assert($message instanceof ListToolsRequest);

return new Response($message->getId(), new ListToolsResult(array_values($this->toolDefinitions), null));
}
};

$callToolHandler = new class($toolDefinitions) implements MethodHandlerInterface {
/**
* @param array<string, Tool> $toolDefinitions
*/
public function __construct(private array $toolDefinitions)
{
}

public function supports(HasMethodInterface $message): bool
{
return $message instanceof CallToolRequest;
}

public function handle(CallToolRequest|HasMethodInterface $message, SessionInterface $session): Response|Error
{
assert($message instanceof CallToolRequest);

$name = $message->name;
$args = $message->arguments ?? [];

if (!isset($this->toolDefinitions[$name])) {
return new Error($message->getId(), Error::METHOD_NOT_FOUND, sprintf('Tool not found: %s', $name));
}

try {
switch ($name) {
case 'say_hello':
$greetName = (string) ($args['name'] ?? 'world');
$result = [new TextContent(sprintf('Hello, %s!', $greetName))];
break;
case 'sum':
$a = (float) ($args['a'] ?? 0);
$b = (float) ($args['b'] ?? 0);
$result = [new TextContent((string) ($a + $b))];
break;
default:
$result = [new TextContent('Unknown tool')];
}

return new Response($message->getId(), new CallToolResult($result));
} catch (Throwable $e) {
return new Response($message->getId(), new CallToolResult([new TextContent('Tool execution failed')], true));
}
}
};

$capabilities = new ServerCapabilities(tools: true, resources: false, prompts: false);

$server = Server::builder()
->setServerInfo('Custom Handlers Server', '1.0.0')
->setLogger(logger())
->setContainer(container())
->setCapabilities($capabilities)
->addMethodHandlers([$listToolsHandler, $callToolHandler])
->build();

$transport = new StdioTransport(logger: logger());

$server->connect($transport);

$transport->listen();

logger()->info('Server listener stopped gracefully.');
75 changes: 0 additions & 75 deletions src/Capability/Completion/Completer.php

This file was deleted.

25 changes: 0 additions & 25 deletions src/Capability/Completion/CompleterInterface.php

This file was deleted.

Loading