Adopt Schema component in server handlers

This commit is contained in:
Christopher Hertel 2025-08-03 22:06:46 +02:00
commit 09707b4798
No known key found for this signature in database
GPG key ID: 111F6BFF7B6B5AA3
21 changed files with 1516 additions and 2424 deletions

View file

@ -1,163 +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;
use Mcp\Exception\ExceptionInterface;
use Mcp\Exception\HandlerNotFoundException;
use Mcp\Exception\InvalidInputMessageException;
use Mcp\Exception\NotFoundExceptionInterface;
use Mcp\Message\Error;
use Mcp\Message\Factory;
use Mcp\Message\Notification;
use Mcp\Message\Request;
use Mcp\Message\Response;
use Psr\Log\LoggerInterface;
/**
* @final
*
* @author Christopher Hertel <mail@christopher-hertel.de>
*/
class JsonRpcHandler
{
/**
* @var array<int, RequestHandlerInterface>
*/
private readonly array $requestHandlers;
/**
* @var array<int, NotificationHandlerInterface>
*/
private readonly array $notificationHandlers;
/**
* @param iterable<RequestHandlerInterface> $requestHandlers
* @param iterable<NotificationHandlerInterface> $notificationHandlers
*/
public function __construct(
private readonly Factory $messageFactory,
iterable $requestHandlers,
iterable $notificationHandlers,
private readonly LoggerInterface $logger,
) {
$this->requestHandlers = $requestHandlers instanceof \Traversable ? iterator_to_array($requestHandlers) : $requestHandlers;
$this->notificationHandlers = $notificationHandlers instanceof \Traversable ? iterator_to_array($notificationHandlers) : $notificationHandlers;
}
/**
* @return iterable<string|null>
*
* @throws ExceptionInterface When a handler throws an exception during message processing
* @throws \JsonException When JSON encoding of the response fails
*/
public function process(string $input): iterable
{
$this->logger->info('Received message to process', ['message' => $input]);
try {
$messages = $this->messageFactory->create($input);
} catch (\JsonException $e) {
$this->logger->warning('Failed to decode json message', ['exception' => $e]);
yield $this->encodeResponse(Error::parseError($e->getMessage()));
return;
}
foreach ($messages as $message) {
if ($message instanceof InvalidInputMessageException) {
$this->logger->warning('Failed to create message', ['exception' => $message]);
yield $this->encodeResponse(Error::invalidRequest(0, $message->getMessage()));
continue;
}
$this->logger->info('Decoded incoming message', ['message' => $message]);
try {
yield $message instanceof Notification
? $this->handleNotification($message)
: $this->encodeResponse($this->handleRequest($message));
} catch (\DomainException) {
yield null;
} catch (NotFoundExceptionInterface $e) {
$this->logger->warning(\sprintf('Failed to create response: %s', $e->getMessage()), ['exception' => $e]);
yield $this->encodeResponse(Error::methodNotFound($message->id, $e->getMessage()));
} catch (\InvalidArgumentException $e) {
$this->logger->warning(\sprintf('Invalid argument: %s', $e->getMessage()), ['exception' => $e]);
yield $this->encodeResponse(Error::invalidParams($message->id, $e->getMessage()));
} catch (\Throwable $e) {
$this->logger->critical(\sprintf('Uncaught exception: %s', $e->getMessage()), ['exception' => $e]);
yield $this->encodeResponse(Error::internalError($message->id, $e->getMessage()));
}
}
}
/**
* @throws \JsonException When JSON encoding fails
*/
private function encodeResponse(Response|Error|null $response): ?string
{
if (null === $response) {
$this->logger->warning('Response is null');
return null;
}
$this->logger->info('Encoding response', ['response' => $response]);
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);
}
/**
* @return null
*
* @throws ExceptionInterface When a notification handler throws an exception
*/
private function handleNotification(Notification $notification)
{
$handled = false;
foreach ($this->notificationHandlers as $handler) {
if ($handler->supports($notification)) {
$handler->handle($notification);
$handled = true;
}
}
if (!$handled) {
$this->logger->warning(\sprintf('No handler found for "%s".', $notification->method), ['notification' => $notification]);
}
return null;
}
/**
* @throws NotFoundExceptionInterface When no handler is found for the request method
* @throws ExceptionInterface When a request handler throws an exception
*/
private function handleRequest(Request $request): Response|Error
{
foreach ($this->requestHandlers as $handler) {
if ($handler->supports($request)) {
return $handler->createResponse($request);
}
}
throw new HandlerNotFoundException(\sprintf('No handler found for method "%s".', $request->method));
}
}

View file

@ -12,17 +12,20 @@
namespace Mcp\Server;
use Mcp\Exception\ExceptionInterface;
use Mcp\Message\Notification;
use Mcp\Schema\JsonRpc\Error;
use Mcp\Schema\JsonRpc\HasMethodInterface;
use Mcp\Schema\JsonRpc\Request;
use Mcp\Schema\JsonRpc\Response;
/**
* @author Christopher Hertel <mail@christopher-hertel.de>
*/
interface NotificationHandlerInterface
interface MethodHandlerInterface
{
public function supports(Notification $message): bool;
public function supports(HasMethodInterface $message): bool;
/**
* @throws ExceptionInterface When the handler encounters an error processing the notification
* @throws ExceptionInterface When the handler encounters an error processing the request
*/
public function handle(Notification $notification): void;
public function handle(HasMethodInterface $message): Response|Error|null;
}

View file

@ -1,28 +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\NotificationHandler;
use Mcp\Message\Notification;
use Mcp\Server\NotificationHandlerInterface;
/**
* @author Christopher Hertel <mail@christopher-hertel.de>
*/
abstract class BaseNotificationHandler implements NotificationHandlerInterface
{
public function supports(Notification $message): bool
{
return $message->method === \sprintf('notifications/%s', $this->supportedNotification());
}
abstract protected function supportedNotification(): string;
}

View file

@ -11,19 +11,24 @@
namespace Mcp\Server\NotificationHandler;
use Mcp\Message\Notification;
use Mcp\Schema\JsonRpc\Error;
use Mcp\Schema\JsonRpc\HasMethodInterface;
use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Notification\InitializedNotification;
use Mcp\Server\MethodHandlerInterface;
/**
* @author Christopher Hertel <mail@christopher-hertel.de>
*/
final class InitializedHandler extends BaseNotificationHandler
final class InitializedHandler implements MethodHandlerInterface
{
protected function supportedNotification(): string
public function supports(HasMethodInterface $message): bool
{
return 'initialized';
return $message instanceof InitializedNotification;
}
public function handle(Notification $notification): void
public function handle(InitializedNotification|HasMethodInterface $message): Response|Error|null
{
return null;
}
}

View file

@ -1,28 +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\RequestHandler;
use Mcp\Message\Request;
use Mcp\Server\RequestHandlerInterface;
/**
* @author Christopher Hertel <mail@christopher-hertel.de>
*/
abstract class BaseRequestHandler implements RequestHandlerInterface
{
public function supports(Request $message): bool
{
return $message->method === $this->supportedMethod();
}
abstract protected function supportedMethod(): string;
}

View file

@ -0,0 +1,50 @@
<?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\RequestHandler;
use Mcp\Capability\Tool\ToolExecutorInterface;
use Mcp\Exception\ExceptionInterface;
use Mcp\Schema\JsonRpc\Error;
use Mcp\Schema\JsonRpc\HasMethodInterface;
use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Request\CallToolRequest;
use Mcp\Server\MethodHandlerInterface;
/**
* @author Christopher Hertel <mail@christopher-hertel.de>
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class CallToolHandler implements MethodHandlerInterface
{
public function __construct(
private readonly ToolExecutorInterface $toolExecutor,
) {
}
public function supports(HasMethodInterface $message): bool
{
return $message instanceof CallToolRequest;
}
public function handle(CallToolRequest|HasMethodInterface $message): Response|Error
{
\assert($message instanceof CallToolRequest);
try {
$result = $this->toolExecutor->call($message);
} catch (ExceptionInterface) {
return Error::forInternalError('Error while executing tool', $message->getId());
}
return new Response($message->getId(), $result);
}
}

View file

@ -0,0 +1,49 @@
<?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\RequestHandler;
use Mcp\Capability\Prompt\PromptGetterInterface;
use Mcp\Exception\ExceptionInterface;
use Mcp\Schema\JsonRpc\Error;
use Mcp\Schema\JsonRpc\HasMethodInterface;
use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Request\GetPromptRequest;
use Mcp\Server\MethodHandlerInterface;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class GetPromptHandler implements MethodHandlerInterface
{
public function __construct(
private readonly PromptGetterInterface $getter,
) {
}
public function supports(HasMethodInterface $message): bool
{
return $message instanceof GetPromptRequest;
}
public function handle(GetPromptRequest|HasMethodInterface $message): Response|Error
{
\assert($message instanceof GetPromptRequest);
try {
$result = $this->getter->get($message);
} catch (ExceptionInterface) {
return Error::forInternalError('Error while handling prompt', $message->getId());
}
return new Response($message->getId(), $result);
}
}

View file

@ -11,35 +11,36 @@
namespace Mcp\Server\RequestHandler;
use Mcp\Message\Request;
use Mcp\Message\Response;
use Mcp\Schema\Implementation;
use Mcp\Schema\JsonRpc\HasMethodInterface;
use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Request\InitializeRequest;
use Mcp\Schema\Result\InitializeResult;
use Mcp\Schema\ServerCapabilities;
use Mcp\Server\MethodHandlerInterface;
/**
* @author Christopher Hertel <mail@christopher-hertel.de>
*/
final class InitializeHandler extends BaseRequestHandler
final class InitializeHandler implements MethodHandlerInterface
{
public function __construct(
private readonly string $name = 'app',
private readonly string $version = 'dev',
public readonly ?ServerCapabilities $capabilities = new ServerCapabilities(),
public readonly ?Implementation $serverInfo = new Implementation(),
) {
}
public function createResponse(Request $message): Response
public function supports(HasMethodInterface $message): bool
{
return new Response($message->id, [
'protocolVersion' => '2025-03-26',
'capabilities' => [
'prompts' => ['listChanged' => false],
'tools' => ['listChanged' => false],
'resources' => ['listChanged' => false, 'subscribe' => false],
],
'serverInfo' => ['name' => $this->name, 'version' => $this->version],
]);
return $message instanceof InitializeRequest;
}
protected function supportedMethod(): string
public function handle(InitializeRequest|HasMethodInterface $message): Response
{
return 'initialize';
\assert($message instanceof InitializeRequest);
return new Response($message->getId(),
new InitializeResult($this->capabilities, $this->serverInfo),
);
}
}

View file

@ -0,0 +1,64 @@
<?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\RequestHandler;
use Mcp\Capability\Prompt\CollectionInterface;
use Mcp\Schema\JsonRpc\HasMethodInterface;
use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Prompt;
use Mcp\Schema\PromptArgument;
use Mcp\Schema\Request\ListPromptsRequest;
use Mcp\Schema\Result\ListPromptsResult;
use Mcp\Server\MethodHandlerInterface;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class ListPromptsHandler implements MethodHandlerInterface
{
public function __construct(
private readonly CollectionInterface $collection,
private readonly int $pageSize = 20,
) {
}
public function supports(HasMethodInterface $message): bool
{
return $message instanceof ListPromptsRequest;
}
public function handle(ListPromptsRequest|HasMethodInterface $message): Response
{
\assert($message instanceof ListPromptsRequest);
$cursor = null;
$prompts = [];
$metadataList = $this->collection->getMetadata($this->pageSize, $message->cursor);
foreach ($metadataList as $metadata) {
$cursor = $metadata->getName();
$prompts[] = new Prompt(
$metadata->getName(),
$metadata->getDescription(),
array_map(fn (array $data) => PromptArgument::fromArray($data), $metadata->getArguments()),
);
}
$nextCursor = (null !== $cursor && \count($prompts) === $this->pageSize) ? $cursor : null;
return new Response(
$message->getId(),
new ListPromptsResult($prompts, $nextCursor),
);
}
}

View file

@ -0,0 +1,66 @@
<?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\RequestHandler;
use Mcp\Capability\Resource\CollectionInterface;
use Mcp\Schema\JsonRpc\HasMethodInterface;
use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Request\ListResourcesRequest;
use Mcp\Schema\Resource;
use Mcp\Schema\Result\ListResourcesResult;
use Mcp\Server\MethodHandlerInterface;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class ListResourcesHandler implements MethodHandlerInterface
{
public function __construct(
private readonly CollectionInterface $collection,
private readonly int $pageSize = 20,
) {
}
public function supports(HasMethodInterface $message): bool
{
return $message instanceof ListResourcesRequest;
}
public function handle(ListResourcesRequest|HasMethodInterface $message): Response
{
\assert($message instanceof ListResourcesRequest);
$cursor = null;
$resources = [];
$metadataList = $this->collection->getMetadata($this->pageSize, $message->cursor);
foreach ($metadataList as $metadata) {
$cursor = $metadata->getUri();
$resources[] = new Resource(
$metadata->getUri(),
$metadata->getName(),
$metadata->getDescription(),
$metadata->getMimeType(),
null,
$metadata->getSize(),
);
}
$nextCursor = (null !== $cursor && \count($resources) === $this->pageSize) ? $cursor : null;
return new Response(
$message->getId(),
new ListResourcesResult($resources, $nextCursor),
);
}
}

View file

@ -0,0 +1,69 @@
<?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\RequestHandler;
use Mcp\Capability\Tool\CollectionInterface;
use Mcp\Schema\JsonRpc\HasMethodInterface;
use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Request\ListToolsRequest;
use Mcp\Schema\Result\ListToolsResult;
use Mcp\Schema\Tool;
use Mcp\Server\MethodHandlerInterface;
/**
* @author Christopher Hertel <mail@christopher-hertel.de>
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class ListToolsHandler implements MethodHandlerInterface
{
public function __construct(
private readonly CollectionInterface $collection,
private readonly int $pageSize = 20,
) {
}
public function supports(HasMethodInterface $message): bool
{
return $message instanceof ListToolsRequest;
}
public function handle(ListToolsRequest|HasMethodInterface $message): Response
{
\assert($message instanceof ListToolsRequest);
$cursor = null;
$tools = [];
$metadataList = $this->collection->getMetadata($this->pageSize, $message->cursor);
foreach ($metadataList as $tool) {
$cursor = $tool->getName();
$inputSchema = $tool->getInputSchema();
$tools[] = new Tool(
$tool->getName(),
[] === $inputSchema ? [
'type' => 'object',
'$schema' => 'http://json-schema.org/draft-07/schema#',
] : $inputSchema,
$tool->getDescription(),
null,
);
}
$nextCursor = (null !== $cursor && \count($tools) === $this->pageSize) ? $cursor : null;
return new Response(
$message->getId(),
new ListToolsResult($tools, $nextCursor),
);
}
}

View file

@ -11,21 +11,26 @@
namespace Mcp\Server\RequestHandler;
use Mcp\Message\Request;
use Mcp\Message\Response;
use Mcp\Schema\JsonRpc\HasMethodInterface;
use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Request\PingRequest;
use Mcp\Schema\Result\EmptyResult;
use Mcp\Server\MethodHandlerInterface;
/**
* @author Christopher Hertel <mail@christopher-hertel.de>
*/
final class PingHandler extends BaseRequestHandler
final class PingHandler implements MethodHandlerInterface
{
public function createResponse(Request $message): Response
public function supports(HasMethodInterface $message): bool
{
return new Response($message->id, []);
return $message instanceof PingRequest;
}
protected function supportedMethod(): string
public function handle(PingRequest|HasMethodInterface $message): Response
{
return 'ping';
\assert($message instanceof PingRequest);
return new Response($message->getId(), new EmptyResult());
}
}

View file

@ -1,83 +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\RequestHandler;
use Mcp\Capability\Prompt\PromptGet;
use Mcp\Capability\Prompt\PromptGetterInterface;
use Mcp\Exception\ExceptionInterface;
use Mcp\Exception\InvalidArgumentException;
use Mcp\Message\Error;
use Mcp\Message\Request;
use Mcp\Message\Response;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class PromptGetHandler extends BaseRequestHandler
{
public function __construct(
private readonly PromptGetterInterface $getter,
) {
}
public function createResponse(Request $message): Response|Error
{
$name = $message->params['name'];
$arguments = $message->params['arguments'] ?? [];
try {
$result = $this->getter->get(new PromptGet(uniqid('', true), $name, $arguments));
} catch (ExceptionInterface) {
return Error::internalError($message->id, 'Error while handling prompt');
}
$messages = [];
foreach ($result->messages as $resultMessage) {
$content = match ($resultMessage->type) {
'text' => [
'type' => 'text',
'text' => $resultMessage->result,
],
'image', 'audio' => [
'type' => $resultMessage->type,
'data' => $resultMessage->result,
'mimeType' => $resultMessage->mimeType,
],
'resource' => [
'type' => 'resource',
'resource' => [
'uri' => $resultMessage->uri,
'mimeType' => $resultMessage->mimeType,
'text' => $resultMessage->result,
],
],
// TODO better exception
default => throw new InvalidArgumentException('Unsupported PromptGet result type: '.$resultMessage->type),
};
$messages[] = [
'role' => $resultMessage->role,
'content' => $content,
];
}
return new Response($message->id, [
'description' => $result->description,
'messages' => $messages,
]);
}
protected function supportedMethod(): string
{
return 'prompts/get';
}
}

View file

@ -1,85 +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\RequestHandler;
use Mcp\Capability\Prompt\CollectionInterface;
use Mcp\Message\Request;
use Mcp\Message\Response;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class PromptListHandler extends BaseRequestHandler
{
public function __construct(
private readonly CollectionInterface $collection,
private readonly int $pageSize = 20,
) {
}
public function createResponse(Request $message): Response
{
$nextCursor = null;
$prompts = [];
$metadataList = $this->collection->getMetadata(
$this->pageSize,
$message->params['cursor'] ?? null
);
foreach ($metadataList as $metadata) {
$nextCursor = $metadata->getName();
$result = [
'name' => $metadata->getName(),
];
$description = $metadata->getDescription();
if (null !== $description) {
$result['description'] = $description;
}
$arguments = [];
foreach ($metadata->getArguments() as $data) {
$argument = [
'name' => $data['name'],
'required' => $data['required'] ?? false,
];
if (isset($data['description'])) {
$argument['description'] = $data['description'];
}
$arguments[] = $argument;
}
if ([] !== $arguments) {
$result['arguments'] = $arguments;
}
$prompts[] = $result;
}
$result = [
'prompts' => $prompts,
];
if (null !== $nextCursor && \count($prompts) === $this->pageSize) {
$result['nextCursor'] = $nextCursor;
}
return new Response($message->id, $result);
}
protected function supportedMethod(): string
{
return 'prompts/list';
}
}

View file

@ -0,0 +1,52 @@
<?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\RequestHandler;
use Mcp\Capability\Resource\ResourceReaderInterface;
use Mcp\Exception\ExceptionInterface;
use Mcp\Exception\ResourceNotFoundException;
use Mcp\Schema\JsonRpc\Error;
use Mcp\Schema\JsonRpc\HasMethodInterface;
use Mcp\Schema\JsonRpc\Response;
use Mcp\Schema\Request\ReadResourceRequest;
use Mcp\Server\MethodHandlerInterface;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class ReadResourceHandler implements MethodHandlerInterface
{
public function __construct(
private readonly ResourceReaderInterface $reader,
) {
}
public function supports(HasMethodInterface $message): bool
{
return $message instanceof ReadResourceRequest;
}
public function handle(ReadResourceRequest|HasMethodInterface $message): Response|Error
{
\assert($message instanceof ReadResourceRequest);
try {
$result = $this->reader->read($message);
} catch (ResourceNotFoundException $e) {
return new Error($message->getId(), Error::RESOURCE_NOT_FOUND, $e->getMessage());
} catch (ExceptionInterface) {
return Error::forInternalError('Error while reading resource', $message->getId());
}
return new Response($message->getId(), $result);
}
}

View file

@ -1,79 +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\RequestHandler;
use Mcp\Capability\Resource\CollectionInterface;
use Mcp\Message\Request;
use Mcp\Message\Response;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class ResourceListHandler extends BaseRequestHandler
{
public function __construct(
private readonly CollectionInterface $collection,
private readonly int $pageSize = 20,
) {
}
public function createResponse(Request $message): Response
{
$nextCursor = null;
$resources = [];
$metadataList = $this->collection->getMetadata(
$this->pageSize,
$message->params['cursor'] ?? null
);
foreach ($metadataList as $metadata) {
$nextCursor = $metadata->getUri();
$result = [
'uri' => $metadata->getUri(),
'name' => $metadata->getName(),
];
$description = $metadata->getDescription();
if (null !== $description) {
$result['description'] = $description;
}
$mimeType = $metadata->getMimeType();
if (null !== $mimeType) {
$result['mimeType'] = $mimeType;
}
$size = $metadata->getSize();
if (null !== $size) {
$result['size'] = $size;
}
$resources[] = $result;
}
$result = [
'resources' => $resources,
];
if (null !== $nextCursor && \count($resources) === $this->pageSize) {
$result['nextCursor'] = $nextCursor;
}
return new Response($message->id, $result);
}
protected function supportedMethod(): string
{
return 'resources/list';
}
}

View file

@ -1,59 +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\RequestHandler;
use Mcp\Capability\Resource\ResourceRead;
use Mcp\Capability\Resource\ResourceReaderInterface;
use Mcp\Exception\ExceptionInterface;
use Mcp\Exception\ResourceNotFoundException;
use Mcp\Message\Error;
use Mcp\Message\Request;
use Mcp\Message\Response;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class ResourceReadHandler extends BaseRequestHandler
{
public function __construct(
private readonly ResourceReaderInterface $reader,
) {
}
public function createResponse(Request $message): Response|Error
{
$uri = $message->params['uri'];
try {
$result = $this->reader->read(new ResourceRead(uniqid('', true), $uri));
} catch (ResourceNotFoundException $e) {
return new Error($message->id, Error::RESOURCE_NOT_FOUND, $e->getMessage());
} catch (ExceptionInterface) {
return Error::internalError($message->id, 'Error while reading resource');
}
return new Response($message->id, [
'contents' => [
[
'uri' => $result->uri,
'mimeType' => $result->mimeType,
$result->type => $result->result,
],
],
]);
}
protected function supportedMethod(): string
{
return 'resources/read';
}
}

View file

@ -1,76 +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\RequestHandler;
use Mcp\Capability\Tool\ToolCall;
use Mcp\Capability\Tool\ToolExecutorInterface;
use Mcp\Exception\ExceptionInterface;
use Mcp\Exception\InvalidArgumentException;
use Mcp\Message\Error;
use Mcp\Message\Request;
use Mcp\Message\Response;
/**
* @author Christopher Hertel <mail@christopher-hertel.de>
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class ToolCallHandler extends BaseRequestHandler
{
public function __construct(
private readonly ToolExecutorInterface $toolExecutor,
) {
}
public function createResponse(Request $message): Response|Error
{
$name = $message->params['name'];
$arguments = $message->params['arguments'] ?? [];
try {
$result = $this->toolExecutor->call(new ToolCall(uniqid('', true), $name, $arguments));
} catch (ExceptionInterface) {
return Error::internalError($message->id, 'Error while executing tool');
}
$content = match ($result->type) {
'text' => [
'type' => 'text',
'text' => $result->result,
],
'image', 'audio' => [
'type' => $result->type,
'data' => $result->result,
'mimeType' => $result->mimeType,
],
'resource' => [
'type' => 'resource',
'resource' => [
'uri' => $result->uri,
'mimeType' => $result->mimeType,
'text' => $result->result,
],
],
// TODO better exception
default => throw new InvalidArgumentException('Unsupported tool result type: '.$result->type),
};
return new Response($message->id, [
'content' => [$content], // TODO: allow multiple `ToolCallResult`s in the future
'isError' => $result->isError,
]);
}
protected function supportedMethod(): string
{
return 'tools/call';
}
}

View file

@ -1,68 +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\RequestHandler;
use Mcp\Capability\Tool\CollectionInterface;
use Mcp\Message\Request;
use Mcp\Message\Response;
/**
* @author Christopher Hertel <mail@christopher-hertel.de>
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class ToolListHandler extends BaseRequestHandler
{
public function __construct(
private readonly CollectionInterface $collection,
private readonly int $pageSize = 20,
) {
}
public function createResponse(Request $message): Response
{
$nextCursor = null;
$tools = [];
$metadataList = $this->collection->getMetadata(
$this->pageSize,
$message->params['cursor'] ?? null
);
foreach ($metadataList as $tool) {
$nextCursor = $tool->getName();
$inputSchema = $tool->getInputSchema();
$tools[] = [
'name' => $tool->getName(),
'description' => $tool->getDescription(),
'inputSchema' => [] === $inputSchema ? [
'type' => 'object',
'$schema' => 'http://json-schema.org/draft-07/schema#',
] : $inputSchema,
];
}
$result = [
'tools' => $tools,
];
if (null !== $nextCursor && \count($tools) === $this->pageSize) {
$result['nextCursor'] = $nextCursor;
}
return new Response($message->id, $result);
}
protected function supportedMethod(): string
{
return 'tools/list';
}
}

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