[Server] Refactor protocol pipeline to separate request and notification handling (#106)

This commit is contained in:
Kyrian Obikwelu 2025-10-12 13:53:41 +01:00 • committed by GitHub
commit 54a5d1495c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 1843 additions and 719 deletions

View file

@ -12,7 +12,7 @@ various aspects of the server behavior.
- [Session Management](#session-management) - [Session Management](#session-management)
- [Manual Capability Registration](#manual-capability-registration) - [Manual Capability Registration](#manual-capability-registration)
- [Service Dependencies](#service-dependencies) - [Service Dependencies](#service-dependencies)
- [Custom Method Handlers](#custom-method-handlers) - [Custom Message Handlers](#custom-message-handlers)
- [Complete Example](#complete-example) - [Complete Example](#complete-example)
- [Method Reference](#method-reference) - [Method Reference](#method-reference)
@ -344,50 +344,101 @@ $server = Server::builder()
->setEventDispatcher($eventDispatcher); ->setEventDispatcher($eventDispatcher);
``` ```
## Custom Method Handlers ## Custom Message Handlers
**Low-level escape hatch.** Custom method handlers run before the SDK’s built-in handlers and give you total control over **Low-level escape hatch.** Custom message 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 individual JSON-RPC messages. They do not receive the builder's registry, container, or discovery output unless you pass
those dependencies in yourself. those dependencies in yourself.
Attach handlers with `addMethodHandler()` (single) or `addMethodHandlers()` (multiple). You can call these methods as > **Warning**: Custom message handlers bypass discovery, manual capability registration, and container lookups (unless
many times as needed; each call prepends the handlers so they execute before the defaults: > 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 ```php
$server = Server::builder() $server = Server::builder()
->addMethodHandler(new AuditHandler()) ->addRequestHandler(new CustomListToolsHandler())
->addMethodHandlers([ ->addRequestHandlers([
new CustomListToolsHandler(),
new CustomCallToolHandler(), new CustomCallToolHandler(),
new CustomGetPromptHandler(),
]) ])
->build(); ->build();
``` ```
Custom handlers implement `MethodHandlerInterface`: Request handlers implement `RequestHandlerInterface`:
```php ```php
use Mcp\Schema\JsonRpc\HasMethodInterface; use Mcp\Schema\JsonRpc\Error;
use Mcp\Server\Handler\MethodHandlerInterface; use Mcp\Schema\JsonRpc\Request;
use Mcp\Schema\JsonRpc\Response;
use Mcp\Server\Handler\Request\RequestHandlerInterface;
use Mcp\Server\Session\SessionInterface; use Mcp\Server\Session\SessionInterface;
interface MethodHandlerInterface interface RequestHandlerInterface
{ {
public function supports(HasMethodInterface $message): bool; public function supports(Request $request): bool;
public function handle(HasMethodInterface $message, SessionInterface $session); public function handle(Request $request, SessionInterface $session): Response|Error;
} }
``` ```
- `supports()` decides if the handler should look at the incoming message. - `supports()` decides if the handler should process the incoming request
- `handle()` must return a JSON-RPC `Response`, an `Error`, or `null`. - `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 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. custom `tools/list` and `tools/call` request handlers independently of the registry.
> **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 ## Complete Example
@ -453,8 +504,10 @@ $server = Server::builder()
| `setLogger()` | logger | Set PSR-3 logger | | `setLogger()` | logger | Set PSR-3 logger |
| `setContainer()` | container | Set PSR-11 container | | `setContainer()` | container | Set PSR-11 container |
| `setEventDispatcher()` | dispatcher | Set PSR-14 event dispatcher | | `setEventDispatcher()` | dispatcher | Set PSR-14 event dispatcher |
| `addMethodHandler()` | handler | Prepend a single custom method handler | | `addRequestHandler()` | handler | Prepend a single custom request handler |
| `addMethodHandlers()` | handlers | Prepend multiple custom method handlers | | `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?, description?, annotations?, inputSchema? | Register tool | | `addTool()` | handler, name?, description?, annotations?, inputSchema? | Register tool |
| `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 |

View file

@ -15,7 +15,7 @@ chdir(__DIR__);
use Mcp\Schema\Content\TextContent; use Mcp\Schema\Content\TextContent;
use Mcp\Schema\JsonRpc\Error; use Mcp\Schema\JsonRpc\Error;
use Mcp\Schema\JsonRpc\HasMethodInterface; use Mcp\Schema\JsonRpc\Request;
use Mcp\Schema\JsonRpc\Response; use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Request\CallToolRequest; use Mcp\Schema\Request\CallToolRequest;
use Mcp\Schema\Request\ListToolsRequest; use Mcp\Schema\Request\ListToolsRequest;
@ -24,7 +24,7 @@ use Mcp\Schema\Result\ListToolsResult;
use Mcp\Schema\ServerCapabilities; use Mcp\Schema\ServerCapabilities;
use Mcp\Schema\Tool; use Mcp\Schema\Tool;
use Mcp\Server; use Mcp\Server;
use Mcp\Server\Handler\MethodHandlerInterface; use Mcp\Server\Handler\Request\RequestHandlerInterface;
use Mcp\Server\Session\SessionInterface; use Mcp\Server\Session\SessionInterface;
use Mcp\Server\Transport\StdioTransport; use Mcp\Server\Transport\StdioTransport;
@ -58,7 +58,7 @@ $toolDefinitions = [
), ),
]; ];
$listToolsHandler = new class($toolDefinitions) implements MethodHandlerInterface { $listToolsHandler = new class($toolDefinitions) implements RequestHandlerInterface {
/** /**
* @param array<string, Tool> $toolDefinitions * @param array<string, Tool> $toolDefinitions
*/ */
@ -66,20 +66,20 @@ $listToolsHandler = new class($toolDefinitions) implements MethodHandlerInterfac
{ {
} }
public function supports(HasMethodInterface $message): bool public function supports(Request $request): bool
{ {
return $message instanceof ListToolsRequest; return $request instanceof ListToolsRequest;
} }
public function handle(ListToolsRequest|HasMethodInterface $message, SessionInterface $session): Response public function handle(Request $request, SessionInterface $session): Response
{ {
assert($message instanceof ListToolsRequest); assert($request instanceof ListToolsRequest);
return new Response($message->getId(), new ListToolsResult(array_values($this->toolDefinitions), null)); return new Response($request->getId(), new ListToolsResult(array_values($this->toolDefinitions), null));
} }
}; };
$callToolHandler = new class($toolDefinitions) implements MethodHandlerInterface { $callToolHandler = new class($toolDefinitions) implements RequestHandlerInterface {
/** /**
* @param array<string, Tool> $toolDefinitions * @param array<string, Tool> $toolDefinitions
*/ */
@ -87,20 +87,20 @@ $callToolHandler = new class($toolDefinitions) implements MethodHandlerInterface
{ {
} }
public function supports(HasMethodInterface $message): bool public function supports(Request $request): bool
{ {
return $message instanceof CallToolRequest; return $request instanceof CallToolRequest;
} }
public function handle(CallToolRequest|HasMethodInterface $message, SessionInterface $session): Response|Error public function handle(Request $request, SessionInterface $session): Response|Error
{ {
assert($message instanceof CallToolRequest); assert($request instanceof CallToolRequest);
$name = $message->name; $name = $request->name;
$args = $message->arguments ?? []; $args = $request->arguments ?? [];
if (!isset($this->toolDefinitions[$name])) { if (!isset($this->toolDefinitions[$name])) {
return new Error($message->getId(), Error::METHOD_NOT_FOUND, sprintf('Tool not found: %s', $name)); return new Error($request->getId(), Error::METHOD_NOT_FOUND, sprintf('Tool not found: %s', $name));
} }
try { try {
@ -118,9 +118,9 @@ $callToolHandler = new class($toolDefinitions) implements MethodHandlerInterface
$result = [new TextContent('Unknown tool')]; $result = [new TextContent('Unknown tool')];
} }
return new Response($message->getId(), new CallToolResult($result)); return new Response($request->getId(), new CallToolResult($result));
} catch (Throwable $e) { } catch (Throwable $e) {
return new Response($message->getId(), new CallToolResult([new TextContent('Tool execution failed')], true)); return new Response($request->getId(), new CallToolResult([new TextContent('Tool execution failed')], true));
} }
} }
}; };
@ -132,7 +132,7 @@ $server = Server::builder()
->setLogger(logger()) ->setLogger(logger())
->setContainer(container()) ->setContainer(container())
->setCapabilities($capabilities) ->setCapabilities($capabilities)
->addMethodHandlers([$listToolsHandler, $callToolHandler]) ->addRequestHandlers([$listToolsHandler, $callToolHandler])
->build(); ->build();
$transport = new StdioTransport(logger: logger()); $transport = new StdioTransport(logger: logger());

View file

@ -13,62 +13,75 @@ namespace Mcp\JsonRpc;
use Mcp\Exception\InvalidArgumentException; use Mcp\Exception\InvalidArgumentException;
use Mcp\Exception\InvalidInputMessageException; use Mcp\Exception\InvalidInputMessageException;
use Mcp\Schema\JsonRpc\HasMethodInterface; use Mcp\Schema;
use Mcp\Schema\Notification; use Mcp\Schema\JsonRpc\Error;
use Mcp\Schema\Request; use Mcp\Schema\JsonRpc\MessageInterface;
use Mcp\Schema\JsonRpc\Notification;
use Mcp\Schema\JsonRpc\Request;
use Mcp\Schema\JsonRpc\Response;
/** /**
* Factory for creating JSON-RPC message objects from raw input.
*
* Handles all types of JSON-RPC messages:
* - Requests (have method + id)
* - Notifications (have method, no id)
* - Responses (have result + id)
* - Errors (have error + id)
*
* @author Christopher Hertel <mail@christopher-hertel.de> * @author Christopher Hertel <mail@christopher-hertel.de>
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
*/ */
final class MessageFactory final class MessageFactory
{ {
/** /**
* Registry of all known messages. * Registry of all known message classes that have methods.
* *
* @var array<int, class-string<HasMethodInterface>> * @var array<int, class-string<Request|Notification>>
*/ */
private const REGISTERED_MESSAGES = [ private const REGISTERED_MESSAGES = [
Notification\CancelledNotification::class, Schema\Notification\CancelledNotification::class,
Notification\InitializedNotification::class, Schema\Notification\InitializedNotification::class,
Notification\LoggingMessageNotification::class, Schema\Notification\LoggingMessageNotification::class,
Notification\ProgressNotification::class, Schema\Notification\ProgressNotification::class,
Notification\PromptListChangedNotification::class, Schema\Notification\PromptListChangedNotification::class,
Notification\ResourceListChangedNotification::class, Schema\Notification\ResourceListChangedNotification::class,
Notification\ResourceUpdatedNotification::class, Schema\Notification\ResourceUpdatedNotification::class,
Notification\RootsListChangedNotification::class, Schema\Notification\RootsListChangedNotification::class,
Notification\ToolListChangedNotification::class, Schema\Notification\ToolListChangedNotification::class,
Request\CallToolRequest::class,
Request\CompletionCompleteRequest::class, Schema\Request\CallToolRequest::class,
Request\CreateSamplingMessageRequest::class, Schema\Request\CompletionCompleteRequest::class,
Request\GetPromptRequest::class, Schema\Request\CreateSamplingMessageRequest::class,
Request\InitializeRequest::class, Schema\Request\GetPromptRequest::class,
Request\ListPromptsRequest::class, Schema\Request\InitializeRequest::class,
Request\ListResourcesRequest::class, Schema\Request\ListPromptsRequest::class,
Request\ListResourceTemplatesRequest::class, Schema\Request\ListResourcesRequest::class,
Request\ListRootsRequest::class, Schema\Request\ListResourceTemplatesRequest::class,
Request\ListToolsRequest::class, Schema\Request\ListRootsRequest::class,
Request\PingRequest::class, Schema\Request\ListToolsRequest::class,
Request\ReadResourceRequest::class, Schema\Request\PingRequest::class,
Request\ResourceSubscribeRequest::class, Schema\Request\ReadResourceRequest::class,
Request\ResourceUnsubscribeRequest::class, Schema\Request\ResourceSubscribeRequest::class,
Request\SetLogLevelRequest::class, Schema\Request\ResourceUnsubscribeRequest::class,
Schema\Request\SetLogLevelRequest::class,
]; ];
/** /**
* @param array<int, class-string<HasMethodInterface>> $registeredMessages * @param array<int, class-string<Request|Notification>> $registeredMessages
*/ */
public function __construct( public function __construct(
private readonly array $registeredMessages, private readonly array $registeredMessages,
) { ) {
foreach ($this->registeredMessages as $message) { foreach ($this->registeredMessages as $messageClass) {
if (!is_subclass_of($message, HasMethodInterface::class)) { if (!is_subclass_of($messageClass, Request::class) && !is_subclass_of($messageClass, Notification::class)) {
throw new InvalidArgumentException(\sprintf('Message classes must implement %s.', HasMethodInterface::class)); throw new InvalidArgumentException(\sprintf('Message classes must extend %s or %s.', Request::class, Notification::class));
} }
} }
} }
/** /**
* Creates a new Factory instance with the all the protocol's default notifications and requests. * Creates a new Factory instance with all the protocol's default messages.
*/ */
public static function make(): self public static function make(): self
{ {
@ -76,11 +89,16 @@ final class MessageFactory
} }
/** /**
* @return iterable<HasMethodInterface|InvalidInputMessageException> * Creates message objects from JSON input.
*
* Supports both single messages and batch requests. Returns an array containing
* MessageInterface objects or InvalidInputMessageException instances for invalid messages.
*
* @return array<MessageInterface|InvalidInputMessageException>
* *
* @throws \JsonException When the input string is not valid JSON * @throws \JsonException When the input string is not valid JSON
*/ */
public function create(string $input): iterable public function create(string $input): array
{ {
$data = json_decode($input, true, flags: \JSON_THROW_ON_ERROR); $data = json_decode($input, true, flags: \JSON_THROW_ON_ERROR);
@ -88,32 +106,63 @@ final class MessageFactory
$data = [$data]; $data = [$data];
} }
$messages = [];
foreach ($data as $message) { foreach ($data as $message) {
if (!isset($message['method']) || !\is_string($message['method'])) { try {
yield new InvalidInputMessageException('Invalid JSON-RPC request, missing valid "method".'); $messages[] = $this->createMessage($message);
continue; } catch (InvalidInputMessageException $e) {
$messages[] = $e;
}
}
return $messages;
}
/**
* Creates a single message object from parsed JSON data.
*
* @param array<string, mixed> $data
*
* @throws InvalidInputMessageException
*/
private function createMessage(array $data): MessageInterface
{
try {
if (isset($data['error'])) {
return Error::fromArray($data);
} }
try { if (isset($data['result'])) {
yield $this->getType($message['method'])::fromArray($message); return Response::fromArray($data);
} catch (InvalidInputMessageException $e) {
yield $e;
continue;
} }
if (!isset($data['method'])) {
throw new InvalidInputMessageException('Invalid JSON-RPC message: missing "method", "result", or "error" field.');
}
$messageClass = $this->findMessageClassByMethod($data['method']);
return $messageClass::fromArray($data);
} catch (InvalidArgumentException $e) {
throw new InvalidInputMessageException($e->getMessage(), 0, $e);
} }
} }
/** /**
* @return class-string<HasMethodInterface> * Finds the registered message class for a given method name.
*
* @return class-string<Request|Notification>
*
* @throws InvalidInputMessageException
*/ */
private function getType(string $method): string private function findMessageClassByMethod(string $method): string
{ {
foreach (self::REGISTERED_MESSAGES as $type) { foreach ($this->registeredMessages as $messageClass) {
if ($type::getMethod() === $method) { if ($messageClass::getMethod() === $method) {
return $type; return $messageClass;
} }
} }
throw new InvalidInputMessageException(\sprintf('Invalid JSON-RPC request, unknown method "%s".', $method)); throw new InvalidInputMessageException(\sprintf('Unknown method "%s".', $method));
} }
} }

View file

@ -57,17 +57,23 @@ class Error implements MessageInterface
if (!isset($data['jsonrpc']) || MessageInterface::JSONRPC_VERSION !== $data['jsonrpc']) { if (!isset($data['jsonrpc']) || MessageInterface::JSONRPC_VERSION !== $data['jsonrpc']) {
throw new InvalidArgumentException('Invalid or missing "jsonrpc" in Error data.'); throw new InvalidArgumentException('Invalid or missing "jsonrpc" in Error data.');
} }
if (!isset($data['id']) || !\is_string($data['id'])) { if (!isset($data['id'])) {
throw new InvalidArgumentException('Invalid or missing "id" in Error data.'); throw new InvalidArgumentException('Invalid or missing "id" in Error data.');
} }
if (!isset($data['code']) || !\is_int($data['code'])) { if (!\is_string($data['id']) && !\is_int($data['id'])) {
throw new InvalidArgumentException('Invalid "id" type in Error data.');
}
if (!isset($data['error']) || !\is_array($data['error'])) {
throw new InvalidArgumentException('Invalid or missing "error" field in Error data.');
}
if (!isset($data['error']['code']) || !\is_int($data['error']['code'])) {
throw new InvalidArgumentException('Invalid or missing "code" in Error data.'); throw new InvalidArgumentException('Invalid or missing "code" in Error data.');
} }
if (!isset($data['message']) || !\is_string($data['message'])) { if (!isset($data['error']['message']) || !\is_string($data['error']['message'])) {
throw new InvalidArgumentException('Invalid or missing "message" in Error data.'); throw new InvalidArgumentException('Invalid or missing "message" in Error data.');
} }
return new self($data['id'], $data['code'], $data['message'], $data['data'] ?? null); return new self($data['id'], $data['error']['code'], $data['error']['message'], $data['error']['data'] ?? null);
} }
public static function forParseError(string $message, string|int $id = ''): self public static function forParseError(string $message, string|int $id = ''): self

View file

@ -56,6 +56,9 @@ class Response implements MessageInterface
if (!isset($data['result'])) { if (!isset($data['result'])) {
throw new InvalidArgumentException('Response must contain "result" field.'); throw new InvalidArgumentException('Response must contain "result" field.');
} }
if (!\is_array($data['result'])) {
throw new InvalidArgumentException('Response "result" must be an array.');
}
return new self($data['id'], $data['result']); return new self($data['id'], $data['result']);
} }

View file

@ -12,11 +12,10 @@
namespace Mcp; namespace Mcp;
use Mcp\Server\Builder; use Mcp\Server\Builder;
use Mcp\Server\Handler\JsonRpcHandler; use Mcp\Server\Protocol;
use Mcp\Server\Transport\TransportInterface; use Mcp\Server\Transport\TransportInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger; use Psr\Log\NullLogger;
use Symfony\Component\Uid\Uuid;
/** /**
* @author Christopher Hertel <mail@christopher-hertel.de> * @author Christopher Hertel <mail@christopher-hertel.de>
@ -25,7 +24,7 @@ use Symfony\Component\Uid\Uuid;
final class Server final class Server
{ {
public function __construct( public function __construct(
private readonly JsonRpcHandler $jsonRpcHandler, private readonly Protocol $protocol,
private readonly LoggerInterface $logger = new NullLogger(), private readonly LoggerInterface $logger = new NullLogger(),
) { ) {
} }
@ -44,25 +43,11 @@ final class Server
*/ */
public function run(TransportInterface $transport): mixed public function run(TransportInterface $transport): mixed
{ {
$this->logger->info('Running server...');
$transport->initialize(); $transport->initialize();
$this->logger->info('Transport initialized.', [ $this->protocol->connect($transport);
'transport' => $transport::class,
]);
$transport->onMessage(function (string $message, ?Uuid $sessionId) use ($transport) {
foreach ($this->jsonRpcHandler->process($message, $sessionId) as [$response, $context]) {
if (null === $response) {
continue;
}
$transport->send($response, $context);
}
});
$transport->onSessionEnd(function (Uuid $sessionId) {
$this->jsonRpcHandler->destroySession($sessionId);
});
try { try {
return $transport->listen(); return $transport->listen();

View file

@ -36,8 +36,8 @@ use Mcp\Schema\ServerCapabilities;
use Mcp\Schema\Tool; use Mcp\Schema\Tool;
use Mcp\Schema\ToolAnnotations; use Mcp\Schema\ToolAnnotations;
use Mcp\Server; use Mcp\Server;
use Mcp\Server\Handler\JsonRpcHandler; use Mcp\Server\Handler\Notification\NotificationHandlerInterface;
use Mcp\Server\Handler\MethodHandlerInterface; use Mcp\Server\Handler\Request\RequestHandlerInterface;
use Mcp\Server\Session\InMemorySessionStore; use Mcp\Server\Session\InMemorySessionStore;
use Mcp\Server\Session\SessionFactory; use Mcp\Server\Session\SessionFactory;
use Mcp\Server\Session\SessionFactoryInterface; use Mcp\Server\Session\SessionFactoryInterface;
@ -76,9 +76,14 @@ final class Builder
private ?string $instructions = null; private ?string $instructions = null;
/** /**
* @var array<int, MethodHandlerInterface> * @var array<int, RequestHandlerInterface>
*/ */
private array $customMethodHandlers = []; private array $requestHandlers = [];
/**
* @var array<int, NotificationHandlerInterface>
*/
private array $notificationHandlers = [];
/** /**
* @var array{ * @var array{
@ -185,9 +190,9 @@ final class Builder
/** /**
* Register a single custom method handler. * Register a single custom method handler.
*/ */
public function addMethodHandler(MethodHandlerInterface $handler): self public function addRequestHandler(RequestHandlerInterface $handler): self
{ {
$this->customMethodHandlers[] = $handler; $this->requestHandlers[] = $handler;
return $this; return $this;
} }
@ -195,12 +200,36 @@ final class Builder
/** /**
* Register multiple custom method handlers. * Register multiple custom method handlers.
* *
* @param iterable<int, MethodHandlerInterface> $handlers * @param iterable<int, RequestHandlerInterface> $handlers
*/ */
public function addMethodHandlers(iterable $handlers): self public function addRequestHandlers(iterable $handlers): self
{ {
foreach ($handlers as $handler) { foreach ($handlers as $handler) {
$this->customMethodHandlers[] = $handler; $this->requestHandlers[] = $handler;
}
return $this;
}
/**
* Register a single custom notification handler.
*/
public function addNotificationHandler(NotificationHandlerInterface $handler): self
{
$this->notificationHandlers[] = $handler;
return $this;
}
/**
* Register multiple custom notification handlers.
*
* @param iterable<int, NotificationHandlerInterface> $handlers
*/
public function addNotificationHandlers(iterable $handlers): self
{
foreach ($handlers as $handler) {
$this->notificationHandlers[] = $handler;
} }
return $this; return $this;
@ -365,7 +394,7 @@ final class Builder
$configuration = new Configuration($this->serverInfo, $capabilities, $this->paginationLimit, $this->instructions); $configuration = new Configuration($this->serverInfo, $capabilities, $this->paginationLimit, $this->instructions);
$referenceHandler = new ReferenceHandler($container); $referenceHandler = new ReferenceHandler($container);
$methodHandlers = array_merge($this->customMethodHandlers, [ $requestHandlers = array_merge($this->requestHandlers, [
new Handler\Request\PingHandler(), new Handler\Request\PingHandler(),
new Handler\Request\InitializeHandler($configuration), new Handler\Request\InitializeHandler($configuration),
new Handler\Request\ListToolsHandler($registry, $this->paginationLimit), new Handler\Request\ListToolsHandler($registry, $this->paginationLimit),
@ -375,19 +404,22 @@ final class Builder
new Handler\Request\ReadResourceHandler($registry, $referenceHandler, $logger), new Handler\Request\ReadResourceHandler($registry, $referenceHandler, $logger),
new Handler\Request\ListPromptsHandler($registry, $this->paginationLimit), new Handler\Request\ListPromptsHandler($registry, $this->paginationLimit),
new Handler\Request\GetPromptHandler($registry, $referenceHandler, $logger), new Handler\Request\GetPromptHandler($registry, $referenceHandler, $logger),
]);
$notificationHandlers = array_merge($this->notificationHandlers, [
new Handler\Notification\InitializedHandler(), new Handler\Notification\InitializedHandler(),
]); ]);
$jsonRpcHandler = new JsonRpcHandler( $protocol = new Protocol(
methodHandlers: $methodHandlers, requestHandlers: $requestHandlers,
notificationHandlers: $notificationHandlers,
messageFactory: $messageFactory, messageFactory: $messageFactory,
sessionFactory: $sessionFactory, sessionFactory: $sessionFactory,
sessionStore: $sessionStore, sessionStore: $sessionStore,
logger: $logger, logger: $logger,
); );
return new Server($jsonRpcHandler, $logger); return new Server($protocol, $logger);
} }
private function performDiscovery( private function performDiscovery(

View file

@ -1,258 +0,0 @@
<?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\Exception\ExceptionInterface;
use Mcp\Exception\HandlerNotFoundException;
use Mcp\Exception\InvalidInputMessageException;
use Mcp\Exception\NotFoundExceptionInterface;
use Mcp\JsonRpc\MessageFactory;
use Mcp\Schema\JsonRpc\Error;
use Mcp\Schema\JsonRpc\HasMethodInterface;
use Mcp\Schema\JsonRpc\Request;
use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Request\InitializeRequest;
use Mcp\Server\Session\SessionFactoryInterface;
use Mcp\Server\Session\SessionInterface;
use Mcp\Server\Session\SessionStoreInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Component\Uid\Uuid;
/**
* @final
*
* @author Christopher Hertel <mail@christopher-hertel.de>
*/
class JsonRpcHandler
{
/**
* @param array<int, MethodHandlerInterface> $methodHandlers
*/
public function __construct(
private readonly array $methodHandlers,
private readonly MessageFactory $messageFactory,
private readonly SessionFactoryInterface $sessionFactory,
private readonly SessionStoreInterface $sessionStore,
private readonly LoggerInterface $logger = new NullLogger(),
) {
}
/**
* @return iterable<array{string|null, array<string, mixed>}>
*/
public function process(string $input, ?Uuid $sessionId): iterable
{
$this->logger->info('Received message to process.', ['message' => $input]);
$this->runGarbageCollection();
try {
$messages = iterator_to_array($this->messageFactory->create($input));
} catch (\JsonException $e) {
$this->logger->warning('Failed to decode json message.', ['exception' => $e]);
$error = Error::forParseError($e->getMessage());
yield [$this->encodeResponse($error), []];
return;
}
$hasInitializeRequest = false;
foreach ($messages as $message) {
if ($message instanceof InitializeRequest) {
$hasInitializeRequest = true;
break;
}
}
$session = null;
if ($hasInitializeRequest) {
// Spec: An initialize request must not be part of a batch.
if (\count($messages) > 1) {
$error = Error::forInvalidRequest('The "initialize" request MUST NOT be part of a batch.');
yield [$this->encodeResponse($error), []];
return;
}
// Spec: An initialize request must not have a session ID.
if ($sessionId) {
$error = Error::forInvalidRequest('A session ID MUST NOT be sent with an "initialize" request.');
yield [$this->encodeResponse($error), []];
return;
}
$session = $this->sessionFactory->create($this->sessionStore);
} else {
if (!$sessionId) {
$error = Error::forInvalidRequest('A valid session id is REQUIRED for non-initialize requests.');
yield [$this->encodeResponse($error), ['status_code' => 400]];
return;
}
if (!$this->sessionStore->exists($sessionId)) {
$error = Error::forInvalidRequest('Session not found or has expired.');
yield [$this->encodeResponse($error), ['status_code' => 404]];
return;
}
$session = $this->sessionFactory->createWithId($sessionId, $this->sessionStore);
}
foreach ($messages as $message) {
if ($message instanceof InvalidInputMessageException) {
$this->logger->warning('Failed to create message.', ['exception' => $message]);
$error = Error::forInvalidRequest($message->getMessage());
yield [$this->encodeResponse($error), []];
continue;
}
$this->logger->debug(\sprintf('Decoded incoming message "%s".', $message::class), [
'method' => $message->getMethod(),
]);
$messageId = $message instanceof Request ? $message->getId() : 0;
try {
$response = $this->handle($message, $session);
yield [$this->encodeResponse($response), ['session_id' => $session->getId()]];
} catch (\DomainException) {
yield [null, []];
} catch (NotFoundExceptionInterface $e) {
$this->logger->warning(
\sprintf('Failed to create response: %s', $e->getMessage()),
['exception' => $e],
);
$error = Error::forMethodNotFound($e->getMessage(), $messageId);
yield [$this->encodeResponse($error), []];
} catch (\InvalidArgumentException $e) {
$this->logger->warning(\sprintf('Invalid argument: %s', $e->getMessage()), ['exception' => $e]);
$error = Error::forInvalidParams($e->getMessage(), $messageId);
yield [$this->encodeResponse($error), []];
} catch (\Throwable $e) {
$this->logger->critical(\sprintf('Uncaught exception: %s', $e->getMessage()), ['exception' => $e]);
$error = Error::forInternalError($e->getMessage(), $messageId);
yield [$this->encodeResponse($error), []];
}
}
$session->save();
}
/**
* Encodes a response to JSON, handling encoding errors gracefully.
*/
private function encodeResponse(Response|Error|null $response): ?string
{
if (null === $response) {
$this->logger->info('The handler created an empty response.');
return null;
}
$this->logger->info('Encoding response.', ['response' => $response]);
try {
if ($response instanceof Response && [] === $response->result) {
return json_encode($response, \JSON_THROW_ON_ERROR | \JSON_FORCE_OBJECT);
}
return json_encode($response, \JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
$this->logger->error('Failed to encode response to JSON.', [
'message_id' => $response->getId(),
'exception' => $e,
]);
$fallbackError = new Error(
id: $response->getId(),
code: Error::INTERNAL_ERROR,
message: 'Response could not be encoded to JSON'
);
return json_encode($fallbackError, \JSON_THROW_ON_ERROR);
}
}
/**
* If the handler does support the message, but does not create a response, other handlers will be tried.
*
* @throws NotFoundExceptionInterface When no handler is found for the request method
* @throws ExceptionInterface When a request handler throws an exception
*/
private function handle(HasMethodInterface $message, SessionInterface $session): Response|Error|null
{
$this->logger->info(\sprintf('Handling message for method "%s".', $message::getMethod()), [
'message' => $message,
]);
$handled = false;
foreach ($this->methodHandlers as $handler) {
if (!$handler->supports($message)) {
continue;
}
$return = $handler->handle($message, $session);
$handled = true;
$this->logger->debug(\sprintf('Message handled by "%s".', $handler::class), [
'method' => $message::getMethod(),
'response' => $return,
]);
if (null !== $return) {
return $return;
}
}
if ($handled) {
return null;
}
throw new HandlerNotFoundException(\sprintf('No handler found for method "%s".', $message::getMethod()));
}
/**
* Run garbage collection on expired sessions.
* Uses the session store's internal TTL configuration.
*/
private function runGarbageCollection(): void
{
if (random_int(0, 100) > 1) {
return;
}
$deletedSessions = $this->sessionStore->gc();
if (!empty($deletedSessions)) {
$this->logger->debug('Garbage collected expired sessions.', [
'count' => \count($deletedSessions),
'session_ids' => array_map(fn (Uuid $id) => $id->toRfc4122(), $deletedSessions),
]);
}
}
/**
* Destroy a specific session.
*/
public function destroySession(Uuid $sessionId): void
{
$this->sessionStore->destroy($sessionId);
$this->logger->info('Session destroyed.', ['session_id' => $sessionId->toRfc4122()]);
}
}

View file

@ -1,32 +0,0 @@
<?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\Exception\ExceptionInterface;
use Mcp\Schema\JsonRpc\Error;
use Mcp\Schema\JsonRpc\HasMethodInterface;
use Mcp\Schema\JsonRpc\Request;
use Mcp\Schema\JsonRpc\Response;
use Mcp\Server\Session\SessionInterface;
/**
* @author Christopher Hertel <mail@christopher-hertel.de>
*/
interface MethodHandlerInterface
{
public function supports(HasMethodInterface $message): bool;
/**
* @throws ExceptionInterface When the handler encounters an error processing the request
*/
public function handle(HasMethodInterface $message, SessionInterface $session): Response|Error|null;
}

View file

@ -11,27 +11,24 @@
namespace Mcp\Server\Handler\Notification; namespace Mcp\Server\Handler\Notification;
use Mcp\Schema\JsonRpc\Error; use Mcp\Schema\JsonRpc\Notification;
use Mcp\Schema\JsonRpc\HasMethodInterface;
use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Notification\InitializedNotification; use Mcp\Schema\Notification\InitializedNotification;
use Mcp\Server\Handler\MethodHandlerInterface;
use Mcp\Server\Session\SessionInterface; use Mcp\Server\Session\SessionInterface;
/** /**
* @author Christopher Hertel <mail@christopher-hertel.de> * @author Christopher Hertel <mail@christopher-hertel.de>
*/ */
final class InitializedHandler implements MethodHandlerInterface final class InitializedHandler implements NotificationHandlerInterface
{ {
public function supports(HasMethodInterface $message): bool public function supports(Notification $notification): bool
{ {
return $message instanceof InitializedNotification; return $notification instanceof InitializedNotification;
} }
public function handle(InitializedNotification|HasMethodInterface $message, SessionInterface $session): Response|Error|null public function handle(Notification $message, SessionInterface $session): void
{ {
$session->set('initialized', true); \assert($message instanceof InitializedNotification);
return null; $session->set('initialized', true);
} }
} }

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\Notification;
use Mcp\Schema\JsonRpc\Notification;
use Mcp\Server\Session\SessionInterface;
/**
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
*/
interface NotificationHandlerInterface
{
public function supports(Notification $notification): bool;
public function handle(Notification $notification, SessionInterface $session): void;
}

View file

@ -17,11 +17,10 @@ use Mcp\Exception\ExceptionInterface;
use Mcp\Exception\ToolCallException; use Mcp\Exception\ToolCallException;
use Mcp\Exception\ToolNotFoundException; use Mcp\Exception\ToolNotFoundException;
use Mcp\Schema\JsonRpc\Error; use Mcp\Schema\JsonRpc\Error;
use Mcp\Schema\JsonRpc\HasMethodInterface; use Mcp\Schema\JsonRpc\Request;
use Mcp\Schema\JsonRpc\Response; use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Request\CallToolRequest; use Mcp\Schema\Request\CallToolRequest;
use Mcp\Schema\Result\CallToolResult; use Mcp\Schema\Result\CallToolResult;
use Mcp\Server\Handler\MethodHandlerInterface;
use Mcp\Server\Session\SessionInterface; use Mcp\Server\Session\SessionInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger; use Psr\Log\NullLogger;
@ -30,7 +29,7 @@ use Psr\Log\NullLogger;
* @author Christopher Hertel <mail@christopher-hertel.de> * @author Christopher Hertel <mail@christopher-hertel.de>
* @author Tobias Nyholm <tobias.nyholm@gmail.com> * @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/ */
final class CallToolHandler implements MethodHandlerInterface final class CallToolHandler implements RequestHandlerInterface
{ {
public function __construct( public function __construct(
private readonly ReferenceProviderInterface $referenceProvider, private readonly ReferenceProviderInterface $referenceProvider,
@ -39,24 +38,24 @@ final class CallToolHandler implements MethodHandlerInterface
) { ) {
} }
public function supports(HasMethodInterface $message): bool public function supports(Request $request): bool
{ {
return $message instanceof CallToolRequest; return $request instanceof CallToolRequest;
} }
public function handle(CallToolRequest|HasMethodInterface $message, SessionInterface $session): Response|Error public function handle(Request $request, SessionInterface $session): Response|Error
{ {
\assert($message instanceof CallToolRequest); \assert($request instanceof CallToolRequest);
$toolName = $message->name; $toolName = $request->name;
$arguments = $message->arguments ?? []; $arguments = $request->arguments ?? [];
$this->logger->debug('Executing tool', ['name' => $toolName, 'arguments' => $arguments]); $this->logger->debug('Executing tool', ['name' => $toolName, 'arguments' => $arguments]);
try { try {
$reference = $this->referenceProvider->getTool($toolName); $reference = $this->referenceProvider->getTool($toolName);
if (null === $reference) { if (null === $reference) {
throw new ToolNotFoundException($message); throw new ToolNotFoundException($request);
} }
$result = $this->referenceHandler->handle($reference, $arguments); $result = $this->referenceHandler->handle($reference, $arguments);
@ -67,25 +66,25 @@ final class CallToolHandler implements MethodHandlerInterface
'result_type' => \gettype($result), 'result_type' => \gettype($result),
]); ]);
return new Response($message->getId(), new CallToolResult($formatted)); return new Response($request->getId(), new CallToolResult($formatted));
} catch (ToolNotFoundException $e) { } catch (ToolNotFoundException $e) {
$this->logger->error('Tool not found', ['name' => $toolName]); $this->logger->error('Tool not found', ['name' => $toolName]);
return new Error($message->getId(), Error::METHOD_NOT_FOUND, $e->getMessage()); return new Error($request->getId(), Error::METHOD_NOT_FOUND, $e->getMessage());
} catch (ToolCallException|ExceptionInterface $e) { } catch (ToolCallException|ExceptionInterface $e) {
$this->logger->error(\sprintf('Error while executing tool "%s": "%s".', $toolName, $e->getMessage()), [ $this->logger->error(\sprintf('Error while executing tool "%s": "%s".', $toolName, $e->getMessage()), [
'tool' => $toolName, 'tool' => $toolName,
'arguments' => $arguments, 'arguments' => $arguments,
]); ]);
return Error::forInternalError('Error while executing tool', $message->getId()); return Error::forInternalError('Error while executing tool', $request->getId());
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->logger->error('Unhandled error during tool execution', [ $this->logger->error('Unhandled error during tool execution', [
'name' => $toolName, 'name' => $toolName,
'exception' => $e->getMessage(), 'exception' => $e->getMessage(),
]); ]);
return Error::forInternalError('Error while executing tool', $message->getId()); return Error::forInternalError('Error while executing tool', $request->getId());
} }
} }
} }

View file

@ -14,11 +14,10 @@ namespace Mcp\Server\Handler\Request;
use Mcp\Capability\Completion\ProviderInterface; use Mcp\Capability\Completion\ProviderInterface;
use Mcp\Capability\Registry\ReferenceProviderInterface; use Mcp\Capability\Registry\ReferenceProviderInterface;
use Mcp\Schema\JsonRpc\Error; use Mcp\Schema\JsonRpc\Error;
use Mcp\Schema\JsonRpc\HasMethodInterface; use Mcp\Schema\JsonRpc\Request;
use Mcp\Schema\JsonRpc\Response; use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Request\CompletionCompleteRequest; use Mcp\Schema\Request\CompletionCompleteRequest;
use Mcp\Schema\Result\CompletionCompleteResult; use Mcp\Schema\Result\CompletionCompleteResult;
use Mcp\Server\Handler\MethodHandlerInterface;
use Mcp\Server\Session\SessionInterface; use Mcp\Server\Session\SessionInterface;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
@ -27,7 +26,7 @@ use Psr\Container\ContainerInterface;
* *
* @author Kyrian Obikwelu <koshnawaza@gmail.com> * @author Kyrian Obikwelu <koshnawaza@gmail.com>
*/ */
final class CompletionCompleteHandler implements MethodHandlerInterface final class CompletionCompleteHandler implements RequestHandlerInterface
{ {
public function __construct( public function __construct(
private readonly ReferenceProviderInterface $referenceProvider, private readonly ReferenceProviderInterface $referenceProvider,
@ -35,43 +34,43 @@ final class CompletionCompleteHandler implements MethodHandlerInterface
) { ) {
} }
public function supports(HasMethodInterface $message): bool public function supports(Request $request): bool
{ {
return $message instanceof CompletionCompleteRequest; return $request instanceof CompletionCompleteRequest;
} }
public function handle(CompletionCompleteRequest|HasMethodInterface $message, SessionInterface $session): Response|Error public function handle(Request $request, SessionInterface $session): Response|Error
{ {
\assert($message instanceof CompletionCompleteRequest); \assert($request instanceof CompletionCompleteRequest);
$name = $message->argument['name'] ?? ''; $name = $request->argument['name'] ?? '';
$value = $message->argument['value'] ?? ''; $value = $request->argument['value'] ?? '';
$reference = match ($message->ref->type) { $reference = match ($request->ref->type) {
'ref/prompt' => $this->referenceProvider->getPrompt($message->ref->name), 'ref/prompt' => $this->referenceProvider->getPrompt($request->ref->name),
'ref/resource' => $this->referenceProvider->getResourceTemplate($message->ref->uri), 'ref/resource' => $this->referenceProvider->getResourceTemplate($request->ref->uri),
default => null, default => null,
}; };
if (null === $reference) { if (null === $reference) {
return new Response($message->getId(), new CompletionCompleteResult([])); return new Response($request->getId(), new CompletionCompleteResult([]));
} }
$providers = $reference->completionProviders; $providers = $reference->completionProviders;
$provider = $providers[$name] ?? null; $provider = $providers[$name] ?? null;
if (null === $provider) { if (null === $provider) {
return new Response($message->getId(), new CompletionCompleteResult([])); return new Response($request->getId(), new CompletionCompleteResult([]));
} }
if (\is_string($provider)) { if (\is_string($provider)) {
if (!class_exists($provider)) { if (!class_exists($provider)) {
return Error::forInternalError('Invalid completion provider', $message->getId()); return Error::forInternalError('Invalid completion provider', $request->getId());
} }
$provider = $this->container?->has($provider) ? $this->container->get($provider) : new $provider(); $provider = $this->container?->has($provider) ? $this->container->get($provider) : new $provider();
} }
if (!$provider instanceof ProviderInterface) { if (!$provider instanceof ProviderInterface) {
return Error::forInternalError('Invalid completion provider type', $message->getId()); return Error::forInternalError('Invalid completion provider type', $request->getId());
} }
try { try {
@ -80,9 +79,9 @@ final class CompletionCompleteHandler implements MethodHandlerInterface
$hasMore = $total > 100; $hasMore = $total > 100;
$paged = \array_slice($completions, 0, 100); $paged = \array_slice($completions, 0, 100);
return new Response($message->getId(), new CompletionCompleteResult($paged, $total, $hasMore)); return new Response($request->getId(), new CompletionCompleteResult($paged, $total, $hasMore));
} catch (\Throwable) { } catch (\Throwable) {
return Error::forInternalError('Error while handling completion request', $message->getId()); return Error::forInternalError('Error while handling completion request', $request->getId());
} }
} }
} }

View file

@ -17,11 +17,10 @@ use Mcp\Exception\ExceptionInterface;
use Mcp\Exception\PromptGetException; use Mcp\Exception\PromptGetException;
use Mcp\Exception\PromptNotFoundException; use Mcp\Exception\PromptNotFoundException;
use Mcp\Schema\JsonRpc\Error; use Mcp\Schema\JsonRpc\Error;
use Mcp\Schema\JsonRpc\HasMethodInterface; use Mcp\Schema\JsonRpc\Request;
use Mcp\Schema\JsonRpc\Response; use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Request\GetPromptRequest; use Mcp\Schema\Request\GetPromptRequest;
use Mcp\Schema\Result\GetPromptResult; use Mcp\Schema\Result\GetPromptResult;
use Mcp\Server\Handler\MethodHandlerInterface;
use Mcp\Server\Session\SessionInterface; use Mcp\Server\Session\SessionInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger; use Psr\Log\NullLogger;
@ -29,7 +28,7 @@ use Psr\Log\NullLogger;
/** /**
* @author Tobias Nyholm <tobias.nyholm@gmail.com> * @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/ */
final class GetPromptHandler implements MethodHandlerInterface final class GetPromptHandler implements RequestHandlerInterface
{ {
public function __construct( public function __construct(
private readonly ReferenceProviderInterface $referenceProvider, private readonly ReferenceProviderInterface $referenceProvider,
@ -38,41 +37,41 @@ final class GetPromptHandler implements MethodHandlerInterface
) { ) {
} }
public function supports(HasMethodInterface $message): bool public function supports(Request $request): bool
{ {
return $message instanceof GetPromptRequest; return $request instanceof GetPromptRequest;
} }
public function handle(GetPromptRequest|HasMethodInterface $message, SessionInterface $session): Response|Error public function handle(Request $request, SessionInterface $session): Response|Error
{ {
\assert($message instanceof GetPromptRequest); \assert($request instanceof GetPromptRequest);
$promptName = $message->name; $promptName = $request->name;
$arguments = $message->arguments ?? []; $arguments = $request->arguments ?? [];
try { try {
$reference = $this->referenceProvider->getPrompt($promptName); $reference = $this->referenceProvider->getPrompt($promptName);
if (null === $reference) { if (null === $reference) {
throw new PromptNotFoundException($message); throw new PromptNotFoundException($request);
} }
$result = $this->referenceHandler->handle($reference, $arguments); $result = $this->referenceHandler->handle($reference, $arguments);
$formatted = $reference->formatResult($result); $formatted = $reference->formatResult($result);
return new Response($message->getId(), new GetPromptResult($formatted)); return new Response($request->getId(), new GetPromptResult($formatted));
} catch (PromptNotFoundException $e) { } catch (PromptNotFoundException $e) {
$this->logger->error('Prompt not found', ['prompt_name' => $promptName]); $this->logger->error('Prompt not found', ['prompt_name' => $promptName]);
return new Error($message->getId(), Error::METHOD_NOT_FOUND, $e->getMessage()); return new Error($request->getId(), Error::METHOD_NOT_FOUND, $e->getMessage());
} catch (PromptGetException|ExceptionInterface $e) { } catch (PromptGetException|ExceptionInterface $e) {
$this->logger->error('Error while handling prompt', ['prompt_name' => $promptName]); $this->logger->error('Error while handling prompt', ['prompt_name' => $promptName]);
return Error::forInternalError('Error while handling prompt', $message->getId()); return Error::forInternalError('Error while handling prompt', $request->getId());
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->logger->error('Error while handling prompt', ['prompt_name' => $promptName]); $this->logger->error('Error while handling prompt', ['prompt_name' => $promptName]);
return Error::forInternalError('Error while handling prompt', $message->getId()); return Error::forInternalError('Error while handling prompt', $request->getId());
} }
} }
} }

View file

@ -12,38 +12,37 @@
namespace Mcp\Server\Handler\Request; namespace Mcp\Server\Handler\Request;
use Mcp\Schema\Implementation; use Mcp\Schema\Implementation;
use Mcp\Schema\JsonRpc\HasMethodInterface; use Mcp\Schema\JsonRpc\Request;
use Mcp\Schema\JsonRpc\Response; use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Request\InitializeRequest; use Mcp\Schema\Request\InitializeRequest;
use Mcp\Schema\Result\InitializeResult; use Mcp\Schema\Result\InitializeResult;
use Mcp\Schema\ServerCapabilities; use Mcp\Schema\ServerCapabilities;
use Mcp\Server\Configuration; use Mcp\Server\Configuration;
use Mcp\Server\Handler\MethodHandlerInterface;
use Mcp\Server\Session\SessionInterface; use Mcp\Server\Session\SessionInterface;
/** /**
* @author Christopher Hertel <mail@christopher-hertel.de> * @author Christopher Hertel <mail@christopher-hertel.de>
*/ */
final class InitializeHandler implements MethodHandlerInterface final class InitializeHandler implements RequestHandlerInterface
{ {
public function __construct( public function __construct(
public readonly ?Configuration $configuration = null, public readonly ?Configuration $configuration = null,
) { ) {
} }
public function supports(HasMethodInterface $message): bool public function supports(Request $request): bool
{ {
return $message instanceof InitializeRequest; return $request instanceof InitializeRequest;
} }
public function handle(InitializeRequest|HasMethodInterface $message, SessionInterface $session): Response public function handle(Request $request, SessionInterface $session): Response
{ {
\assert($message instanceof InitializeRequest); \assert($request instanceof InitializeRequest);
$session->set('client_info', $message->clientInfo->jsonSerialize()); $session->set('client_info', $request->clientInfo->jsonSerialize());
return new Response( return new Response(
$message->getId(), $request->getId(),
new InitializeResult( new InitializeResult(
$this->configuration->capabilities ?? new ServerCapabilities(), $this->configuration->capabilities ?? new ServerCapabilities(),
$this->configuration->serverInfo ?? new Implementation(), $this->configuration->serverInfo ?? new Implementation(),

View file

@ -13,17 +13,16 @@ namespace Mcp\Server\Handler\Request;
use Mcp\Capability\Registry\ReferenceProviderInterface; use Mcp\Capability\Registry\ReferenceProviderInterface;
use Mcp\Exception\InvalidCursorException; use Mcp\Exception\InvalidCursorException;
use Mcp\Schema\JsonRpc\HasMethodInterface; use Mcp\Schema\JsonRpc\Request;
use Mcp\Schema\JsonRpc\Response; use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Request\ListPromptsRequest; use Mcp\Schema\Request\ListPromptsRequest;
use Mcp\Schema\Result\ListPromptsResult; use Mcp\Schema\Result\ListPromptsResult;
use Mcp\Server\Handler\MethodHandlerInterface;
use Mcp\Server\Session\SessionInterface; use Mcp\Server\Session\SessionInterface;
/** /**
* @author Tobias Nyholm <tobias.nyholm@gmail.com> * @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/ */
final class ListPromptsHandler implements MethodHandlerInterface final class ListPromptsHandler implements RequestHandlerInterface
{ {
public function __construct( public function __construct(
private readonly ReferenceProviderInterface $registry, private readonly ReferenceProviderInterface $registry,
@ -31,22 +30,22 @@ final class ListPromptsHandler implements MethodHandlerInterface
) { ) {
} }
public function supports(HasMethodInterface $message): bool public function supports(Request $request): bool
{ {
return $message instanceof ListPromptsRequest; return $request instanceof ListPromptsRequest;
} }
/** /**
* @throws InvalidCursorException * @throws InvalidCursorException
*/ */
public function handle(ListPromptsRequest|HasMethodInterface $message, SessionInterface $session): Response public function handle(Request $request, SessionInterface $session): Response
{ {
\assert($message instanceof ListPromptsRequest); \assert($request instanceof ListPromptsRequest);
$page = $this->registry->getPrompts($this->pageSize, $message->cursor); $page = $this->registry->getPrompts($this->pageSize, $request->cursor);
return new Response( return new Response(
$message->getId(), $request->getId(),
new ListPromptsResult($page->references, $page->nextCursor), new ListPromptsResult($page->references, $page->nextCursor),
); );
} }

View file

@ -13,17 +13,16 @@ namespace Mcp\Server\Handler\Request;
use Mcp\Capability\Registry\ReferenceProviderInterface; use Mcp\Capability\Registry\ReferenceProviderInterface;
use Mcp\Exception\InvalidCursorException; use Mcp\Exception\InvalidCursorException;
use Mcp\Schema\JsonRpc\HasMethodInterface; use Mcp\Schema\JsonRpc\Request;
use Mcp\Schema\JsonRpc\Response; use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Request\ListResourceTemplatesRequest; use Mcp\Schema\Request\ListResourceTemplatesRequest;
use Mcp\Schema\Result\ListResourceTemplatesResult; use Mcp\Schema\Result\ListResourceTemplatesResult;
use Mcp\Server\Handler\MethodHandlerInterface;
use Mcp\Server\Session\SessionInterface; use Mcp\Server\Session\SessionInterface;
/** /**
* @author Christopher Hertel <mail@christopher-hertel.de> * @author Christopher Hertel <mail@christopher-hertel.de>
*/ */
final class ListResourceTemplatesHandler implements MethodHandlerInterface final class ListResourceTemplatesHandler implements RequestHandlerInterface
{ {
public function __construct( public function __construct(
private readonly ReferenceProviderInterface $registry, private readonly ReferenceProviderInterface $registry,
@ -31,22 +30,22 @@ final class ListResourceTemplatesHandler implements MethodHandlerInterface
) { ) {
} }
public function supports(HasMethodInterface $message): bool public function supports(Request $request): bool
{ {
return $message instanceof ListResourceTemplatesRequest; return $request instanceof ListResourceTemplatesRequest;
} }
/** /**
* @throws InvalidCursorException * @throws InvalidCursorException
*/ */
public function handle(ListResourceTemplatesRequest|HasMethodInterface $message, SessionInterface $session): Response public function handle(Request $request, SessionInterface $session): Response
{ {
\assert($message instanceof ListResourceTemplatesRequest); \assert($request instanceof ListResourceTemplatesRequest);
$page = $this->registry->getResourceTemplates($this->pageSize, $message->cursor); $page = $this->registry->getResourceTemplates($this->pageSize, $request->cursor);
return new Response( return new Response(
$message->getId(), $request->getId(),
new ListResourceTemplatesResult($page->references, $page->nextCursor), new ListResourceTemplatesResult($page->references, $page->nextCursor),
); );
} }

View file

@ -13,17 +13,16 @@ namespace Mcp\Server\Handler\Request;
use Mcp\Capability\Registry\ReferenceProviderInterface; use Mcp\Capability\Registry\ReferenceProviderInterface;
use Mcp\Exception\InvalidCursorException; use Mcp\Exception\InvalidCursorException;
use Mcp\Schema\JsonRpc\HasMethodInterface; use Mcp\Schema\JsonRpc\Request;
use Mcp\Schema\JsonRpc\Response; use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Request\ListResourcesRequest; use Mcp\Schema\Request\ListResourcesRequest;
use Mcp\Schema\Result\ListResourcesResult; use Mcp\Schema\Result\ListResourcesResult;
use Mcp\Server\Handler\MethodHandlerInterface;
use Mcp\Server\Session\SessionInterface; use Mcp\Server\Session\SessionInterface;
/** /**
* @author Tobias Nyholm <tobias.nyholm@gmail.com> * @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/ */
final class ListResourcesHandler implements MethodHandlerInterface final class ListResourcesHandler implements RequestHandlerInterface
{ {
public function __construct( public function __construct(
private readonly ReferenceProviderInterface $registry, private readonly ReferenceProviderInterface $registry,
@ -31,22 +30,22 @@ final class ListResourcesHandler implements MethodHandlerInterface
) { ) {
} }
public function supports(HasMethodInterface $message): bool public function supports(Request $request): bool
{ {
return $message instanceof ListResourcesRequest; return $request instanceof ListResourcesRequest;
} }
/** /**
* @throws InvalidCursorException * @throws InvalidCursorException
*/ */
public function handle(ListResourcesRequest|HasMethodInterface $message, SessionInterface $session): Response public function handle(Request $request, SessionInterface $session): Response
{ {
\assert($message instanceof ListResourcesRequest); \assert($request instanceof ListResourcesRequest);
$page = $this->registry->getResources($this->pageSize, $message->cursor); $page = $this->registry->getResources($this->pageSize, $request->cursor);
return new Response( return new Response(
$message->getId(), $request->getId(),
new ListResourcesResult($page->references, $page->nextCursor), new ListResourcesResult($page->references, $page->nextCursor),
); );
} }

View file

@ -13,18 +13,17 @@ namespace Mcp\Server\Handler\Request;
use Mcp\Capability\Registry\ReferenceProviderInterface; use Mcp\Capability\Registry\ReferenceProviderInterface;
use Mcp\Exception\InvalidCursorException; use Mcp\Exception\InvalidCursorException;
use Mcp\Schema\JsonRpc\HasMethodInterface; use Mcp\Schema\JsonRpc\Request;
use Mcp\Schema\JsonRpc\Response; use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Request\ListToolsRequest; use Mcp\Schema\Request\ListToolsRequest;
use Mcp\Schema\Result\ListToolsResult; use Mcp\Schema\Result\ListToolsResult;
use Mcp\Server\Handler\MethodHandlerInterface;
use Mcp\Server\Session\SessionInterface; use Mcp\Server\Session\SessionInterface;
/** /**
* @author Christopher Hertel <mail@christopher-hertel.de> * @author Christopher Hertel <mail@christopher-hertel.de>
* @author Tobias Nyholm <tobias.nyholm@gmail.com> * @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/ */
final class ListToolsHandler implements MethodHandlerInterface final class ListToolsHandler implements RequestHandlerInterface
{ {
public function __construct( public function __construct(
private readonly ReferenceProviderInterface $registry, private readonly ReferenceProviderInterface $registry,
@ -32,22 +31,22 @@ final class ListToolsHandler implements MethodHandlerInterface
) { ) {
} }
public function supports(HasMethodInterface $message): bool public function supports(Request $request): bool
{ {
return $message instanceof ListToolsRequest; return $request instanceof ListToolsRequest;
} }
/** /**
* @throws InvalidCursorException When the cursor is invalid * @throws InvalidCursorException When the cursor is invalid
*/ */
public function handle(ListToolsRequest|HasMethodInterface $message, SessionInterface $session): Response public function handle(Request $request, SessionInterface $session): Response
{ {
\assert($message instanceof ListToolsRequest); \assert($request instanceof ListToolsRequest);
$page = $this->registry->getTools($this->pageSize, $message->cursor); $page = $this->registry->getTools($this->pageSize, $request->cursor);
return new Response( return new Response(
$message->getId(), $request->getId(),
new ListToolsResult($page->references, $page->nextCursor), new ListToolsResult($page->references, $page->nextCursor),
); );
} }

View file

@ -11,27 +11,26 @@
namespace Mcp\Server\Handler\Request; namespace Mcp\Server\Handler\Request;
use Mcp\Schema\JsonRpc\HasMethodInterface; use Mcp\Schema\JsonRpc\Request;
use Mcp\Schema\JsonRpc\Response; use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Request\PingRequest; use Mcp\Schema\Request\PingRequest;
use Mcp\Schema\Result\EmptyResult; use Mcp\Schema\Result\EmptyResult;
use Mcp\Server\Handler\MethodHandlerInterface;
use Mcp\Server\Session\SessionInterface; use Mcp\Server\Session\SessionInterface;
/** /**
* @author Christopher Hertel <mail@christopher-hertel.de> * @author Christopher Hertel <mail@christopher-hertel.de>
*/ */
final class PingHandler implements MethodHandlerInterface final class PingHandler implements RequestHandlerInterface
{ {
public function supports(HasMethodInterface $message): bool public function supports(Request $request): bool
{ {
return $message instanceof PingRequest; return $request instanceof PingRequest;
} }
public function handle(PingRequest|HasMethodInterface $message, SessionInterface $session): Response public function handle(Request $request, SessionInterface $session): Response
{ {
\assert($message instanceof PingRequest); \assert($request instanceof PingRequest);
return new Response($message->getId(), new EmptyResult()); return new Response($request->getId(), new EmptyResult());
} }
} }

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