[Server] Refactor protocol pipeline to separate request and notification handling (#106)
This commit is contained in:
parent
9ff6516e5b
commit
54a5d1495c
18 changed files with 1843 additions and 719 deletions
|
|
@ -36,8 +36,8 @@ use Mcp\Schema\ServerCapabilities;
|
|||
use Mcp\Schema\Tool;
|
||||
use Mcp\Schema\ToolAnnotations;
|
||||
use Mcp\Server;
|
||||
use Mcp\Server\Handler\JsonRpcHandler;
|
||||
use Mcp\Server\Handler\MethodHandlerInterface;
|
||||
use Mcp\Server\Handler\Notification\NotificationHandlerInterface;
|
||||
use Mcp\Server\Handler\Request\RequestHandlerInterface;
|
||||
use Mcp\Server\Session\InMemorySessionStore;
|
||||
use Mcp\Server\Session\SessionFactory;
|
||||
use Mcp\Server\Session\SessionFactoryInterface;
|
||||
|
|
@ -76,9 +76,14 @@ final class Builder
|
|||
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{
|
||||
|
|
@ -185,9 +190,9 @@ final class Builder
|
|||
/**
|
||||
* 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;
|
||||
}
|
||||
|
|
@ -195,12 +200,36 @@ final class Builder
|
|||
/**
|
||||
* 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) {
|
||||
$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;
|
||||
|
|
@ -365,7 +394,7 @@ final class Builder
|
|||
$configuration = new Configuration($this->serverInfo, $capabilities, $this->paginationLimit, $this->instructions);
|
||||
$referenceHandler = new ReferenceHandler($container);
|
||||
|
||||
$methodHandlers = array_merge($this->customMethodHandlers, [
|
||||
$requestHandlers = array_merge($this->requestHandlers, [
|
||||
new Handler\Request\PingHandler(),
|
||||
new Handler\Request\InitializeHandler($configuration),
|
||||
new Handler\Request\ListToolsHandler($registry, $this->paginationLimit),
|
||||
|
|
@ -375,19 +404,22 @@ final class Builder
|
|||
new Handler\Request\ReadResourceHandler($registry, $referenceHandler, $logger),
|
||||
new Handler\Request\ListPromptsHandler($registry, $this->paginationLimit),
|
||||
new Handler\Request\GetPromptHandler($registry, $referenceHandler, $logger),
|
||||
]);
|
||||
|
||||
$notificationHandlers = array_merge($this->notificationHandlers, [
|
||||
new Handler\Notification\InitializedHandler(),
|
||||
]);
|
||||
|
||||
$jsonRpcHandler = new JsonRpcHandler(
|
||||
methodHandlers: $methodHandlers,
|
||||
$protocol = new Protocol(
|
||||
requestHandlers: $requestHandlers,
|
||||
notificationHandlers: $notificationHandlers,
|
||||
messageFactory: $messageFactory,
|
||||
sessionFactory: $sessionFactory,
|
||||
sessionStore: $sessionStore,
|
||||
logger: $logger,
|
||||
);
|
||||
|
||||
return new Server($jsonRpcHandler, $logger);
|
||||
return new Server($protocol, $logger);
|
||||
}
|
||||
|
||||
private function performDiscovery(
|
||||
|
|
|
|||
|
|
@ -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()]);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -11,27 +11,24 @@
|
|||
|
||||
namespace Mcp\Server\Handler\Notification;
|
||||
|
||||
use Mcp\Schema\JsonRpc\Error;
|
||||
use Mcp\Schema\JsonRpc\HasMethodInterface;
|
||||
use Mcp\Schema\JsonRpc\Response;
|
||||
use Mcp\Schema\JsonRpc\Notification;
|
||||
use Mcp\Schema\Notification\InitializedNotification;
|
||||
use Mcp\Server\Handler\MethodHandlerInterface;
|
||||
use Mcp\Server\Session\SessionInterface;
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -17,11 +17,10 @@ use Mcp\Exception\ExceptionInterface;
|
|||
use Mcp\Exception\ToolCallException;
|
||||
use Mcp\Exception\ToolNotFoundException;
|
||||
use Mcp\Schema\JsonRpc\Error;
|
||||
use Mcp\Schema\JsonRpc\HasMethodInterface;
|
||||
use Mcp\Schema\JsonRpc\Request;
|
||||
use Mcp\Schema\JsonRpc\Response;
|
||||
use Mcp\Schema\Request\CallToolRequest;
|
||||
use Mcp\Schema\Result\CallToolResult;
|
||||
use Mcp\Server\Handler\MethodHandlerInterface;
|
||||
use Mcp\Server\Session\SessionInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
|
|
@ -30,7 +29,7 @@ use Psr\Log\NullLogger;
|
|||
* @author Christopher Hertel <mail@christopher-hertel.de>
|
||||
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
||||
*/
|
||||
final class CallToolHandler implements MethodHandlerInterface
|
||||
final class CallToolHandler implements RequestHandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
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;
|
||||
$arguments = $message->arguments ?? [];
|
||||
$toolName = $request->name;
|
||||
$arguments = $request->arguments ?? [];
|
||||
|
||||
$this->logger->debug('Executing tool', ['name' => $toolName, 'arguments' => $arguments]);
|
||||
|
||||
try {
|
||||
$reference = $this->referenceProvider->getTool($toolName);
|
||||
if (null === $reference) {
|
||||
throw new ToolNotFoundException($message);
|
||||
throw new ToolNotFoundException($request);
|
||||
}
|
||||
|
||||
$result = $this->referenceHandler->handle($reference, $arguments);
|
||||
|
|
@ -67,25 +66,25 @@ final class CallToolHandler implements MethodHandlerInterface
|
|||
'result_type' => \gettype($result),
|
||||
]);
|
||||
|
||||
return new Response($message->getId(), new CallToolResult($formatted));
|
||||
return new Response($request->getId(), new CallToolResult($formatted));
|
||||
} catch (ToolNotFoundException $e) {
|
||||
$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) {
|
||||
$this->logger->error(\sprintf('Error while executing tool "%s": "%s".', $toolName, $e->getMessage()), [
|
||||
'tool' => $toolName,
|
||||
'arguments' => $arguments,
|
||||
]);
|
||||
|
||||
return Error::forInternalError('Error while executing tool', $message->getId());
|
||||
return Error::forInternalError('Error while executing tool', $request->getId());
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error('Unhandled error during tool execution', [
|
||||
'name' => $toolName,
|
||||
'exception' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return Error::forInternalError('Error while executing tool', $message->getId());
|
||||
return Error::forInternalError('Error while executing tool', $request->getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,11 +14,10 @@ namespace Mcp\Server\Handler\Request;
|
|||
use Mcp\Capability\Completion\ProviderInterface;
|
||||
use Mcp\Capability\Registry\ReferenceProviderInterface;
|
||||
use Mcp\Schema\JsonRpc\Error;
|
||||
use Mcp\Schema\JsonRpc\HasMethodInterface;
|
||||
use Mcp\Schema\JsonRpc\Request;
|
||||
use Mcp\Schema\JsonRpc\Response;
|
||||
use Mcp\Schema\Request\CompletionCompleteRequest;
|
||||
use Mcp\Schema\Result\CompletionCompleteResult;
|
||||
use Mcp\Server\Handler\MethodHandlerInterface;
|
||||
use Mcp\Server\Session\SessionInterface;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
|
|
@ -27,7 +26,7 @@ use Psr\Container\ContainerInterface;
|
|||
*
|
||||
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
||||
*/
|
||||
final class CompletionCompleteHandler implements MethodHandlerInterface
|
||||
final class CompletionCompleteHandler implements RequestHandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
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'] ?? '';
|
||||
$value = $message->argument['value'] ?? '';
|
||||
$name = $request->argument['name'] ?? '';
|
||||
$value = $request->argument['value'] ?? '';
|
||||
|
||||
$reference = match ($message->ref->type) {
|
||||
'ref/prompt' => $this->referenceProvider->getPrompt($message->ref->name),
|
||||
'ref/resource' => $this->referenceProvider->getResourceTemplate($message->ref->uri),
|
||||
$reference = match ($request->ref->type) {
|
||||
'ref/prompt' => $this->referenceProvider->getPrompt($request->ref->name),
|
||||
'ref/resource' => $this->referenceProvider->getResourceTemplate($request->ref->uri),
|
||||
default => null,
|
||||
};
|
||||
|
||||
if (null === $reference) {
|
||||
return new Response($message->getId(), new CompletionCompleteResult([]));
|
||||
return new Response($request->getId(), new CompletionCompleteResult([]));
|
||||
}
|
||||
|
||||
$providers = $reference->completionProviders;
|
||||
$provider = $providers[$name] ?? null;
|
||||
if (null === $provider) {
|
||||
return new Response($message->getId(), new CompletionCompleteResult([]));
|
||||
return new Response($request->getId(), new CompletionCompleteResult([]));
|
||||
}
|
||||
|
||||
if (\is_string($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();
|
||||
}
|
||||
|
||||
if (!$provider instanceof ProviderInterface) {
|
||||
return Error::forInternalError('Invalid completion provider type', $message->getId());
|
||||
return Error::forInternalError('Invalid completion provider type', $request->getId());
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -80,9 +79,9 @@ final class CompletionCompleteHandler implements MethodHandlerInterface
|
|||
$hasMore = $total > 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) {
|
||||
return Error::forInternalError('Error while handling completion request', $message->getId());
|
||||
return Error::forInternalError('Error while handling completion request', $request->getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,11 +17,10 @@ use Mcp\Exception\ExceptionInterface;
|
|||
use Mcp\Exception\PromptGetException;
|
||||
use Mcp\Exception\PromptNotFoundException;
|
||||
use Mcp\Schema\JsonRpc\Error;
|
||||
use Mcp\Schema\JsonRpc\HasMethodInterface;
|
||||
use Mcp\Schema\JsonRpc\Request;
|
||||
use Mcp\Schema\JsonRpc\Response;
|
||||
use Mcp\Schema\Request\GetPromptRequest;
|
||||
use Mcp\Schema\Result\GetPromptResult;
|
||||
use Mcp\Server\Handler\MethodHandlerInterface;
|
||||
use Mcp\Server\Session\SessionInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
|
|
@ -29,7 +28,7 @@ use Psr\Log\NullLogger;
|
|||
/**
|
||||
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
||||
*/
|
||||
final class GetPromptHandler implements MethodHandlerInterface
|
||||
final class GetPromptHandler implements RequestHandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
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;
|
||||
$arguments = $message->arguments ?? [];
|
||||
$promptName = $request->name;
|
||||
$arguments = $request->arguments ?? [];
|
||||
|
||||
try {
|
||||
$reference = $this->referenceProvider->getPrompt($promptName);
|
||||
if (null === $reference) {
|
||||
throw new PromptNotFoundException($message);
|
||||
throw new PromptNotFoundException($request);
|
||||
}
|
||||
|
||||
$result = $this->referenceHandler->handle($reference, $arguments);
|
||||
|
||||
$formatted = $reference->formatResult($result);
|
||||
|
||||
return new Response($message->getId(), new GetPromptResult($formatted));
|
||||
return new Response($request->getId(), new GetPromptResult($formatted));
|
||||
} catch (PromptNotFoundException $e) {
|
||||
$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) {
|
||||
$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) {
|
||||
$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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,38 +12,37 @@
|
|||
namespace Mcp\Server\Handler\Request;
|
||||
|
||||
use Mcp\Schema\Implementation;
|
||||
use Mcp\Schema\JsonRpc\HasMethodInterface;
|
||||
use Mcp\Schema\JsonRpc\Request;
|
||||
use Mcp\Schema\JsonRpc\Response;
|
||||
use Mcp\Schema\Request\InitializeRequest;
|
||||
use Mcp\Schema\Result\InitializeResult;
|
||||
use Mcp\Schema\ServerCapabilities;
|
||||
use Mcp\Server\Configuration;
|
||||
use Mcp\Server\Handler\MethodHandlerInterface;
|
||||
use Mcp\Server\Session\SessionInterface;
|
||||
|
||||
/**
|
||||
* @author Christopher Hertel <mail@christopher-hertel.de>
|
||||
*/
|
||||
final class InitializeHandler implements MethodHandlerInterface
|
||||
final class InitializeHandler implements RequestHandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
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(
|
||||
$message->getId(),
|
||||
$request->getId(),
|
||||
new InitializeResult(
|
||||
$this->configuration->capabilities ?? new ServerCapabilities(),
|
||||
$this->configuration->serverInfo ?? new Implementation(),
|
||||
|
|
|
|||
|
|
@ -13,17 +13,16 @@ namespace Mcp\Server\Handler\Request;
|
|||
|
||||
use Mcp\Capability\Registry\ReferenceProviderInterface;
|
||||
use Mcp\Exception\InvalidCursorException;
|
||||
use Mcp\Schema\JsonRpc\HasMethodInterface;
|
||||
use Mcp\Schema\JsonRpc\Request;
|
||||
use Mcp\Schema\JsonRpc\Response;
|
||||
use Mcp\Schema\Request\ListPromptsRequest;
|
||||
use Mcp\Schema\Result\ListPromptsResult;
|
||||
use Mcp\Server\Handler\MethodHandlerInterface;
|
||||
use Mcp\Server\Session\SessionInterface;
|
||||
|
||||
/**
|
||||
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
||||
*/
|
||||
final class ListPromptsHandler implements MethodHandlerInterface
|
||||
final class ListPromptsHandler implements RequestHandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
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
|
||||
*/
|
||||
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(
|
||||
$message->getId(),
|
||||
$request->getId(),
|
||||
new ListPromptsResult($page->references, $page->nextCursor),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,17 +13,16 @@ namespace Mcp\Server\Handler\Request;
|
|||
|
||||
use Mcp\Capability\Registry\ReferenceProviderInterface;
|
||||
use Mcp\Exception\InvalidCursorException;
|
||||
use Mcp\Schema\JsonRpc\HasMethodInterface;
|
||||
use Mcp\Schema\JsonRpc\Request;
|
||||
use Mcp\Schema\JsonRpc\Response;
|
||||
use Mcp\Schema\Request\ListResourceTemplatesRequest;
|
||||
use Mcp\Schema\Result\ListResourceTemplatesResult;
|
||||
use Mcp\Server\Handler\MethodHandlerInterface;
|
||||
use Mcp\Server\Session\SessionInterface;
|
||||
|
||||
/**
|
||||
* @author Christopher Hertel <mail@christopher-hertel.de>
|
||||
*/
|
||||
final class ListResourceTemplatesHandler implements MethodHandlerInterface
|
||||
final class ListResourceTemplatesHandler implements RequestHandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
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
|
||||
*/
|
||||
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(
|
||||
$message->getId(),
|
||||
$request->getId(),
|
||||
new ListResourceTemplatesResult($page->references, $page->nextCursor),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,17 +13,16 @@ namespace Mcp\Server\Handler\Request;
|
|||
|
||||
use Mcp\Capability\Registry\ReferenceProviderInterface;
|
||||
use Mcp\Exception\InvalidCursorException;
|
||||
use Mcp\Schema\JsonRpc\HasMethodInterface;
|
||||
use Mcp\Schema\JsonRpc\Request;
|
||||
use Mcp\Schema\JsonRpc\Response;
|
||||
use Mcp\Schema\Request\ListResourcesRequest;
|
||||
use Mcp\Schema\Result\ListResourcesResult;
|
||||
use Mcp\Server\Handler\MethodHandlerInterface;
|
||||
use Mcp\Server\Session\SessionInterface;
|
||||
|
||||
/**
|
||||
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
||||
*/
|
||||
final class ListResourcesHandler implements MethodHandlerInterface
|
||||
final class ListResourcesHandler implements RequestHandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
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
|
||||
*/
|
||||
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(
|
||||
$message->getId(),
|
||||
$request->getId(),
|
||||
new ListResourcesResult($page->references, $page->nextCursor),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,18 +13,17 @@ namespace Mcp\Server\Handler\Request;
|
|||
|
||||
use Mcp\Capability\Registry\ReferenceProviderInterface;
|
||||
use Mcp\Exception\InvalidCursorException;
|
||||
use Mcp\Schema\JsonRpc\HasMethodInterface;
|
||||
use Mcp\Schema\JsonRpc\Request;
|
||||
use Mcp\Schema\JsonRpc\Response;
|
||||
use Mcp\Schema\Request\ListToolsRequest;
|
||||
use Mcp\Schema\Result\ListToolsResult;
|
||||
use Mcp\Server\Handler\MethodHandlerInterface;
|
||||
use Mcp\Server\Session\SessionInterface;
|
||||
|
||||
/**
|
||||
* @author Christopher Hertel <mail@christopher-hertel.de>
|
||||
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
||||
*/
|
||||
final class ListToolsHandler implements MethodHandlerInterface
|
||||
final class ListToolsHandler implements RequestHandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
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
|
||||
*/
|
||||
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(
|
||||
$message->getId(),
|
||||
$request->getId(),
|
||||
new ListToolsResult($page->references, $page->nextCursor),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,27 +11,26 @@
|
|||
|
||||
namespace Mcp\Server\Handler\Request;
|
||||
|
||||
use Mcp\Schema\JsonRpc\HasMethodInterface;
|
||||
use Mcp\Schema\JsonRpc\Request;
|
||||
use Mcp\Schema\JsonRpc\Response;
|
||||
use Mcp\Schema\Request\PingRequest;
|
||||
use Mcp\Schema\Result\EmptyResult;
|
||||
use Mcp\Server\Handler\MethodHandlerInterface;
|
||||
use Mcp\Server\Session\SessionInterface;
|
||||
|
||||
/**
|
||||
* @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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,11 +16,10 @@ use Mcp\Capability\Registry\ReferenceProviderInterface;
|
|||
use Mcp\Capability\Registry\ResourceTemplateReference;
|
||||
use Mcp\Exception\ResourceNotFoundException;
|
||||
use Mcp\Schema\JsonRpc\Error;
|
||||
use Mcp\Schema\JsonRpc\HasMethodInterface;
|
||||
use Mcp\Schema\JsonRpc\Request;
|
||||
use Mcp\Schema\JsonRpc\Response;
|
||||
use Mcp\Schema\Request\ReadResourceRequest;
|
||||
use Mcp\Schema\Result\ReadResourceResult;
|
||||
use Mcp\Server\Handler\MethodHandlerInterface;
|
||||
use Mcp\Server\Session\SessionInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
|
|
@ -28,7 +27,7 @@ use Psr\Log\NullLogger;
|
|||
/**
|
||||
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
||||
*/
|
||||
final class ReadResourceHandler implements MethodHandlerInterface
|
||||
final class ReadResourceHandler implements RequestHandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ReferenceProviderInterface $referenceProvider,
|
||||
|
|
@ -37,23 +36,23 @@ final class ReadResourceHandler implements MethodHandlerInterface
|
|||
) {
|
||||
}
|
||||
|
||||
public function supports(HasMethodInterface $message): bool
|
||||
public function supports(Request $request): bool
|
||||
{
|
||||
return $message instanceof ReadResourceRequest;
|
||||
return $request instanceof ReadResourceRequest;
|
||||
}
|
||||
|
||||
public function handle(ReadResourceRequest|HasMethodInterface $message, SessionInterface $session): Response|Error
|
||||
public function handle(Request $request, SessionInterface $session): Response|Error
|
||||
{
|
||||
\assert($message instanceof ReadResourceRequest);
|
||||
\assert($request instanceof ReadResourceRequest);
|
||||
|
||||
$uri = $message->uri;
|
||||
$uri = $request->uri;
|
||||
|
||||
$this->logger->debug('Reading resource', ['uri' => $uri]);
|
||||
|
||||
try {
|
||||
$reference = $this->referenceProvider->getResource($uri);
|
||||
if (null === $reference) {
|
||||
throw new ResourceNotFoundException($message);
|
||||
throw new ResourceNotFoundException($request);
|
||||
}
|
||||
|
||||
$result = $this->referenceHandler->handle($reference, ['uri' => $uri]);
|
||||
|
|
@ -64,15 +63,15 @@ final class ReadResourceHandler implements MethodHandlerInterface
|
|||
$formatted = $reference->formatResult($result, $uri, $reference->schema->mimeType);
|
||||
}
|
||||
|
||||
return new Response($message->getId(), new ReadResourceResult($formatted));
|
||||
return new Response($request->getId(), new ReadResourceResult($formatted));
|
||||
} catch (ResourceNotFoundException $e) {
|
||||
$this->logger->error('Resource not found', ['uri' => $uri]);
|
||||
|
||||
return new Error($message->getId(), Error::RESOURCE_NOT_FOUND, $e->getMessage());
|
||||
return new Error($request->getId(), Error::RESOURCE_NOT_FOUND, $e->getMessage());
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error(\sprintf('Error while reading resource "%s": "%s".', $uri, $e->getMessage()));
|
||||
|
||||
return Error::forInternalError('Error while reading resource', $message->getId());
|
||||
return Error::forInternalError('Error while reading resource', $request->getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
27
src/Server/Handler/Request/RequestHandlerInterface.php
Normal file
27
src/Server/Handler/Request/RequestHandlerInterface.php
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?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\Request;
|
||||
|
||||
use Mcp\Schema\JsonRpc\Error;
|
||||
use Mcp\Schema\JsonRpc\Request;
|
||||
use Mcp\Schema\JsonRpc\Response;
|
||||
use Mcp\Server\Session\SessionInterface;
|
||||
|
||||
/**
|
||||
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
||||
*/
|
||||
interface RequestHandlerInterface
|
||||
{
|
||||
public function supports(Request $request): bool;
|
||||
|
||||
public function handle(Request $request, SessionInterface $session): Response|Error;
|
||||
}
|
||||
324
src/Server/Protocol.php
Normal file
324
src/Server/Protocol.php
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
<?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;
|
||||
|
||||
use Mcp\Exception\InvalidInputMessageException;
|
||||
use Mcp\JsonRpc\MessageFactory;
|
||||
use Mcp\Schema\JsonRpc\Error;
|
||||
use Mcp\Schema\JsonRpc\Notification;
|
||||
use Mcp\Schema\JsonRpc\Request;
|
||||
use Mcp\Schema\JsonRpc\Response;
|
||||
use Mcp\Schema\Request\InitializeRequest;
|
||||
use Mcp\Server\Handler\Notification\NotificationHandlerInterface;
|
||||
use Mcp\Server\Handler\Request\RequestHandlerInterface;
|
||||
use Mcp\Server\Session\SessionFactoryInterface;
|
||||
use Mcp\Server\Session\SessionInterface;
|
||||
use Mcp\Server\Session\SessionStoreInterface;
|
||||
use Mcp\Server\Transport\TransportInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
/**
|
||||
* @final
|
||||
*
|
||||
* @author Christopher Hertel <mail@christopher-hertel.de>
|
||||
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
||||
*/
|
||||
class Protocol
|
||||
{
|
||||
/** @var TransportInterface<mixed>|null */
|
||||
private ?TransportInterface $transport = null;
|
||||
|
||||
/**
|
||||
* @param array<int, RequestHandlerInterface> $requestHandlers
|
||||
* @param array<int, NotificationHandlerInterface> $notificationHandlers
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly array $requestHandlers,
|
||||
private readonly array $notificationHandlers,
|
||||
private readonly MessageFactory $messageFactory,
|
||||
private readonly SessionFactoryInterface $sessionFactory,
|
||||
private readonly SessionStoreInterface $sessionStore,
|
||||
private readonly LoggerInterface $logger = new NullLogger(),
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect this protocol to a transport.
|
||||
*
|
||||
* The protocol takes ownership of the transport and sets up all callbacks.
|
||||
*
|
||||
* @param TransportInterface<mixed> $transport
|
||||
*/
|
||||
public function connect(TransportInterface $transport): void
|
||||
{
|
||||
if ($this->transport) {
|
||||
throw new \RuntimeException('Protocol already connected to a transport');
|
||||
}
|
||||
|
||||
$this->transport = $transport;
|
||||
|
||||
$this->transport->onMessage([$this, 'processInput']);
|
||||
|
||||
$this->transport->onSessionEnd([$this, 'destroySession']);
|
||||
|
||||
$this->logger->info('Protocol connected to transport', ['transport' => $transport::class]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming message from the transport.
|
||||
*
|
||||
* This is called by the transport whenever ANY message arrives.
|
||||
*/
|
||||
public function processInput(string $input, ?Uuid $sessionId): void
|
||||
{
|
||||
$this->logger->info('Received message to process.', ['message' => $input]);
|
||||
|
||||
$this->gcSessions();
|
||||
|
||||
try {
|
||||
$messages = $this->messageFactory->create($input);
|
||||
} catch (\JsonException $e) {
|
||||
$this->logger->warning('Failed to decode json message.', ['exception' => $e]);
|
||||
$error = Error::forParseError($e->getMessage());
|
||||
$this->sendResponse($error, ['session_id' => $sessionId]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$session = $this->resolveSession($sessionId, $messages);
|
||||
if (null === $session) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($messages as $message) {
|
||||
if ($message instanceof InvalidInputMessageException) {
|
||||
$this->handleInvalidMessage($message, $session);
|
||||
} elseif ($message instanceof Request) {
|
||||
$this->handleRequest($message, $session);
|
||||
} elseif ($message instanceof Response || $message instanceof Error) {
|
||||
$this->handleResponse($message, $session);
|
||||
} elseif ($message instanceof Notification) {
|
||||
$this->handleNotification($message, $session);
|
||||
}
|
||||
}
|
||||
|
||||
$session->save();
|
||||
}
|
||||
|
||||
private function handleInvalidMessage(InvalidInputMessageException $exception, SessionInterface $session): void
|
||||
{
|
||||
$this->logger->warning('Failed to create message.', ['exception' => $exception]);
|
||||
|
||||
$error = Error::forInvalidRequest($exception->getMessage());
|
||||
$this->sendResponse($error, ['session_id' => $session->getId()]);
|
||||
}
|
||||
|
||||
private function handleRequest(Request $request, SessionInterface $session): void
|
||||
{
|
||||
$this->logger->info('Handling request.', ['request' => $request]);
|
||||
|
||||
$handlerFound = false;
|
||||
|
||||
foreach ($this->requestHandlers as $handler) {
|
||||
if (!$handler->supports($request)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$handlerFound = true;
|
||||
|
||||
try {
|
||||
$response = $handler->handle($request, $session);
|
||||
$this->sendResponse($response, ['session_id' => $session->getId()]);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$this->logger->warning(\sprintf('Invalid argument: %s', $e->getMessage()), ['exception' => $e]);
|
||||
$error = Error::forInvalidParams($e->getMessage(), $request->getId());
|
||||
$this->sendResponse($error, ['session_id' => $session->getId()]);
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error(\sprintf('Uncaught exception: %s', $e->getMessage()), ['exception' => $e]);
|
||||
$error = Error::forInternalError($e->getMessage(), $request->getId());
|
||||
$this->sendResponse($error, ['session_id' => $session->getId()]);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (!$handlerFound) {
|
||||
$error = Error::forMethodNotFound(\sprintf('No handler found for method "%s".', $request::getMethod()), $request->getId());
|
||||
$this->sendResponse($error, ['session_id' => $session->getId()]);
|
||||
}
|
||||
}
|
||||
|
||||
private function handleResponse(Response|Error $response, SessionInterface $session): void
|
||||
{
|
||||
$this->logger->info('Handling response.', ['response' => $response]);
|
||||
// TODO: Implement response handling
|
||||
}
|
||||
|
||||
private function handleNotification(Notification $notification, SessionInterface $session): void
|
||||
{
|
||||
$this->logger->info('Handling notification.', ['notification' => $notification]);
|
||||
|
||||
foreach ($this->notificationHandlers as $handler) {
|
||||
if (!$handler->supports($notification)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$handler->handle($notification, $session);
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error(\sprintf('Error while handling notification: %s', $e->getMessage()), ['exception' => $e]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $context
|
||||
*/
|
||||
public function sendRequest(Request $request, array $context = []): void
|
||||
{
|
||||
$this->logger->info('Sending request.', ['request' => $request, 'context' => $context]);
|
||||
// TODO: Implement request sending
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $context
|
||||
*/
|
||||
public function sendResponse(Response|Error $response, array $context = []): void
|
||||
{
|
||||
$this->logger->info('Sending response.', ['response' => $response, 'context' => $context]);
|
||||
|
||||
$encoded = null;
|
||||
|
||||
try {
|
||||
if ($response instanceof Response && [] === $response->result) {
|
||||
$encoded = json_encode($response, \JSON_THROW_ON_ERROR | \JSON_FORCE_OBJECT);
|
||||
}
|
||||
|
||||
$encoded = 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'
|
||||
);
|
||||
|
||||
$encoded = json_encode($fallbackError, \JSON_THROW_ON_ERROR);
|
||||
}
|
||||
$context['type'] = 'response';
|
||||
|
||||
$this->transport->send($encoded, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $context
|
||||
*/
|
||||
public function sendNotification(Notification $notification, array $context = []): void
|
||||
{
|
||||
$this->logger->info('Sending notification.', ['notification' => $notification, 'context' => $context]);
|
||||
$context['type'] = 'notification';
|
||||
// TODO: Implement notification sending
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, mixed> $messages
|
||||
*/
|
||||
private function hasInitializeRequest(array $messages): bool
|
||||
{
|
||||
foreach ($messages as $message) {
|
||||
if ($message instanceof InitializeRequest) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves and validates the session based on the request context.
|
||||
*
|
||||
* @param Uuid|null $sessionId The session ID from the transport
|
||||
* @param array<int,mixed> $messages The parsed messages
|
||||
*/
|
||||
private function resolveSession(?Uuid $sessionId, array $messages): ?SessionInterface
|
||||
{
|
||||
if ($this->hasInitializeRequest($messages)) {
|
||||
// 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.');
|
||||
$this->sendResponse($error, ['session_id' => $sessionId]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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.');
|
||||
$this->sendResponse($error);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->sessionFactory->create($this->sessionStore);
|
||||
}
|
||||
|
||||
if (!$sessionId) {
|
||||
$error = Error::forInvalidRequest('A valid session id is REQUIRED for non-initialize requests.');
|
||||
$this->sendResponse($error, ['status_code' => 400]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$this->sessionStore->exists($sessionId)) {
|
||||
$error = Error::forInvalidRequest('Session not found or has expired.');
|
||||
$this->sendResponse($error, ['status_code' => 404]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->sessionFactory->createWithId($sessionId, $this->sessionStore);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run garbage collection on expired sessions.
|
||||
* Uses the session store's internal TTL configuration.
|
||||
*/
|
||||
private function gcSessions(): 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()]);
|
||||
}
|
||||
}
|
||||
|
|
@ -27,9 +27,11 @@ interface TransportInterface
|
|||
public function initialize(): void;
|
||||
|
||||
/**
|
||||
* Registers a callback that will be invoked whenever the transport receives an incoming message.
|
||||
* Register callback for ALL incoming messages.
|
||||
*
|
||||
* @param callable(string $message, ?Uuid $sessionId): void $listener The callback function to execute when the message occurs
|
||||
* The transport calls this whenever ANY message arrives, regardless of source.
|
||||
*
|
||||
* @param callable(string $message, ?Uuid $sessionId): void $listener
|
||||
*/
|
||||
public function onMessage(callable $listener): void;
|
||||
|
||||
|
|
@ -45,15 +47,21 @@ interface TransportInterface
|
|||
public function listen(): mixed;
|
||||
|
||||
/**
|
||||
* Sends a raw JSON-RPC message string back to the client.
|
||||
* Send a message to the client.
|
||||
*
|
||||
* @param string $data The JSON-RPC message string to send
|
||||
* @param array<string, mixed> $context The context of the message
|
||||
* The transport decides HOW to send based on context
|
||||
*
|
||||
* @param array<string, mixed> $context Context about this message:
|
||||
* - 'session_id': Uuid|null
|
||||
* - 'type': 'response'|'request'|'notification'
|
||||
* - 'in_reply_to': int|string|null (ID of request this responds to)
|
||||
* - 'expects_response': bool (if this is a request needing response)
|
||||
*/
|
||||
public function send(string $data, array $context): void;
|
||||
|
||||
/**
|
||||
* Registers a callback that will be invoked when a session needs to be destroyed.
|
||||
* Register callback for session termination.
|
||||
*
|
||||
* This can happen when a client disconnects or explicitly ends their session.
|
||||
*
|
||||
* @param callable(Uuid $sessionId): void $listener The callback function to execute when destroying a session
|
||||
|
|
@ -65,7 +73,6 @@ interface TransportInterface
|
|||
*
|
||||
* This method should be called when the transport is no longer needed.
|
||||
* It should clean up any resources and close any connections.
|
||||
* `Server::run()` calls this automatically after `listen()` exits.
|
||||
*/
|
||||
public function close(): void;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue