Skip to content

Commit 54a5d14

Browse files
[Server] Refactor protocol pipeline to separate request and notification handling (#106)
1 parent 9ff6516 commit 54a5d14

28 files changed

+1843
-719
lines changed

docs/server-builder.md

Lines changed: 78 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ various aspects of the server behavior.
1212
- [Session Management](#session-management)
1313
- [Manual Capability Registration](#manual-capability-registration)
1414
- [Service Dependencies](#service-dependencies)
15-
- [Custom Method Handlers](#custom-method-handlers)
15+
- [Custom Message Handlers](#custom-message-handlers)
1616
- [Complete Example](#complete-example)
1717
- [Method Reference](#method-reference)
1818

@@ -344,50 +344,101 @@ $server = Server::builder()
344344
->setEventDispatcher($eventDispatcher);
345345
```
346346

347-
## Custom Method Handlers
347+
## Custom Message Handlers
348348

349-
**Low-level escape hatch.** Custom method handlers run before the SDKs built-in handlers and give you total control over
350-
individual JSON-RPC methods. They do not receive the builders registry, container, or discovery output unless you pass
349+
**Low-level escape hatch.** Custom message handlers run before the SDK's built-in handlers and give you total control over
350+
individual JSON-RPC messages. They do not receive the builder's registry, container, or discovery output unless you pass
351351
those dependencies in yourself.
352352

353-
Attach handlers with `addMethodHandler()` (single) or `addMethodHandlers()` (multiple). You can call these methods as
354-
many times as needed; each call prepends the handlers so they execute before the defaults:
353+
> **Warning**: Custom message handlers bypass discovery, manual capability registration, and container lookups (unless
354+
> you explicitly pass them). Tools, resources, and prompts you register elsewhere will not show up unless your handler
355+
> loads and executes them manually. Reach for this API only when you need that level of control and are comfortable
356+
> taking on the additional plumbing.
357+
358+
### Request Handlers
359+
360+
Handle JSON-RPC requests (messages with an `id` that expect a response). Request handlers **must** return either a
361+
`Response` or an `Error` object.
362+
363+
Attach request handlers with `addRequestHandler()` (single) or `addRequestHandlers()` (multiple). You can call these
364+
methods as many times as needed; each call prepends the handlers so they execute before the defaults:
355365

356366
```php
357367
$server = Server::builder()
358-
->addMethodHandler(new AuditHandler())
359-
->addMethodHandlers([
360-
new CustomListToolsHandler(),
368+
->addRequestHandler(new CustomListToolsHandler())
369+
->addRequestHandlers([
361370
new CustomCallToolHandler(),
371+
new CustomGetPromptHandler(),
362372
])
363373
->build();
364374
```
365375

366-
Custom handlers implement `MethodHandlerInterface`:
376+
Request handlers implement `RequestHandlerInterface`:
367377

368378
```php
369-
use Mcp\Schema\JsonRpc\HasMethodInterface;
370-
use Mcp\Server\Handler\MethodHandlerInterface;
379+
use Mcp\Schema\JsonRpc\Error;
380+
use Mcp\Schema\JsonRpc\Request;
381+
use Mcp\Schema\JsonRpc\Response;
382+
use Mcp\Server\Handler\Request\RequestHandlerInterface;
371383
use Mcp\Server\Session\SessionInterface;
372384

373-
interface MethodHandlerInterface
385+
interface RequestHandlerInterface
374386
{
375-
public function supports(HasMethodInterface $message): bool;
387+
public function supports(Request $request): bool;
376388

377-
public function handle(HasMethodInterface $message, SessionInterface $session);
389+
public function handle(Request $request, SessionInterface $session): Response|Error;
378390
}
379391
```
380392

381-
- `supports()` decides if the handler should look at the incoming message.
382-
- `handle()` must return a JSON-RPC `Response`, an `Error`, or `null`.
393+
- `supports()` decides if the handler should process the incoming request
394+
- `handle()` **must** return a `Response` (on success) or an `Error` (on failure)
383395

384-
Check out `examples/custom-method-handlers/server.php` for a complete example showing how to implement
385-
custom `tool/list` and `tool/call` methods independently of the registry.
396+
### Notification Handlers
386397

387-
> **Warning**: Custom method handlers bypass discovery, manual capability registration, and container lookups (unlesss
388-
> you explicitly pass them). Tools, resources, and prompts you register elsewhere will not show up unless your handler
389-
> loads and executes them manually.
390-
> Reach for this API only when you need that level of control and are comfortable taking on the additional plumbing.
398+
Handle JSON-RPC notifications (messages without an `id` that don't expect a response). Notification handlers **do not**
399+
return anything - they perform side effects only.
400+
401+
Attach notification handlers with `addNotificationHandler()` (single) or `addNotificationHandlers()` (multiple):
402+
403+
```php
404+
$server = Server::builder()
405+
->addNotificationHandler(new LoggingNotificationHandler())
406+
->addNotificationHandlers([
407+
new InitializedNotificationHandler(),
408+
new ProgressNotificationHandler(),
409+
])
410+
->build();
411+
```
412+
413+
Notification handlers implement `NotificationHandlerInterface`:
414+
415+
```php
416+
use Mcp\Schema\JsonRpc\Notification;
417+
use Mcp\Server\Handler\Notification\NotificationHandlerInterface;
418+
use Mcp\Server\Session\SessionInterface;
419+
420+
interface NotificationHandlerInterface
421+
{
422+
public function supports(Notification $notification): bool;
423+
424+
public function handle(Notification $notification, SessionInterface $session): void;
425+
}
426+
```
427+
428+
- `supports()` decides if the handler should process the incoming notification
429+
- `handle()` performs side effects but **does not** return a value (notifications have no response)
430+
431+
### Key Differences
432+
433+
| Handler Type | Interface | Returns | Use Case |
434+
|-------------|-----------|---------|----------|
435+
| Request Handler | `RequestHandlerInterface` | `Response\|Error` | Handle requests that need responses (e.g., `tools/list`, `tools/call`) |
436+
| Notification Handler | `NotificationHandlerInterface` | `void` | Handle fire-and-forget notifications (e.g., `notifications/initialized`, `notifications/progress`) |
437+
438+
### Example
439+
440+
Check out `examples/custom-method-handlers/server.php` for a complete example showing how to implement
441+
custom `tools/list` and `tools/call` request handlers independently of the registry.
391442

392443
## Complete Example
393444

@@ -453,8 +504,10 @@ $server = Server::builder()
453504
| `setLogger()` | logger | Set PSR-3 logger |
454505
| `setContainer()` | container | Set PSR-11 container |
455506
| `setEventDispatcher()` | dispatcher | Set PSR-14 event dispatcher |
456-
| `addMethodHandler()` | handler | Prepend a single custom method handler |
457-
| `addMethodHandlers()` | handlers | Prepend multiple custom method handlers |
507+
| `addRequestHandler()` | handler | Prepend a single custom request handler |
508+
| `addRequestHandlers()` | handlers | Prepend multiple custom request handlers |
509+
| `addNotificationHandler()` | handler | Prepend a single custom notification handler |
510+
| `addNotificationHandlers()` | handlers | Prepend multiple custom notification handlers |
458511
| `addTool()` | handler, name?, description?, annotations?, inputSchema? | Register tool |
459512
| `addResource()` | handler, uri, name?, description?, mimeType?, size?, annotations? | Register resource |
460513
| `addResourceTemplate()` | handler, uriTemplate, name?, description?, mimeType?, annotations? | Register resource template |

examples/custom-method-handlers/server.php

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
use Mcp\Schema\Content\TextContent;
1717
use Mcp\Schema\JsonRpc\Error;
18-
use Mcp\Schema\JsonRpc\HasMethodInterface;
18+
use Mcp\Schema\JsonRpc\Request;
1919
use Mcp\Schema\JsonRpc\Response;
2020
use Mcp\Schema\Request\CallToolRequest;
2121
use Mcp\Schema\Request\ListToolsRequest;
@@ -24,7 +24,7 @@
2424
use Mcp\Schema\ServerCapabilities;
2525
use Mcp\Schema\Tool;
2626
use Mcp\Server;
27-
use Mcp\Server\Handler\MethodHandlerInterface;
27+
use Mcp\Server\Handler\Request\RequestHandlerInterface;
2828
use Mcp\Server\Session\SessionInterface;
2929
use Mcp\Server\Transport\StdioTransport;
3030

@@ -58,49 +58,49 @@
5858
),
5959
];
6060

61-
$listToolsHandler = new class($toolDefinitions) implements MethodHandlerInterface {
61+
$listToolsHandler = new class($toolDefinitions) implements RequestHandlerInterface {
6262
/**
6363
* @param array<string, Tool> $toolDefinitions
6464
*/
6565
public function __construct(private array $toolDefinitions)
6666
{
6767
}
6868

69-
public function supports(HasMethodInterface $message): bool
69+
public function supports(Request $request): bool
7070
{
71-
return $message instanceof ListToolsRequest;
71+
return $request instanceof ListToolsRequest;
7272
}
7373

74-
public function handle(ListToolsRequest|HasMethodInterface $message, SessionInterface $session): Response
74+
public function handle(Request $request, SessionInterface $session): Response
7575
{
76-
assert($message instanceof ListToolsRequest);
76+
assert($request instanceof ListToolsRequest);
7777

78-
return new Response($message->getId(), new ListToolsResult(array_values($this->toolDefinitions), null));
78+
return new Response($request->getId(), new ListToolsResult(array_values($this->toolDefinitions), null));
7979
}
8080
};
8181

82-
$callToolHandler = new class($toolDefinitions) implements MethodHandlerInterface {
82+
$callToolHandler = new class($toolDefinitions) implements RequestHandlerInterface {
8383
/**
8484
* @param array<string, Tool> $toolDefinitions
8585
*/
8686
public function __construct(private array $toolDefinitions)
8787
{
8888
}
8989

90-
public function supports(HasMethodInterface $message): bool
90+
public function supports(Request $request): bool
9191
{
92-
return $message instanceof CallToolRequest;
92+
return $request instanceof CallToolRequest;
9393
}
9494

95-
public function handle(CallToolRequest|HasMethodInterface $message, SessionInterface $session): Response|Error
95+
public function handle(Request $request, SessionInterface $session): Response|Error
9696
{
97-
assert($message instanceof CallToolRequest);
97+
assert($request instanceof CallToolRequest);
9898

99-
$name = $message->name;
100-
$args = $message->arguments ?? [];
99+
$name = $request->name;
100+
$args = $request->arguments ?? [];
101101

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

106106
try {
@@ -118,9 +118,9 @@ public function handle(CallToolRequest|HasMethodInterface $message, SessionInter
118118
$result = [new TextContent('Unknown tool')];
119119
}
120120

121-
return new Response($message->getId(), new CallToolResult($result));
121+
return new Response($request->getId(), new CallToolResult($result));
122122
} catch (Throwable $e) {
123-
return new Response($message->getId(), new CallToolResult([new TextContent('Tool execution failed')], true));
123+
return new Response($request->getId(), new CallToolResult([new TextContent('Tool execution failed')], true));
124124
}
125125
}
126126
};
@@ -132,7 +132,7 @@ public function handle(CallToolRequest|HasMethodInterface $message, SessionInter
132132
->setLogger(logger())
133133
->setContainer(container())
134134
->setCapabilities($capabilities)
135-
->addMethodHandlers([$listToolsHandler, $callToolHandler])
135+
->addRequestHandlers([$listToolsHandler, $callToolHandler])
136136
->build();
137137

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

0 commit comments

Comments
 (0)