diff --git a/composer.json b/composer.json index eec8ce27..8314eecb 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,8 @@ "phpunit/phpunit": "^10.5", "symfony/console": "^6.4 || ^7.0", "psr/cache": "^3.0", - "php-cs-fixer/shim": "^3.84" + "php-cs-fixer/shim": "^3.84", + "nyholm/nsa": "^1.3" }, "suggest": { "symfony/console": "To use SymfonyConsoleTransport for STDIO", diff --git a/examples/cli/index.php b/examples/cli/index.php index 90849edc..8c59cc58 100644 --- a/examples/cli/index.php +++ b/examples/cli/index.php @@ -22,10 +22,9 @@ $logger = new SymfonyConsole\Logger\ConsoleLogger($output); // Configure the JsonRpcHandler and build the functionality -$jsonRpcHandler = new Mcp\Server\JsonRpcHandler( - new Mcp\Message\Factory(), - App\Builder::buildRequestHandlers(), - App\Builder::buildNotificationHandlers(), +$jsonRpcHandler = new Mcp\JsonRpc\Handler( + Mcp\JsonRpc\MessageFactory::make(), + App\Builder::buildMethodHandlers(), $logger ); diff --git a/examples/cli/src/Builder.php b/examples/cli/src/Builder.php index 36f1d957..77ea7b75 100644 --- a/examples/cli/src/Builder.php +++ b/examples/cli/src/Builder.php @@ -14,17 +14,9 @@ use Mcp\Capability\PromptChain; use Mcp\Capability\ResourceChain; use Mcp\Capability\ToolChain; -use Mcp\Server\NotificationHandler\InitializedHandler; -use Mcp\Server\NotificationHandlerInterface; -use Mcp\Server\RequestHandler\InitializeHandler; -use Mcp\Server\RequestHandler\PingHandler; -use Mcp\Server\RequestHandler\PromptGetHandler; -use Mcp\Server\RequestHandler\PromptListHandler; -use Mcp\Server\RequestHandler\ResourceListHandler; -use Mcp\Server\RequestHandler\ResourceReadHandler; -use Mcp\Server\RequestHandler\ToolCallHandler; -use Mcp\Server\RequestHandler\ToolListHandler; -use Mcp\Server\RequestHandlerInterface; +use Mcp\Server\MethodHandlerInterface; +use Mcp\Server\NotificationHandler; +use Mcp\Server\RequestHandler; /** * @author Tobias Nyholm @@ -32,9 +24,9 @@ class Builder { /** - * @return list + * @return list */ - public static function buildRequestHandlers(): array + public static function buildMethodHandlers(): array { $promptManager = new PromptChain([ new ExamplePrompt(), @@ -49,24 +41,15 @@ public static function buildRequestHandlers(): array ]); return [ - new InitializeHandler(), - new PingHandler(), - new PromptListHandler($promptManager), - new PromptGetHandler($promptManager), - new ResourceListHandler($resourceManager), - new ResourceReadHandler($resourceManager), - new ToolCallHandler($toolManager), - new ToolListHandler($toolManager), - ]; - } - - /** - * @return list - */ - public static function buildNotificationHandlers(): array - { - return [ - new InitializedHandler(), + new NotificationHandler\InitializedHandler(), + new RequestHandler\InitializeHandler(), + new RequestHandler\PingHandler(), + new RequestHandler\ListPromptsHandler($promptManager), + new RequestHandler\GetPromptHandler($promptManager), + new RequestHandler\ListResourcesHandler($resourceManager), + new RequestHandler\ReadResourceHandler($resourceManager), + new RequestHandler\CallToolHandler($toolManager), + new RequestHandler\ListToolsHandler($toolManager), ]; } } diff --git a/examples/cli/src/ExamplePrompt.php b/examples/cli/src/ExamplePrompt.php index 958a1969..7c4b0927 100644 --- a/examples/cli/src/ExamplePrompt.php +++ b/examples/cli/src/ExamplePrompt.php @@ -12,26 +12,28 @@ namespace App; use Mcp\Capability\Prompt\MetadataInterface; -use Mcp\Capability\Prompt\PromptGet; -use Mcp\Capability\Prompt\PromptGetResult; -use Mcp\Capability\Prompt\PromptGetResultMessages; use Mcp\Capability\Prompt\PromptGetterInterface; +use Mcp\Schema\Content\PromptMessage; +use Mcp\Schema\Content\TextContent; +use Mcp\Schema\Enum\Role; +use Mcp\Schema\Request\GetPromptRequest; +use Mcp\Schema\Result\GetPromptResult; /** * @author Tobias Nyholm */ class ExamplePrompt implements MetadataInterface, PromptGetterInterface { - public function get(PromptGet $input): PromptGetResult + public function get(GetPromptRequest $request): GetPromptResult { - $firstName = $input->arguments['first name'] ?? null; + $firstName = $request->arguments['firstName'] ?? null; - return new PromptGetResult( + return new GetPromptResult( + [new PromptMessage( + Role::User, + new TextContent(\sprintf('Hello %s', $firstName ?? 'World')), + )], $this->getDescription(), - [new PromptGetResultMessages( - 'user', - \sprintf('Hello %s', $firstName ?? 'World') - )] ); } diff --git a/examples/cli/src/ExampleResource.php b/examples/cli/src/ExampleResource.php index 0ab4444c..66cbdc3f 100644 --- a/examples/cli/src/ExampleResource.php +++ b/examples/cli/src/ExampleResource.php @@ -12,21 +12,21 @@ namespace App; use Mcp\Capability\Resource\MetadataInterface; -use Mcp\Capability\Resource\ResourceRead; use Mcp\Capability\Resource\ResourceReaderInterface; -use Mcp\Capability\Resource\ResourceReadResult; +use Mcp\Schema\Content\TextResourceContents; +use Mcp\Schema\Request\ReadResourceRequest; +use Mcp\Schema\Result\ReadResourceResult; /** * @author Tobias Nyholm */ class ExampleResource implements MetadataInterface, ResourceReaderInterface { - public function read(ResourceRead $input): ResourceReadResult + public function read(ReadResourceRequest $request): ReadResourceResult { - return new ResourceReadResult( - 'Content of '.$this->getName(), - $this->getUri(), - ); + return new ReadResourceResult([ + new TextResourceContents($this->getUri(), null, 'Content of My Resource'), + ]); } public function getUri(): string @@ -36,7 +36,7 @@ public function getUri(): string public function getName(): string { - return 'My resource'; + return 'my-resource'; } public function getDescription(): ?string diff --git a/examples/cli/src/ExampleTool.php b/examples/cli/src/ExampleTool.php index c1784c49..0eb9010a 100644 --- a/examples/cli/src/ExampleTool.php +++ b/examples/cli/src/ExampleTool.php @@ -12,22 +12,25 @@ namespace App; use Mcp\Capability\Tool\MetadataInterface; -use Mcp\Capability\Tool\ToolCall; -use Mcp\Capability\Tool\ToolCallResult; use Mcp\Capability\Tool\ToolExecutorInterface; +use Mcp\Schema\Content\TextContent; +use Mcp\Schema\Request\CallToolRequest; +use Mcp\Schema\Result\CallToolResult; /** * @author Tobias Nyholm */ class ExampleTool implements MetadataInterface, ToolExecutorInterface { - public function call(ToolCall $input): ToolCallResult + public function call(CallToolRequest $request): CallToolResult { - $format = $input->arguments['format'] ?? 'Y-m-d H:i:s'; + $format = $request->arguments['format'] ?? 'Y-m-d H:i:s'; - return new ToolCallResult( - (new \DateTime('now', new \DateTimeZone('UTC')))->format($format) - ); + return new CallToolResult([ + new TextContent( + (new \DateTime('now', new \DateTimeZone('UTC')))->format($format), + ), + ]); } public function getName(): string diff --git a/phpstan.dist.neon b/phpstan.dist.neon index ac6db7e2..6dee9037 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -7,3 +7,6 @@ parameters: excludePaths: - examples/cli/vendor/* (?) treatPhpDocTypesAsCertain: false + ignoreErrors: + - + message: "#^Method .*::test.*\\(\\) has no return type specified\\.$#" diff --git a/src/Capability/Prompt/PromptGet.php b/src/Capability/Prompt/PromptGet.php deleted file mode 100644 index 30d5fc6f..00000000 --- a/src/Capability/Prompt/PromptGet.php +++ /dev/null @@ -1,28 +0,0 @@ - - */ -final class PromptGet -{ - /** - * @param array $arguments - */ - public function __construct( - public readonly string $id, - public readonly string $name, - public readonly array $arguments = [], - ) { - } -} diff --git a/src/Capability/Prompt/PromptGetResult.php b/src/Capability/Prompt/PromptGetResult.php deleted file mode 100644 index cb06f33e..00000000 --- a/src/Capability/Prompt/PromptGetResult.php +++ /dev/null @@ -1,27 +0,0 @@ - - */ -final class PromptGetResult -{ - /** - * @param list $messages - */ - public function __construct( - public readonly string $description, - public readonly array $messages = [], - ) { - } -} diff --git a/src/Capability/Prompt/PromptGetResultMessages.php b/src/Capability/Prompt/PromptGetResultMessages.php deleted file mode 100644 index bb05a1de..00000000 --- a/src/Capability/Prompt/PromptGetResultMessages.php +++ /dev/null @@ -1,30 +0,0 @@ - - */ -final class PromptGetResultMessages -{ - public function __construct( - public readonly string $role, - public readonly string $result, - /** - * @var "text"|"image"|"audio"|"resource"|non-empty-string - */ - public readonly string $type = 'text', - public readonly string $mimeType = 'text/plan', - public readonly ?string $uri = null, - ) { - } -} diff --git a/src/Capability/Prompt/PromptGetterInterface.php b/src/Capability/Prompt/PromptGetterInterface.php index cd57e729..35d7b9fb 100644 --- a/src/Capability/Prompt/PromptGetterInterface.php +++ b/src/Capability/Prompt/PromptGetterInterface.php @@ -13,6 +13,8 @@ use Mcp\Exception\PromptGetException; use Mcp\Exception\PromptNotFoundException; +use Mcp\Schema\Request\GetPromptRequest; +use Mcp\Schema\Result\GetPromptResult; /** * @author Tobias Nyholm @@ -23,5 +25,5 @@ interface PromptGetterInterface * @throws PromptGetException if the prompt execution fails * @throws PromptNotFoundException if the prompt is not found */ - public function get(PromptGet $input): PromptGetResult; + public function get(GetPromptRequest $request): GetPromptResult; } diff --git a/src/Capability/PromptChain.php b/src/Capability/PromptChain.php index 8ec7a74c..f7edf62a 100644 --- a/src/Capability/PromptChain.php +++ b/src/Capability/PromptChain.php @@ -14,12 +14,12 @@ use Mcp\Capability\Prompt\CollectionInterface; use Mcp\Capability\Prompt\IdentifierInterface; use Mcp\Capability\Prompt\MetadataInterface; -use Mcp\Capability\Prompt\PromptGet; -use Mcp\Capability\Prompt\PromptGetResult; use Mcp\Capability\Prompt\PromptGetterInterface; use Mcp\Exception\InvalidCursorException; use Mcp\Exception\PromptGetException; use Mcp\Exception\PromptNotFoundException; +use Mcp\Schema\Request\GetPromptRequest; +use Mcp\Schema\Result\GetPromptResult; /** * A collection of prompts. All prompts need to implement IdentifierInterface. @@ -60,18 +60,18 @@ public function getMetadata(int $count, ?string $lastIdentifier = null): iterabl } } - public function get(PromptGet $input): PromptGetResult + public function get(GetPromptRequest $request): GetPromptResult { foreach ($this->items as $item) { - if ($item instanceof PromptGetterInterface && $input->name === $item->getName()) { + if ($item instanceof PromptGetterInterface && $request->name === $item->getName()) { try { - return $item->get($input); + return $item->get($request); } catch (\Throwable $e) { - throw new PromptGetException($input, $e); + throw new PromptGetException($request, $e); } } } - throw new PromptNotFoundException($input); + throw new PromptNotFoundException($request); } } diff --git a/src/Capability/Resource/ResourceReaderInterface.php b/src/Capability/Resource/ResourceReaderInterface.php index 0fdf6042..cba468cd 100644 --- a/src/Capability/Resource/ResourceReaderInterface.php +++ b/src/Capability/Resource/ResourceReaderInterface.php @@ -13,6 +13,8 @@ use Mcp\Exception\ResourceNotFoundException; use Mcp\Exception\ResourceReadException; +use Mcp\Schema\Request\ReadResourceRequest; +use Mcp\Schema\Result\ReadResourceResult; /** * @author Tobias Nyholm @@ -23,5 +25,5 @@ interface ResourceReaderInterface * @throws ResourceReadException if the resource execution fails * @throws ResourceNotFoundException if the resource is not found */ - public function read(ResourceRead $input): ResourceReadResult; + public function read(ReadResourceRequest $request): ReadResourceResult; } diff --git a/src/Capability/ResourceChain.php b/src/Capability/ResourceChain.php index ef3b240d..1d40d492 100644 --- a/src/Capability/ResourceChain.php +++ b/src/Capability/ResourceChain.php @@ -14,12 +14,12 @@ use Mcp\Capability\Resource\CollectionInterface; use Mcp\Capability\Resource\IdentifierInterface; use Mcp\Capability\Resource\MetadataInterface; -use Mcp\Capability\Resource\ResourceRead; use Mcp\Capability\Resource\ResourceReaderInterface; -use Mcp\Capability\Resource\ResourceReadResult; use Mcp\Exception\InvalidCursorException; use Mcp\Exception\ResourceNotFoundException; use Mcp\Exception\ResourceReadException; +use Mcp\Schema\Request\ReadResourceRequest; +use Mcp\Schema\Result\ReadResourceResult; /** * A collection of resources. All resources need to implement IdentifierInterface. @@ -60,18 +60,18 @@ public function getMetadata(int $count, ?string $lastIdentifier = null): iterabl } } - public function read(ResourceRead $input): ResourceReadResult + public function read(ReadResourceRequest $request): ReadResourceResult { foreach ($this->items as $item) { - if ($item instanceof ResourceReaderInterface && $input->uri === $item->getUri()) { + if ($item instanceof ResourceReaderInterface && $request->uri === $item->getUri()) { try { - return $item->read($input); + return $item->read($request); } catch (\Throwable $e) { - throw new ResourceReadException($input, $e); + throw new ResourceReadException($request, $e); } } } - throw new ResourceNotFoundException($input); + throw new ResourceNotFoundException($request); } } diff --git a/src/Capability/Tool/ToolCall.php b/src/Capability/Tool/ToolCall.php deleted file mode 100644 index cd4bb85b..00000000 --- a/src/Capability/Tool/ToolCall.php +++ /dev/null @@ -1,28 +0,0 @@ - - */ -final class ToolCall -{ - /** - * @param array $arguments - */ - public function __construct( - public readonly string $id, - public readonly string $name, - public readonly array $arguments = [], - ) { - } -} diff --git a/src/Capability/Tool/ToolCallResult.php b/src/Capability/Tool/ToolCallResult.php deleted file mode 100644 index 78933a7c..00000000 --- a/src/Capability/Tool/ToolCallResult.php +++ /dev/null @@ -1,30 +0,0 @@ - - */ -final class ToolCallResult -{ - public function __construct( - public readonly string $result, - /** - * @var "text"|"image"|"audio"|"resource"|non-empty-string - */ - public readonly string $type = 'text', - public readonly string $mimeType = 'text/plan', - public readonly bool $isError = false, - public readonly ?string $uri = null, - ) { - } -} diff --git a/src/Capability/Tool/ToolExecutorInterface.php b/src/Capability/Tool/ToolExecutorInterface.php index 91150573..c72b134a 100644 --- a/src/Capability/Tool/ToolExecutorInterface.php +++ b/src/Capability/Tool/ToolExecutorInterface.php @@ -13,6 +13,8 @@ use Mcp\Exception\ToolExecutionException; use Mcp\Exception\ToolNotFoundException; +use Mcp\Schema\Request\CallToolRequest; +use Mcp\Schema\Result\CallToolResult; /** * @author Tobias Nyholm @@ -23,5 +25,5 @@ interface ToolExecutorInterface * @throws ToolExecutionException if the tool execution fails * @throws ToolNotFoundException if the tool is not found */ - public function call(ToolCall $input): ToolCallResult; + public function call(CallToolRequest $request): CallToolResult; } diff --git a/src/Capability/ToolChain.php b/src/Capability/ToolChain.php index 800831e8..7baeee67 100644 --- a/src/Capability/ToolChain.php +++ b/src/Capability/ToolChain.php @@ -14,12 +14,12 @@ use Mcp\Capability\Tool\CollectionInterface; use Mcp\Capability\Tool\IdentifierInterface; use Mcp\Capability\Tool\MetadataInterface; -use Mcp\Capability\Tool\ToolCall; -use Mcp\Capability\Tool\ToolCallResult; use Mcp\Capability\Tool\ToolExecutorInterface; use Mcp\Exception\InvalidCursorException; use Mcp\Exception\ToolExecutionException; use Mcp\Exception\ToolNotFoundException; +use Mcp\Schema\Request\CallToolRequest; +use Mcp\Schema\Result\CallToolResult; /** * A collection of tools. All tools need to implement IdentifierInterface. @@ -60,18 +60,18 @@ public function getMetadata(int $count, ?string $lastIdentifier = null): iterabl } } - public function call(ToolCall $input): ToolCallResult + public function call(CallToolRequest $request): CallToolResult { foreach ($this->items as $item) { - if ($item instanceof ToolExecutorInterface && $input->name === $item->getName()) { + if ($item instanceof ToolExecutorInterface && $request->name === $item->getName()) { try { - return $item->call($input); + return $item->call($request); } catch (\Throwable $e) { - throw new ToolExecutionException($input, $e); + throw new ToolExecutionException($request, $e); } } } - throw new ToolNotFoundException($input); + throw new ToolNotFoundException($request); } } diff --git a/src/Capability/Resource/ResourceRead.php b/src/Exception/LogicException.php similarity index 53% rename from src/Capability/Resource/ResourceRead.php rename to src/Exception/LogicException.php index 1b60d55a..2ebe44c9 100644 --- a/src/Capability/Resource/ResourceRead.php +++ b/src/Exception/LogicException.php @@ -9,16 +9,11 @@ * file that was distributed with this source code. */ -namespace Mcp\Capability\Resource; +namespace Mcp\Exception; /** - * @author Tobias Nyholm + * @author Christopher Hertel */ -final class ResourceRead +final class LogicException extends \LogicException implements ExceptionInterface { - public function __construct( - public readonly string $id, - public readonly string $uri, - ) { - } } diff --git a/src/Exception/PromptGetException.php b/src/Exception/PromptGetException.php index d73e579b..8970ea58 100644 --- a/src/Exception/PromptGetException.php +++ b/src/Exception/PromptGetException.php @@ -11,7 +11,7 @@ namespace Mcp\Exception; -use Mcp\Capability\Prompt\PromptGet; +use Mcp\Schema\Request\GetPromptRequest; /** * @author Tobias Nyholm @@ -19,9 +19,9 @@ final class PromptGetException extends \RuntimeException implements ExceptionInterface { public function __construct( - public readonly PromptGet $promptGet, + public readonly GetPromptRequest $request, ?\Throwable $previous = null, ) { - parent::__construct(\sprintf('Handling prompt "%s" failed with error: %s', $promptGet->name, $previous->getMessage()), previous: $previous); + parent::__construct(\sprintf('Handling prompt "%s" failed with error: "%s".', $request->name, $previous->getMessage()), previous: $previous); } } diff --git a/src/Exception/PromptNotFoundException.php b/src/Exception/PromptNotFoundException.php index e8ba9d7a..82872e8b 100644 --- a/src/Exception/PromptNotFoundException.php +++ b/src/Exception/PromptNotFoundException.php @@ -11,7 +11,7 @@ namespace Mcp\Exception; -use Mcp\Capability\Prompt\PromptGet; +use Mcp\Schema\Request\GetPromptRequest; /** * @author Tobias Nyholm @@ -19,8 +19,8 @@ final class PromptNotFoundException extends \RuntimeException implements NotFoundExceptionInterface { public function __construct( - public readonly PromptGet $promptGet, + public readonly GetPromptRequest $request, ) { - parent::__construct(\sprintf('Prompt not found for name: "%s"', $promptGet->name)); + parent::__construct(\sprintf('Prompt not found for name: "%s".', $request->name)); } } diff --git a/src/Exception/ResourceNotFoundException.php b/src/Exception/ResourceNotFoundException.php index 46a3f472..b5624bbc 100644 --- a/src/Exception/ResourceNotFoundException.php +++ b/src/Exception/ResourceNotFoundException.php @@ -11,7 +11,7 @@ namespace Mcp\Exception; -use Mcp\Capability\Resource\ResourceRead; +use Mcp\Schema\Request\ReadResourceRequest; /** * @author Tobias Nyholm @@ -19,8 +19,8 @@ final class ResourceNotFoundException extends \RuntimeException implements NotFoundExceptionInterface { public function __construct( - public readonly ResourceRead $readRequest, + public readonly ReadResourceRequest $request, ) { - parent::__construct(\sprintf('Resource not found for uri: "%s"', $readRequest->uri)); + parent::__construct(\sprintf('Resource not found for uri: "%s".', $request->uri)); } } diff --git a/src/Exception/ResourceReadException.php b/src/Exception/ResourceReadException.php index e0988463..913064b2 100644 --- a/src/Exception/ResourceReadException.php +++ b/src/Exception/ResourceReadException.php @@ -11,7 +11,7 @@ namespace Mcp\Exception; -use Mcp\Capability\Resource\ResourceRead; +use Mcp\Schema\Request\ReadResourceRequest; /** * @author Tobias Nyholm @@ -19,9 +19,9 @@ final class ResourceReadException extends \RuntimeException implements ExceptionInterface { public function __construct( - public readonly ResourceRead $readRequest, + public readonly ReadResourceRequest $request, ?\Throwable $previous = null, ) { - parent::__construct(\sprintf('Reading resource "%s" failed with error: %s', $readRequest->uri, $previous?->getMessage() ?? ''), previous: $previous); + parent::__construct(\sprintf('Reading resource "%s" failed with error: "%s".', $request->uri, $previous?->getMessage() ?? ''), previous: $previous); } } diff --git a/src/Exception/RuntimeException.php b/src/Exception/RuntimeException.php index ca379bfc..b68a1bf4 100644 --- a/src/Exception/RuntimeException.php +++ b/src/Exception/RuntimeException.php @@ -14,6 +14,6 @@ /** * @author Christopher Hertel */ -class RuntimeException extends \RuntimeException implements ExceptionInterface +final class RuntimeException extends \RuntimeException implements ExceptionInterface { } diff --git a/src/Exception/ToolExecutionException.php b/src/Exception/ToolExecutionException.php index 09d5df2d..f2df9366 100644 --- a/src/Exception/ToolExecutionException.php +++ b/src/Exception/ToolExecutionException.php @@ -11,7 +11,7 @@ namespace Mcp\Exception; -use Mcp\Capability\Tool\ToolCall; +use Mcp\Schema\Request\CallToolRequest; /** * @author Tobias Nyholm @@ -19,9 +19,9 @@ final class ToolExecutionException extends \RuntimeException implements ExceptionInterface { public function __construct( - public readonly ToolCall $toolCall, + public readonly CallToolRequest $request, ?\Throwable $previous = null, ) { - parent::__construct(\sprintf('Execution of tool "%s" failed with error: %s', $toolCall->name, $previous?->getMessage() ?? ''), previous: $previous); + parent::__construct(\sprintf('Execution of tool "%s" failed with error: "%s".', $request->name, $previous?->getMessage() ?? ''), previous: $previous); } } diff --git a/src/Exception/ToolNotFoundException.php b/src/Exception/ToolNotFoundException.php index e9e3bf2d..3795d74e 100644 --- a/src/Exception/ToolNotFoundException.php +++ b/src/Exception/ToolNotFoundException.php @@ -11,7 +11,7 @@ namespace Mcp\Exception; -use Mcp\Capability\Tool\ToolCall; +use Mcp\Schema\Request\CallToolRequest; /** * @author Tobias Nyholm @@ -19,8 +19,8 @@ final class ToolNotFoundException extends \RuntimeException implements NotFoundExceptionInterface { public function __construct( - public readonly ToolCall $toolCall, + public readonly CallToolRequest $request, ) { - parent::__construct(\sprintf('Tool not found for call: "%s"', $toolCall->name)); + parent::__construct(\sprintf('Tool not found for call: "%s".', $request->name)); } } diff --git a/src/Server/JsonRpcHandler.php b/src/JsonRpc/Handler.php similarity index 54% rename from src/Server/JsonRpcHandler.php rename to src/JsonRpc/Handler.php index 9fb9e988..9214c9a1 100644 --- a/src/Server/JsonRpcHandler.php +++ b/src/JsonRpc/Handler.php @@ -9,48 +9,40 @@ * file that was distributed with this source code. */ -namespace Mcp\Server; +namespace Mcp\JsonRpc; 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 Mcp\Schema\JsonRpc\Error; +use Mcp\Schema\JsonRpc\HasMethodInterface; +use Mcp\Schema\JsonRpc\Response; +use Mcp\Server\MethodHandlerInterface; use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; /** * @final * * @author Christopher Hertel */ -class JsonRpcHandler +class Handler { /** - * @var array + * @var array */ - private readonly array $requestHandlers; + private readonly array $methodHandlers; /** - * @var array - */ - private readonly array $notificationHandlers; - - /** - * @param iterable $requestHandlers - * @param iterable $notificationHandlers + * @param iterable $methodHandlers */ public function __construct( - private readonly Factory $messageFactory, - iterable $requestHandlers, - iterable $notificationHandlers, - private readonly LoggerInterface $logger, + private readonly MessageFactory $messageFactory, + iterable $methodHandlers, + private readonly LoggerInterface $logger = new NullLogger(), ) { - $this->requestHandlers = $requestHandlers instanceof \Traversable ? iterator_to_array($requestHandlers) : $requestHandlers; - $this->notificationHandlers = $notificationHandlers instanceof \Traversable ? iterator_to_array($notificationHandlers) : $notificationHandlers; + $this->methodHandlers = $methodHandlers instanceof \Traversable ? iterator_to_array($methodHandlers) : $methodHandlers; } /** @@ -68,7 +60,7 @@ public function process(string $input): iterable } catch (\JsonException $e) { $this->logger->warning('Failed to decode json message', ['exception' => $e]); - yield $this->encodeResponse(Error::parseError($e->getMessage())); + yield $this->encodeResponse(Error::forParseError($e->getMessage())); return; } @@ -76,30 +68,28 @@ public function process(string $input): iterable 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())); + yield $this->encodeResponse(Error::forInvalidRequest($message->getMessage(), 0)); continue; } $this->logger->info('Decoded incoming message', ['message' => $message]); try { - yield $message instanceof Notification - ? $this->handleNotification($message) - : $this->encodeResponse($this->handleRequest($message)); + yield $this->encodeResponse($this->handle($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())); + yield $this->encodeResponse(Error::forMethodNotFound($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())); + yield $this->encodeResponse(Error::forInvalidParams($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())); + yield $this->encodeResponse(Error::forInternalError($e->getMessage())); } } } @@ -110,7 +100,7 @@ public function process(string $input): iterable private function encodeResponse(Response|Error|null $response): ?string { if (null === $response) { - $this->logger->warning('Response is null'); + $this->logger->info('The handler created an empty response.'); return null; } @@ -125,39 +115,29 @@ private function encodeResponse(Response|Error|null $response): ?string } /** - * @return null + * If the handler does support the message, but does not create a response, other handlers will be tried. * - * @throws ExceptionInterface When a notification handler throws an exception + * @throws NotFoundExceptionInterface When no handler is found for the request method + * @throws ExceptionInterface When a request handler throws an exception */ - private function handleNotification(Notification $notification) + private function handle(HasMethodInterface $message): Response|Error|null { $handled = false; - foreach ($this->notificationHandlers as $handler) { - if ($handler->supports($notification)) { - $handler->handle($notification); + foreach ($this->methodHandlers as $handler) { + if ($handler->supports($message)) { + $return = $handler->handle($message); $handled = true; - } - } - if (!$handled) { - $this->logger->warning(\sprintf('No handler found for "%s".', $notification->method), ['notification' => $notification]); + if (null !== $return) { + return $return; + } + } } - 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); - } + if ($handled) { + return null; } - throw new HandlerNotFoundException(\sprintf('No handler found for method "%s".', $request->method)); + throw new HandlerNotFoundException(\sprintf('No handler found for method "%s".', $message::getMethod())); } } diff --git a/src/JsonRpc/MessageFactory.php b/src/JsonRpc/MessageFactory.php new file mode 100644 index 00000000..b6e34cca --- /dev/null +++ b/src/JsonRpc/MessageFactory.php @@ -0,0 +1,119 @@ + + */ +final class MessageFactory +{ + /** + * Registry of all known messages. + * + * @var array> + */ + private const REGISTERED_MESSAGES = [ + Notification\CancelledNotification::class, + Notification\InitializedNotification::class, + Notification\LoggingMessageNotification::class, + Notification\ProgressNotification::class, + Notification\PromptListChangedNotification::class, + Notification\ResourceListChangedNotification::class, + Notification\ResourceUpdatedNotification::class, + Notification\RootsListChangedNotification::class, + Notification\ToolListChangedNotification::class, + Request\CallToolRequest::class, + Request\CompletionCompleteRequest::class, + Request\CreateSamplingMessageRequest::class, + Request\GetPromptRequest::class, + Request\InitializeRequest::class, + Request\ListPromptsRequest::class, + Request\ListResourcesRequest::class, + Request\ListResourceTemplatesRequest::class, + Request\ListRootsRequest::class, + Request\ListToolsRequest::class, + Request\PingRequest::class, + Request\ReadResourceRequest::class, + Request\ResourceSubscribeRequest::class, + Request\ResourceUnsubscribeRequest::class, + Request\SetLogLevelRequest::class, + ]; + + /** + * @param array> $registeredMessages + */ + public function __construct( + private readonly array $registeredMessages, + ) { + foreach ($this->registeredMessages as $message) { + if (!is_subclass_of($message, HasMethodInterface::class)) { + throw new InvalidArgumentException(\sprintf('Message classes must implement %s.', HasMethodInterface::class)); + } + } + } + + /** + * Creates a new Factory instance with the all the protocol's default notifications and requests. + */ + public static function make(): self + { + return new self(self::REGISTERED_MESSAGES); + } + + /** + * @return iterable + * + * @throws \JsonException When the input string is not valid JSON + */ + public function create(string $input): iterable + { + $data = json_decode($input, true, flags: \JSON_THROW_ON_ERROR); + + if ('{' === $input[0]) { + $data = [$data]; + } + + foreach ($data as $message) { + if (!isset($message['method']) || !\is_string($message['method'])) { + yield new InvalidInputMessageException('Invalid JSON-RPC request, missing valid "method".'); + continue; + } + + try { + yield $this->getType($message['method'])::fromArray($message); + } catch (InvalidInputMessageException $e) { + yield $e; + continue; + } + } + } + + /** + * @return class-string + */ + private function getType(string $method): string + { + foreach (self::REGISTERED_MESSAGES as $type) { + if ($type::getMethod() === $method) { + return $type; + } + } + + throw new InvalidInputMessageException(\sprintf('Invalid JSON-RPC request, unknown method "%s".', $method)); + } +} diff --git a/src/Message/Error.php b/src/Message/Error.php deleted file mode 100644 index 400b3555..00000000 --- a/src/Message/Error.php +++ /dev/null @@ -1,76 +0,0 @@ - - */ -final class Error implements \JsonSerializable -{ - public const INVALID_REQUEST = -32600; - public const METHOD_NOT_FOUND = -32601; - public const INVALID_PARAMS = -32602; - public const INTERNAL_ERROR = -32603; - public const PARSE_ERROR = -32700; - public const RESOURCE_NOT_FOUND = -32002; - - public function __construct( - public readonly string|int $id, - public readonly int $code, - public readonly string $message, - ) { - } - - public static function invalidRequest(string|int $id, string $message = 'Invalid Request'): self - { - return new self($id, self::INVALID_REQUEST, $message); - } - - public static function methodNotFound(string|int $id, string $message = 'Method not found'): self - { - return new self($id, self::METHOD_NOT_FOUND, $message); - } - - public static function invalidParams(string|int $id, string $message = 'Invalid params'): self - { - return new self($id, self::INVALID_PARAMS, $message); - } - - public static function internalError(string|int $id, string $message = 'Internal error'): self - { - return new self($id, self::INTERNAL_ERROR, $message); - } - - public static function parseError(string|int $id, string $message = 'Parse error'): self - { - return new self($id, self::PARSE_ERROR, $message); - } - - /** - * @return array{ - * jsonrpc: string, - * id: string|int, - * error: array{code: int, message: string} - * } - */ - public function jsonSerialize(): array - { - return [ - 'jsonrpc' => '2.0', - 'id' => $this->id, - 'error' => [ - 'code' => $this->code, - 'message' => $this->message, - ], - ]; - } -} diff --git a/src/Message/Factory.php b/src/Message/Factory.php deleted file mode 100644 index 0d78449c..00000000 --- a/src/Message/Factory.php +++ /dev/null @@ -1,44 +0,0 @@ - - */ -final class Factory -{ - /** - * @return iterable - * - * @throws \JsonException When the input string is not valid JSON - */ - public function create(string $input): iterable - { - $data = json_decode($input, true, flags: \JSON_THROW_ON_ERROR); - - if ('{' === $input[0]) { - $data = [$data]; - } - - foreach ($data as $message) { - if (!isset($message['method'])) { - yield new InvalidInputMessageException('Invalid JSON-RPC request, missing "method".'); - } elseif (str_starts_with((string) $message['method'], 'notifications/')) { - yield Notification::from($message); - } else { - yield Request::from($message); - } - } - } -} diff --git a/src/Message/Notification.php b/src/Message/Notification.php deleted file mode 100644 index 76c3c4c3..00000000 --- a/src/Message/Notification.php +++ /dev/null @@ -1,55 +0,0 @@ - - */ -final class Notification implements \JsonSerializable, \Stringable -{ - /** - * @param array|null $params - */ - public function __construct( - public readonly string $method, - public readonly ?array $params = null, - ) { - } - - /** - * @param array{method: string, params?: array} $data - */ - public static function from(array $data): self - { - return new self( - $data['method'], - $data['params'] ?? null, - ); - } - - /** - * @return array{jsonrpc: string, method: string, params: array|null} - */ - public function jsonSerialize(): array - { - return [ - 'jsonrpc' => '2.0', - 'method' => $this->method, - 'params' => $this->params, - ]; - } - - public function __toString(): string - { - return \sprintf('%s', $this->method); - } -} diff --git a/src/Message/Request.php b/src/Message/Request.php deleted file mode 100644 index 780971af..00000000 --- a/src/Message/Request.php +++ /dev/null @@ -1,58 +0,0 @@ - - */ -final class Request implements \JsonSerializable, \Stringable -{ - /** - * @param array|null $params - */ - public function __construct( - public readonly int|string $id, - public readonly string $method, - public readonly ?array $params = null, - ) { - } - - /** - * @param array{id: string|int, method: string, params?: array} $data - */ - public static function from(array $data): self - { - return new self( - $data['id'], - $data['method'], - $data['params'] ?? null, - ); - } - - /** - * @return array{jsonrpc: string, id: string|int, method: string, params: array|null} - */ - public function jsonSerialize(): array - { - return [ - 'jsonrpc' => '2.0', - 'id' => $this->id, - 'method' => $this->method, - 'params' => $this->params, - ]; - } - - public function __toString(): string - { - return \sprintf('%s: %s', $this->id, $this->method); - } -} diff --git a/src/Message/Response.php b/src/Message/Response.php deleted file mode 100644 index 25d3d458..00000000 --- a/src/Message/Response.php +++ /dev/null @@ -1,39 +0,0 @@ - - */ -final class Response implements \JsonSerializable -{ - /** - * @param array $result - */ - public function __construct( - public readonly string|int $id, - public readonly array $result = [], - ) { - } - - /** - * @return array{jsonrpc: string, id: string|int, result: array} - */ - public function jsonSerialize(): array - { - return [ - 'jsonrpc' => '2.0', - 'id' => $this->id, - 'result' => $this->result, - ]; - } -} diff --git a/src/Schema/Constants.php b/src/Schema/Constants.php deleted file mode 100644 index f3c2866f..00000000 --- a/src/Schema/Constants.php +++ /dev/null @@ -1,29 +0,0 @@ - - */ -interface Constants -{ - public const LATEST_PROTOCOL_VERSION = '2025-03-26'; - public const JSONRPC_VERSION = '2.0'; - - public const PARSE_ERROR = -32700; - public const INVALID_REQUEST = -32600; - public const METHOD_NOT_FOUND = -32601; - public const INVALID_PARAMS = -32602; - public const INTERNAL_ERROR = -32603; - - public const SERVER_ERROR = -32000; -} diff --git a/src/Schema/Content/TextResourceContents.php b/src/Schema/Content/TextResourceContents.php index 49fc4eb0..04880f59 100644 --- a/src/Schema/Content/TextResourceContents.php +++ b/src/Schema/Content/TextResourceContents.php @@ -61,7 +61,7 @@ public function jsonSerialize(): array { return [ 'text' => $this->text, - ...$this->jsonSerialize(), + ...parent::jsonSerialize(), ]; } } diff --git a/src/Schema/JsonRpc/BatchRequest.php b/src/Schema/JsonRpc/BatchRequest.php deleted file mode 100644 index 936f3220..00000000 --- a/src/Schema/JsonRpc/BatchRequest.php +++ /dev/null @@ -1,164 +0,0 @@ - - */ -class BatchRequest implements MessageInterface, \Countable -{ - /** - * Create a new JSON-RPC 2.0 batch of requests/notifications. - * - * @param array $items - */ - public function __construct(public readonly array $items) - { - foreach ($items as $item) { - if (!($item instanceof Request || $item instanceof Notification)) { - throw new InvalidArgumentException('All items in BatchRequest must be instances of Request or Notification.'); - } - } - } - - /** - * @param array $data - */ - public static function fromArray(array $data): self - { - if (empty($data)) { - throw new InvalidArgumentException('BatchRequest data array must not be empty.'); - } - - $items = []; - foreach ($data as $itemData) { - if (!\is_array($itemData)) { - throw new InvalidArgumentException('BatchRequest item data must be an array.'); - } - if (isset($itemData['id'])) { - $items[] = Request::fromArray($itemData); - } elseif (isset($itemData['method'])) { - $items[] = Notification::fromArray($itemData); - } else { - throw new InvalidArgumentException('Invalid item in BatchRequest data: missing "method" or "id".'); - } - } - - return new self($items); - } - - public function getId(): string|int - { - foreach ($this->items as $item) { - if ($item instanceof Request) { - return $item->getId(); - } - } - - return ''; - } - - /** - * Check if this batch has any requests. - */ - public function hasRequests(): bool - { - $hasRequests = false; - foreach ($this->items as $item) { - if ($item instanceof Request) { - $hasRequests = true; - break; - } - } - - return $hasRequests; - } - - /** - * Check if this batch has any notifications. - */ - public function hasNotifications(): bool - { - $hasNotifications = false; - foreach ($this->items as $item) { - if ($item instanceof Notification) { - $hasNotifications = true; - break; - } - } - - return $hasNotifications; - } - - /** - * Get all requests in this batch. - * - * @return array - */ - public function getRequests(): array - { - return array_filter($this->items, fn ($item) => $item instanceof Request); - } - - /** - * Get all notifications in this batch. - * - * @return array - */ - public function getNotifications(): array - { - return array_filter($this->items, fn ($item) => $item instanceof Notification); - } - - /** - * Get all elements in this batch. - * - * @return array - */ - public function getAll(): array - { - return $this->items; - } - - /** - * Count the total number of elements in this batch. - */ - public function count(): int - { - return \count($this->items); - } - - public function nRequests(): int - { - return \count($this->getRequests()); - } - - public function nNotifications(): int - { - return \count($this->getNotifications()); - } - - /** - * @return array - */ - public function jsonSerialize(): array - { - return $this->items; - } -} diff --git a/src/Schema/JsonRpc/BatchResponse.php b/src/Schema/JsonRpc/BatchResponse.php deleted file mode 100644 index 3c58baf5..00000000 --- a/src/Schema/JsonRpc/BatchResponse.php +++ /dev/null @@ -1,138 +0,0 @@ - - */ -class BatchResponse implements MessageInterface, \Countable -{ - /** - * @param array $items the individual responses/errors in this batch - */ - public function __construct( - public array $items, - ) { - foreach ($items as $item) { - if (!($item instanceof Response || $item instanceof Error)) { - throw new InvalidArgumentException('All items in BatchResponse must be instances of Response or Error.'); - } - } - } - - /** - * @param array $data - */ - public static function fromArray(array $data): self - { - if (empty($data)) { - throw new InvalidArgumentException('BatchResponse data array must not be empty.'); - } - - $items = []; - foreach ($data as $itemData) { - if (!\is_array($itemData)) { - throw new InvalidArgumentException('BatchResponse item data must be an array.'); - } - if (isset($itemData['id'])) { - $items[] = Response::fromArray($itemData); - } elseif (isset($itemData['error'])) { - $items[] = Error::fromArray($itemData); - } else { - throw new InvalidArgumentException('Invalid item in BatchResponse data: missing "id" or "error".'); - } - } - - return new self($items); - } - - public function getId(): string|int - { - foreach ($this->items as $item) { - if ($item instanceof Response) { - return $item->getId(); - } - } - - throw new InvalidArgumentException('BatchResponse does not contain any Response items with an "id".'); - } - - public function hasResponses(): bool - { - $hasResponses = false; - foreach ($this->items as $item) { - if ($item instanceof Response) { - $hasResponses = true; - break; - } - } - - return $hasResponses; - } - - public function hasErrors(): bool - { - $hasErrors = false; - foreach ($this->items as $item) { - if ($item instanceof Error) { - $hasErrors = true; - break; - } - } - - return $hasErrors; - } - - /** - * @return Response[] - */ - public function getResponses(): array - { - return array_filter($this->items, fn ($item) => $item instanceof Response); - } - - /** - * @return Error[] - */ - public function getErrors(): array - { - return array_filter($this->items, fn ($item) => $item instanceof Error); - } - - /** - * @return Error[]|Response[] - */ - public function getAll(): array - { - return $this->items; - } - - public function count(): int - { - return \count($this->items); - } - - /** - * @return Error[]|Response[] - */ - public function jsonSerialize(): array - { - return $this->items; - } -} diff --git a/src/Schema/JsonRpc/Error.php b/src/Schema/JsonRpc/Error.php index eaf62456..64cb8455 100644 --- a/src/Schema/JsonRpc/Error.php +++ b/src/Schema/JsonRpc/Error.php @@ -12,7 +12,6 @@ namespace Mcp\Schema\JsonRpc; use Mcp\Exception\InvalidArgumentException; -use Mcp\Schema\Constants; /** * A response to a request that indicates an error occurred. @@ -29,6 +28,14 @@ */ class Error implements MessageInterface { + public const PARSE_ERROR = -32700; + public const INVALID_REQUEST = -32600; + public const METHOD_NOT_FOUND = -32601; + public const INVALID_PARAMS = -32602; + public const INTERNAL_ERROR = -32603; + public const SERVER_ERROR = -32000; + public const RESOURCE_NOT_FOUND = -32002; + /** * @param int $code the error type that occurred * @param string $message a short description of the error @@ -47,7 +54,7 @@ public function __construct( */ public static function fromArray(array $data): self { - if (!isset($data['jsonrpc']) || Constants::JSONRPC_VERSION !== $data['jsonrpc']) { + if (!isset($data['jsonrpc']) || MessageInterface::JSONRPC_VERSION !== $data['jsonrpc']) { throw new InvalidArgumentException('Invalid or missing "jsonrpc" in Error data.'); } if (!isset($data['id']) || !\is_string($data['id'])) { @@ -65,32 +72,32 @@ public static function fromArray(array $data): self public static function forParseError(string $message, string|int $id = ''): self { - return new self($id, Constants::PARSE_ERROR, $message); + return new self($id, self::PARSE_ERROR, $message); } public static function forInvalidRequest(string $message, string|int $id = ''): self { - return new self($id, Constants::INVALID_REQUEST, $message); + return new self($id, self::INVALID_REQUEST, $message); } public static function forMethodNotFound(string $message, string|int $id = ''): self { - return new self($id, Constants::METHOD_NOT_FOUND, $message); + return new self($id, self::METHOD_NOT_FOUND, $message); } public static function forInvalidParams(string $message, string|int $id = ''): self { - return new self($id, Constants::INVALID_PARAMS, $message); + return new self($id, self::INVALID_PARAMS, $message); } public static function forInternalError(string $message, string|int $id = ''): self { - return new self($id, Constants::INTERNAL_ERROR, $message); + return new self($id, self::INTERNAL_ERROR, $message); } public static function forServerError(string $message, string|int $id = ''): self { - return new self($id, Constants::SERVER_ERROR, $message); + return new self($id, self::SERVER_ERROR, $message); } public function getId(): string|int @@ -121,7 +128,7 @@ public function jsonSerialize(): array } return [ - 'jsonrpc' => Constants::JSONRPC_VERSION, + 'jsonrpc' => MessageInterface::JSONRPC_VERSION, 'id' => $this->id, 'error' => $error, ]; diff --git a/src/Schema/JsonRpc/HasMethodInterface.php b/src/Schema/JsonRpc/HasMethodInterface.php new file mode 100644 index 00000000..18286b72 --- /dev/null +++ b/src/Schema/JsonRpc/HasMethodInterface.php @@ -0,0 +1,29 @@ + + */ +interface HasMethodInterface +{ + public static function getMethod(): string; + + /** + * @param array $data + */ + public static function fromArray(array $data): self; +} diff --git a/src/Schema/JsonRpc/MessageInterface.php b/src/Schema/JsonRpc/MessageInterface.php index 0f73a6bb..919bc16f 100644 --- a/src/Schema/JsonRpc/MessageInterface.php +++ b/src/Schema/JsonRpc/MessageInterface.php @@ -18,4 +18,5 @@ */ interface MessageInterface extends \JsonSerializable { + public const JSONRPC_VERSION = '2.0'; } diff --git a/src/Schema/JsonRpc/Notification.php b/src/Schema/JsonRpc/Notification.php index 07cf98d6..b95deb93 100644 --- a/src/Schema/JsonRpc/Notification.php +++ b/src/Schema/JsonRpc/Notification.php @@ -12,7 +12,6 @@ namespace Mcp\Schema\JsonRpc; use Mcp\Exception\InvalidArgumentException; -use Mcp\Schema\Constants; /** * @phpstan-type NotificationData array{ @@ -23,17 +22,14 @@ * * @author Kyrian Obikwelu */ -class Notification implements MessageInterface +abstract class Notification implements HasMethodInterface, MessageInterface { /** - * @param string $method the name of the method to be invoked - * @param ?array $params parameters for the method + * @var array|null */ - public function __construct( - public readonly string $method, - public readonly ?array $params = null, - ) { - } + protected ?array $meta; + + abstract public static function getMethod(): string; /** * @param NotificationData $data @@ -51,16 +47,19 @@ public static function fromArray(array $data): self throw new InvalidArgumentException('"params" for Notification must be an array/object or null.'); } - return new self($data['method'], $params); + $notification = static::fromParams($params); + + if (isset($data['params']['_meta'])) { + $notification->meta = $data['params']['_meta']; + } + + return $notification; } /** - * @return null + * @param array|null $params */ - public function getId() - { - return null; - } + abstract protected static function fromParams(?array $params): self; /** * @return NotificationData @@ -68,13 +67,22 @@ public function getId() public function jsonSerialize(): array { $array = [ - 'jsonrpc' => Constants::JSONRPC_VERSION, - 'method' => $this->method, + 'jsonrpc' => MessageInterface::JSONRPC_VERSION, + 'method' => static::getMethod(), ]; - if (null !== $this->params) { - $array['params'] = $this->params; + if (null !== $params = $this->getParams()) { + $array['params'] = $params; + } + + if (null !== $this->meta && !isset($params['meta'])) { + $array['params']['_meta'] = $this->meta; } return $array; } + + /** + * @return array|null + */ + abstract protected function getParams(): ?array; } diff --git a/src/Schema/JsonRpc/Parser.php b/src/Schema/JsonRpc/Parser.php deleted file mode 100644 index 5a39e9eb..00000000 --- a/src/Schema/JsonRpc/Parser.php +++ /dev/null @@ -1,80 +0,0 @@ - - */ -class Parser -{ - /** - * Parses a raw JSON string into a JSON-RPC Message object (Request, Notification, Response, Error, or Batch variants). - * - * This method determines if the incoming message is a request-like message (Request, Notification, BatchRequest) - * or a response-like message (Response, Error, BatchResponse) based on the presence of 'method' vs 'result'/'error'. - * - * @param string $json the raw JSON string to parse - * - * @return MessageInterface a specific instance of Request, Notification, Response, Error, BatchRequest, or BatchResponse - * - * @throws \JsonException if the string is not valid JSON - * @throws InvalidArgumentException if the JSON structure does not conform to a recognizable JSON-RPC message type - */ - public static function parse(string $json): MessageInterface - { - $data = json_decode($json, true, 512, \JSON_THROW_ON_ERROR); - - if (!\is_array($data)) { - throw new InvalidArgumentException('Invalid JSON-RPC message: Root must be an object or array.'); - } - - if (array_is_list($data) && !empty($data)) { - $firstItem = $data[0]; - if (!\is_array($firstItem)) { - throw new InvalidArgumentException('Invalid JSON-RPC batch: Items must be objects.'); - } - - if (isset($firstItem['method'])) { - return BatchRequest::fromArray($data); - } elseif (isset($firstItem['id']) && (isset($firstItem['result']) || isset($firstItem['error']))) { - return BatchResponse::fromArray($data); - } else { - throw new InvalidArgumentException('Invalid JSON-RPC batch: Items are not recognizable requests or responses.'); - } - } - - if (!isset($data['jsonrpc']) || Constants::JSONRPC_VERSION !== $data['jsonrpc']) { - throw new InvalidArgumentException('Invalid or missing "jsonrpc" version. Must be "'.Constants::JSONRPC_VERSION.'".'); - } - - if (isset($data['method'])) { - if (isset($data['id'])) { - return Request::fromArray($data); - } else { - return Notification::fromArray($data); - } - } elseif (isset($data['id'])) { - if (\array_key_exists('result', $data)) { - return Response::fromArray($data); - } elseif (isset($data['error'])) { - return Error::fromArray($data); - } else { - throw new InvalidArgumentException('Invalid JSON-RPC response/error: Missing "result" or "error" field for message with "id".'); - } - } - - throw new InvalidArgumentException('Unrecognized JSON-RPC message structure.'); - } -} diff --git a/src/Schema/JsonRpc/Request.php b/src/Schema/JsonRpc/Request.php index 24e2cc96..bad6e2e5 100644 --- a/src/Schema/JsonRpc/Request.php +++ b/src/Schema/JsonRpc/Request.php @@ -12,7 +12,6 @@ namespace Mcp\Schema\JsonRpc; use Mcp\Exception\InvalidArgumentException; -use Mcp\Schema\Constants; /** * @phpstan-type RequestData array{ @@ -24,31 +23,22 @@ * * @author Kyrian Obikwelu */ -class Request implements MessageInterface +abstract class Request implements HasMethodInterface, MessageInterface { + protected string|int $id; /** - * @param string|int $id a unique identifier for the request - * @param string $method the name of the method to be invoked - * @param array|null $params parameters for the method + * @var array|null */ - public function __construct( - public readonly string|int $id, - public readonly string $method, - public readonly ?array $params = null, - ) { - } + protected ?array $meta; - public function getId(): string|int - { - return $this->id; - } + abstract public static function getMethod(): string; /** * @param RequestData $data */ public static function fromArray(array $data): self { - if (($data['jsonrpc'] ?? null) !== Constants::JSONRPC_VERSION) { + if (($data['jsonrpc'] ?? null) !== MessageInterface::JSONRPC_VERSION) { throw new InvalidArgumentException('Invalid or missing "jsonrpc" version for Request.'); } if (!isset($data['id']) || !\is_string($data['id']) && !\is_int($data['id'])) { @@ -65,7 +55,24 @@ public static function fromArray(array $data): self throw new InvalidArgumentException('"params" for Request must be an array/object or null.'); } - return new self($data['id'], $data['method'], $params); + $request = static::fromParams($params); + $request->id = $data['id']; + + if (isset($data['params']['_meta'])) { + $request->meta = $data['params']['_meta']; + } + + return $request; + } + + /** + * @param array|null $params + */ + abstract protected static function fromParams(?array $params): self; + + public function getId(): string|int + { + return $this->id; } /** @@ -74,14 +81,23 @@ public static function fromArray(array $data): self public function jsonSerialize(): array { $array = [ - 'jsonrpc' => Constants::JSONRPC_VERSION, + 'jsonrpc' => MessageInterface::JSONRPC_VERSION, 'id' => $this->id, - 'method' => $this->method, + 'method' => static::getMethod(), ]; - if (null !== $this->params) { - $array['params'] = $this->params; + if (null !== $params = $this->getParams()) { + $array['params'] = $params; + } + + if (null !== $this->meta && !isset($params['meta'])) { + $array['params']['_meta'] = $this->meta; } return $array; } + + /** + * @return array|null + */ + abstract protected function getParams(): ?array; } diff --git a/src/Schema/JsonRpc/Response.php b/src/Schema/JsonRpc/Response.php index cfd82f56..6e5ae2c6 100644 --- a/src/Schema/JsonRpc/Response.php +++ b/src/Schema/JsonRpc/Response.php @@ -12,7 +12,6 @@ namespace Mcp\Schema\JsonRpc; use Mcp\Exception\InvalidArgumentException; -use Mcp\Schema\Constants; /** * @author Kyrian Obikwelu @@ -45,7 +44,7 @@ public function getId(): string|int */ public static function fromArray(array $data): self { - if (($data['jsonrpc'] ?? null) !== Constants::JSONRPC_VERSION) { + if (($data['jsonrpc'] ?? null) !== MessageInterface::JSONRPC_VERSION) { throw new InvalidArgumentException('Invalid or missing "jsonrpc" version for Response.'); } if (!isset($data['id'])) { @@ -71,7 +70,7 @@ public static function fromArray(array $data): self public function jsonSerialize(): array { return [ - 'jsonrpc' => Constants::JSONRPC_VERSION, + 'jsonrpc' => MessageInterface::JSONRPC_VERSION, 'id' => $this->id, 'result' => $this->result, ]; diff --git a/src/Schema/ModelPreferences.php b/src/Schema/ModelPreferences.php index 3e7c9b53..f2f30dbc 100644 --- a/src/Schema/ModelPreferences.php +++ b/src/Schema/ModelPreferences.php @@ -24,6 +24,13 @@ * up to the client to decide how to interpret these preferences and how to * balance them against other considerations. * + * @phpstan-type ModelPreferencesData array{ + * hints?: ModelHint[], + * costPriority?: float, + * speedPriority?: float, + * intelligencePriority?: float, + * } + * * @author Kyrian Obikwelu */ class ModelPreferences implements \JsonSerializable @@ -50,12 +57,20 @@ public function __construct( } /** - * @return array{ - * hints?: ModelHint[], - * costPriority?: float, - * speedPriority?: float, - * intelligencePriority?: float, - * } + * @param ModelPreferencesData $preferences + */ + public static function fromArray(array $preferences): self + { + return new self( + $preferences['hints'] ?? null, + $preferences['costPriority'] ?? null, + $preferences['speedPriority'] ?? null, + $preferences['intelligencePriority'] ?? null, + ); + } + + /** + * @return ModelPreferencesData */ public function jsonSerialize(): array { diff --git a/src/Schema/Notification/CancelledNotification.php b/src/Schema/Notification/CancelledNotification.php index 78066767..14d4e592 100644 --- a/src/Schema/Notification/CancelledNotification.php +++ b/src/Schema/Notification/CancelledNotification.php @@ -28,40 +28,37 @@ class CancelledNotification extends Notification { /** - * @param string $requestId The ID of the request that is being cancelled. This MUST correspond to the ID of a request previously issued in the same direction. - * @param ?string $reason An optional string describing the reason for the cancellation. This MAY be logged or presented to the user. - * @param ?array $_meta additional metadata about the notification + * @param string|int $requestId The ID of the request that is being cancelled. This MUST correspond to the ID of a request previously issued in the same direction. + * @param ?string $reason An optional string describing the reason for the cancellation. This MAY be logged or presented to the user. */ public function __construct( - public readonly string $requestId, + public readonly string|int $requestId, public readonly ?string $reason = null, - public readonly ?array $_meta = null, ) { - $params = [ - 'requestId' => $this->requestId, - ]; - if (null !== $this->reason) { - $params['reason'] = $this->reason; - } - if (null !== $_meta) { - $params['_meta'] = $_meta; - } + } - parent::__construct('notifications/cancelled', $params); + public static function getMethod(): string + { + return 'notifications/cancelled'; } - public static function fromNotification(Notification $notification): self + protected static function fromParams(?array $params): Notification { - if ('notifications/cancelled' !== $notification->method) { - throw new InvalidArgumentException('Notification is not a notifications/cancelled notification'); + if (null === $params || !isset($params['requestId']) || (!\is_string($params['requestId']) && !\is_int($params['requestId']))) { + throw new InvalidArgumentException('Invalid or missing "requestId" parameter for "notifications/cancelled" notification.'); } - $params = $notification->params; + return new self($params['requestId'], $params['reason'] ?? null); + } + + protected function getParams(): ?array + { + $params = ['requestId' => $this->requestId]; - if (!isset($params['requestId']) || !\is_string($params['requestId'])) { - throw new InvalidArgumentException('Missing or invalid requestId parameter for notifications/cancelled notification'); + if (null !== $this->reason) { + $params['reason'] = $this->reason; } - return new self($params['requestId'], $params['reason'] ?? null, $params['_meta'] ?? null); + return $params; } } diff --git a/src/Schema/Notification/InitializedNotification.php b/src/Schema/Notification/InitializedNotification.php index 135fd74a..8e733275 100644 --- a/src/Schema/Notification/InitializedNotification.php +++ b/src/Schema/Notification/InitializedNotification.php @@ -11,7 +11,6 @@ namespace Mcp\Schema\Notification; -use Mcp\Exception\InvalidArgumentException; use Mcp\Schema\JsonRpc\Notification; /** @@ -21,27 +20,18 @@ */ class InitializedNotification extends Notification { - /** - * @param array|null $_meta - */ - public function __construct( - public readonly ?array $_meta = null, - ) { - $params = []; - - if (null !== $_meta) { - $params['_meta'] = $_meta; - } - - parent::__construct('notifications/initialized', $params); + public static function getMethod(): string + { + return 'notifications/initialized'; } - public static function fromNotification(Notification $notification): self + public static function fromParams(?array $params): self { - if ('notifications/initialized' !== $notification->method) { - throw new InvalidArgumentException('Notification is not a notifications/initialized notification'); - } + return new self(); + } - return new self($notification->params['_meta'] ?? null); + protected function getParams(): ?array + { + return null; } } diff --git a/src/Schema/Notification/LoggingMessageNotification.php b/src/Schema/Notification/LoggingMessageNotification.php index 35793120..29e00fca 100644 --- a/src/Schema/Notification/LoggingMessageNotification.php +++ b/src/Schema/Notification/LoggingMessageNotification.php @@ -21,51 +21,49 @@ class LoggingMessageNotification extends Notification { /** - * @param LoggingLevel $level the severity of this log message - * @param mixed $data The data to be logged, such as a string message or an object. Any JSON serializable type is allowed here. - * @param string $logger an optional name of the logger issuing this message - * @param ?array $_meta optional metadata to include in the notification + * @param LoggingLevel $level the severity of this log message + * @param mixed $data The data to be logged, such as a string message or an object. Any JSON serializable type is allowed here. + * @param ?string $logger an optional name of the logger issuing this message */ public function __construct( public readonly LoggingLevel $level, public readonly mixed $data, public readonly ?string $logger = null, - public readonly ?array $_meta = null, ) { - $params = [ - 'level' => $level->value, - 'data' => \is_string($data) ? $data : json_encode($data), - ]; - - if (null !== $logger) { - $params['logger'] = $logger; - } - - if (null !== $_meta) { - $params['_meta'] = $_meta; - } - - parent::__construct('notifications/message', $params); } - public static function fromNotification(Notification $notification): self + public static function getMethod(): string { - if ('notifications/message' !== $notification->method) { - throw new InvalidArgumentException('Notification is not a notifications/message notification'); - } - - $params = $notification->params; + return 'notifications/message'; + } + protected static function fromParams(?array $params): Notification + { if (!isset($params['level']) || !\is_string($params['level'])) { - throw new InvalidArgumentException('Missing or invalid level parameter for notifications/message notification'); + throw new InvalidArgumentException('Missing or invalid "level" parameter for "notifications/message" notification.'); } if (!isset($params['data'])) { - throw new InvalidArgumentException('Missing data parameter for notifications/message notification'); + throw new InvalidArgumentException('Missing "data" parameter for "notifications/message" notification.'); } $level = LoggingLevel::from($params['level']); + $data = \is_string($params['data']) ? $params['data'] : json_encode($params['data']); + + return new self($level, $data, $params['logger'] ?? null); + } + + protected function getParams(): ?array + { + $params = [ + 'level' => $this->level->value, + 'data' => $this->data, + ]; + + if (null !== $this->logger) { + $params['logger'] = $this->logger; + } - return new self($level, $params['data'], $params['logger'] ?? null, $params['_meta'] ?? null); + return $params; } } diff --git a/src/Schema/Notification/ProgressNotification.php b/src/Schema/Notification/ProgressNotification.php index 59091389..d3365a9f 100644 --- a/src/Schema/Notification/ProgressNotification.php +++ b/src/Schema/Notification/ProgressNotification.php @@ -22,45 +22,59 @@ class ProgressNotification extends Notification { /** - * @param string|int $progressToken the progress token which was given in the initial request, used to - * associate this notification with the request that is proceeding - * @param float $progress The progress thus far. This should increase every time progress is - * made, even if the total is unknown. - * @param ?float $total total number of items to process (or total progress required), if known - * @param ?string $message an optional message describing the current progress - * @param ?array $_meta optional metadata to include in the notification + * @param string|int $progressToken the progress token which was given in the initial request, used to + * associate this notification with the request that is proceeding + * @param float $progress The progress thus far. This should increase every time progress is + * made, even if the total is unknown. + * @param ?float $total total number of items to process (or total progress required), if known + * @param ?string $message an optional message describing the current progress */ public function __construct( public readonly string|int $progressToken, public readonly float $progress, public readonly ?float $total = null, public readonly ?string $message = null, - public readonly ?array $_meta = null, ) { - $params = []; - if (null !== $_meta) { - $params['_meta'] = $_meta; - } + } - parent::__construct('notifications/progress', $params); + public static function getMethod(): string + { + return 'notifications/progress'; } - public static function fromNotification(Notification $notification): self + protected static function fromParams(?array $params): Notification { - if ('notifications/progress' !== $notification->method) { - throw new InvalidArgumentException('Notification is not a notifications/progress notification'); + if (!isset($params['progressToken']) || !\is_string($params['progressToken'])) { + throw new InvalidArgumentException('Missing or invalid "progressToken" parameter for "notifications/progress" notification.'); + } + + if (!isset($params['progress']) || !\is_float($params['progress'])) { + throw new InvalidArgumentException('Missing or invalid "progress" parameter for "notifications/progress" notification.'); } - $params = $notification->params; + return new self( + $params['progressToken'], + $params['progress'], + $params['total'] ?? null, + $params['message'] ?? null, + ); + } - if (!isset($params['progressToken']) || !\is_string($params['progressToken'])) { - throw new InvalidArgumentException('Missing or invalid progressToken parameter for notifications/progress notification'); + protected function getParams(): ?array + { + $params = [ + 'progressToken' => $this->progressToken, + 'progress' => $this->progress, + ]; + + if (null !== $this->total) { + $params['total'] = $this->total; } - if (!isset($params['progress']) || !\is_float($params['progress'])) { - throw new InvalidArgumentException('Missing or invalid progress parameter for notifications/progress notification'); + if (null !== $this->message) { + $params['message'] = $this->message; } - return new self($params['progressToken'], $params['progress'], $params['total'] ?? null, $params['message'] ?? null, $params['_meta'] ?? null); + return $params; } } diff --git a/src/Schema/Notification/PromptListChangedNotification.php b/src/Schema/Notification/PromptListChangedNotification.php index e2ab2d3d..1557e1e3 100644 --- a/src/Schema/Notification/PromptListChangedNotification.php +++ b/src/Schema/Notification/PromptListChangedNotification.php @@ -11,7 +11,6 @@ namespace Mcp\Schema\Notification; -use Mcp\Exception\InvalidArgumentException; use Mcp\Schema\JsonRpc\Notification; /** @@ -21,26 +20,18 @@ */ class PromptListChangedNotification extends Notification { - /** - * @param array|null $_meta - */ - public function __construct( - public readonly ?array $_meta = null, - ) { - $params = []; - if (null !== $_meta) { - $params['_meta'] = $_meta; - } - - parent::__construct('notifications/prompts/list_changed', $params); + public static function getMethod(): string + { + return 'notifications/prompts/list_changed'; } - public static function fromNotification(Notification $notification): self + protected static function fromParams(?array $params): Notification { - if ('notifications/prompts/list_changed' !== $notification->method) { - throw new InvalidArgumentException('Notification is not a notifications/prompts/list_changed notification'); - } + return new self(); + } - return new self($notification->params['_meta'] ?? null); + protected function getParams(): ?array + { + return null; } } diff --git a/src/Schema/Notification/ResourceListChangedNotification.php b/src/Schema/Notification/ResourceListChangedNotification.php index 6a03c5d2..9a15e12c 100644 --- a/src/Schema/Notification/ResourceListChangedNotification.php +++ b/src/Schema/Notification/ResourceListChangedNotification.php @@ -11,7 +11,6 @@ namespace Mcp\Schema\Notification; -use Mcp\Exception\InvalidArgumentException; use Mcp\Schema\JsonRpc\Notification; /** @@ -21,26 +20,18 @@ */ class ResourceListChangedNotification extends Notification { - /** - * @param array|null $_meta - */ - public function __construct( - public readonly ?array $_meta = null, - ) { - $params = []; - if (null !== $_meta) { - $params['_meta'] = $_meta; - } - - parent::__construct('notifications/resources/list_changed', $params); + public static function getMethod(): string + { + return 'notifications/resources/list_changed'; } - public static function fromNotification(Notification $notification): self + protected static function fromParams(?array $params): Notification { - if ('notifications/resources/list_changed' !== $notification->method) { - throw new InvalidArgumentException('Notification is not a notifications/resources/list_changed notification'); - } + return new self(); + } - return new self($notification->params['_meta'] ?? null); + protected function getParams(): ?array + { + return null; } } diff --git a/src/Schema/Notification/ResourceUpdatedNotification.php b/src/Schema/Notification/ResourceUpdatedNotification.php index a5d071ed..946d500e 100644 --- a/src/Schema/Notification/ResourceUpdatedNotification.php +++ b/src/Schema/Notification/ResourceUpdatedNotification.php @@ -19,33 +19,29 @@ */ class ResourceUpdatedNotification extends Notification { - /** - * @param array|null $_meta - */ public function __construct( public readonly string $uri, - public readonly ?array $_meta = null, ) { - $params = ['uri' => $uri]; - if (null !== $_meta) { - $params['_meta'] = $_meta; - } + } - parent::__construct('notifications/resources/updated', $params); + public static function getMethod(): string + { + return 'notifications/resources/updated'; } - public static function fromNotification(Notification $notification): self + protected static function fromParams(?array $params): Notification { - if ('notifications/resources/updated' !== $notification->method) { - throw new InvalidArgumentException('Notification is not a notifications/resources/updated notification'); + if (null === $params || !isset($params['uri']) || !\is_string($params['uri'])) { + throw new InvalidArgumentException('Invalid or missing "uri" parameter for notifications/resources/updated notification.'); } - $params = $notification->params; - - if (!isset($params['uri']) || !\is_string($params['uri'])) { - throw new InvalidArgumentException('Missing or invalid uri parameter for notifications/resources/updated notification'); - } + return new self($params['uri']); + } - return new self($params['uri'], $params['_meta'] ?? null); + protected function getParams(): ?array + { + return [ + 'uri' => $this->uri, + ]; } } diff --git a/src/Schema/Notification/RootsListChangedNotification.php b/src/Schema/Notification/RootsListChangedNotification.php index 8c3e8b64..3c95829a 100644 --- a/src/Schema/Notification/RootsListChangedNotification.php +++ b/src/Schema/Notification/RootsListChangedNotification.php @@ -11,7 +11,6 @@ namespace Mcp\Schema\Notification; -use Mcp\Exception\InvalidArgumentException; use Mcp\Schema\JsonRpc\Notification; /** @@ -23,26 +22,18 @@ */ class RootsListChangedNotification extends Notification { - /** - * @param array|null $_meta - */ - public function __construct( - public readonly ?array $_meta = null, - ) { - $params = []; - if (null !== $_meta) { - $params['_meta'] = $_meta; - } - - parent::__construct('notifications/roots/list_changed', $params); + public static function getMethod(): string + { + return 'notifications/roots/list_changed'; } - public static function fromNotification(Notification $notification): self + protected static function fromParams(?array $params): Notification { - if ('notifications/roots/list_changed' !== $notification->method) { - throw new InvalidArgumentException('Notification is not a notifications/roots/list_changed notification'); - } + return new self(); + } - return new self($notification->params['_meta'] ?? null); + protected function getParams(): ?array + { + return null; } } diff --git a/src/Schema/Notification/ToolListChangedNotification.php b/src/Schema/Notification/ToolListChangedNotification.php index 64d42a93..e081df29 100644 --- a/src/Schema/Notification/ToolListChangedNotification.php +++ b/src/Schema/Notification/ToolListChangedNotification.php @@ -11,7 +11,6 @@ namespace Mcp\Schema\Notification; -use Mcp\Exception\InvalidArgumentException; use Mcp\Schema\JsonRpc\Notification; /** @@ -21,26 +20,18 @@ */ class ToolListChangedNotification extends Notification { - /** - * @param array|null $_meta - */ - public function __construct( - public readonly ?array $_meta = null, - ) { - $params = []; - if (null !== $_meta) { - $params['_meta'] = $_meta; - } - - parent::__construct('notifications/tools/list_changed', $params); + public static function getMethod(): string + { + return 'notifications/tools/list_changed'; } - public static function fromNotification(Notification $notification): self + protected static function fromParams(?array $params): Notification { - if ('notifications/tools/list_changed' !== $notification->method) { - throw new InvalidArgumentException('Notification is not a notifications/tools/list_changed notification'); - } + return new self(); + } - return new self($notification->params['_meta'] ?? null); + protected function getParams(): ?array + { + return null; } } diff --git a/src/Schema/Request/CallToolRequest.php b/src/Schema/Request/CallToolRequest.php index 2e3a9724..0bc7fcf3 100644 --- a/src/Schema/Request/CallToolRequest.php +++ b/src/Schema/Request/CallToolRequest.php @@ -22,36 +22,22 @@ class CallToolRequest extends Request { /** - * @param string $name the name of the tool to invoke - * @param array $arguments the arguments to pass to the tool - * @param ?array $_meta optional metadata to include in the request + * @param string $name the name of the tool to invoke + * @param array $arguments the arguments to pass to the tool */ public function __construct( - string|int $id, public readonly string $name, public readonly array $arguments, - public readonly ?array $_meta = null, ) { - $params = [ - 'name' => $name, - 'arguments' => (object) $arguments, - ]; - - if (null !== $_meta) { - $params['_meta'] = $_meta; - } - - parent::__construct($id, 'tools/call', $params); } - public static function fromRequest(Request $request): self + public static function getMethod(): string { - if ('tools/call' !== $request->method) { - throw new InvalidArgumentException('Request is not a call tool request'); - } - - $params = $request->params ?? []; + return 'tools/call'; + } + protected static function fromParams(?array $params): Request + { if (!isset($params['name']) || !\is_string($params['name'])) { throw new InvalidArgumentException('Missing or invalid "name" parameter for tools/call.'); } @@ -67,10 +53,16 @@ public static function fromRequest(Request $request): self } return new self( - $request->id, $params['name'], $arguments, - $params['_meta'] ?? null ); } + + protected function getParams(): ?array + { + return [ + 'name' => $this->name, + 'arguments' => $this->arguments, + ]; + } } diff --git a/src/Schema/Request/CompletionCompleteRequest.php b/src/Schema/Request/CompletionCompleteRequest.php index 3581513e..324b0041 100644 --- a/src/Schema/Request/CompletionCompleteRequest.php +++ b/src/Schema/Request/CompletionCompleteRequest.php @@ -26,34 +26,20 @@ class CompletionCompleteRequest extends Request /** * @param PromptReference|ResourceReference $ref the prompt or resource to complete * @param array{ name: string, value: string } $argument the argument to complete - * @param ?array $_meta optional metadata to include in the request */ public function __construct( - string|int $id, public readonly PromptReference|ResourceReference $ref, public readonly array $argument, - public readonly ?array $_meta = null, ) { - $params = [ - 'ref' => $this->ref, - 'argument' => $this->argument, - ]; - - if (null !== $this->_meta) { - $params['_meta'] = $this->_meta; - } - - parent::__construct($id, 'completion/complete', $params); } - public static function fromRequest(Request $request): self + public static function getMethod(): string { - if ('completion/complete' !== $request->method) { - throw new InvalidArgumentException('Request is not a completion/complete request'); - } - - $params = $request->params; + return 'completion/complete'; + } + protected static function fromParams(?array $params): Request + { if (!isset($params['ref']) || !\is_array($params['ref'])) { throw new InvalidArgumentException('Missing or invalid "ref" parameter for completion/complete.'); } @@ -68,6 +54,14 @@ public static function fromRequest(Request $request): self throw new InvalidArgumentException('Missing or invalid "argument" parameter for completion/complete.'); } - return new self($request->id, $ref, $params['argument'], $params['_meta'] ?? null); + return new self($ref, $params['argument']); + } + + protected function getParams(): ?array + { + return [ + 'ref' => $this->ref, + 'argument' => $this->argument, + ]; } } diff --git a/src/Schema/Request/CreateSamplingMessageRequest.php b/src/Schema/Request/CreateSamplingMessageRequest.php index 5f44e440..131e44e4 100644 --- a/src/Schema/Request/CreateSamplingMessageRequest.php +++ b/src/Schema/Request/CreateSamplingMessageRequest.php @@ -40,10 +40,8 @@ class CreateSamplingMessageRequest extends Request * @param string[]|null $stopSequences A list of sequences to stop sampling at. The client MAY ignore this request. * @param ?array $metadata Optional metadata to pass through to the LLM provider. The format of * this metadata is provider-specific. - * @param ?array $_meta optional metadata to include in the request */ public function __construct( - string|int $id, public readonly array $messages, public readonly int $maxTokens, public readonly ?ModelPreferences $preferences = null, @@ -52,8 +50,43 @@ public function __construct( public readonly ?float $temperature = null, public readonly ?array $stopSequences = null, public readonly ?array $metadata = null, - public readonly ?array $_meta = null, ) { + } + + public static function getMethod(): string + { + return 'sampling/createMessage'; + } + + protected static function fromParams(?array $params): Request + { + if (!isset($params['messages']) || !\is_array($params['messages'])) { + throw new \InvalidArgumentException('Missing or invalid "messages" parameter for sampling/createMessage.'); + } + + if (!isset($params['maxTokens']) || !\is_int($params['maxTokens'])) { + throw new \InvalidArgumentException('Missing or invalid "maxTokens" parameter for sampling/createMessage.'); + } + + $preferences = null; + if (isset($params['preferences'])) { + $preferences = ModelPreferences::fromArray($params['preferences']); + } + + return new self( + $params['messages'], + $params['maxTokens'], + $preferences, + $params['systemPrompt'] ?? null, + $params['includeContext'] ?? null, + $params['temperature'] ?? null, + $params['stopSequences'] ?? null, + $params['metadata'] ?? null, + ); + } + + protected function getParams(): ?array + { $params = [ 'messages' => $this->messages, 'maxTokens' => $this->maxTokens, @@ -83,10 +116,6 @@ public function __construct( $params['metadata'] = $this->metadata; } - if (null !== $this->_meta) { - $params['_meta'] = $this->_meta; - } - - parent::__construct($id, 'sampling/createMessage', $params); + return $params; } } diff --git a/src/Schema/Request/GetPromptRequest.php b/src/Schema/Request/GetPromptRequest.php index 90ea75ed..68954e9c 100644 --- a/src/Schema/Request/GetPromptRequest.php +++ b/src/Schema/Request/GetPromptRequest.php @@ -22,47 +22,47 @@ class GetPromptRequest extends Request { /** - * @param string|int $id the ID of the request to cancel * @param string $name the name of the prompt to get * @param array|null $arguments the arguments to pass to the prompt - * @param ?array $_meta optional metadata to include in the request */ public function __construct( - string|int $id, public readonly string $name, public readonly ?array $arguments = null, - public readonly ?array $_meta = null, ) { - $params = ['name' => $name]; + } + + public static function getMethod(): string + { + return 'prompts/get'; + } - if (null !== $_meta) { - $params['_meta'] = $_meta; + protected static function fromParams(?array $params): Request + { + if (!isset($params['name']) || !\is_string($params['name']) || empty($params['name'])) { + throw new InvalidArgumentException('Missing or invalid "name" parameter for prompts/get.'); } + $arguments = $params['arguments'] ?? null; if (null !== $arguments) { - $params['arguments'] = (object) $arguments; + if ($arguments instanceof \stdClass) { + $arguments = (array) $arguments; + } + if (!\is_array($arguments)) { + throw new InvalidArgumentException('Parameter "arguments" must be an array for prompts/get.'); + } } - parent::__construct($id, 'prompts/get', $params); + return new self($params['name'], $arguments); } - public static function fromRequest(Request $request): self + protected function getParams(): ?array { - if ('prompts/get' !== $request->method) { - throw new InvalidArgumentException('Request is not a prompts/get request'); - } - - $params = $request->params; - - if (!isset($params['name']) || !\is_string($params['name']) || empty($params['name'])) { - throw new InvalidArgumentException('Missing or invalid "name" parameter for prompts/get.'); - } + $params = ['name' => $this->name]; - $arguments = $params['arguments'] ?? new \stdClass(); - if (!\is_array($arguments) && !$arguments instanceof \stdClass) { - throw new InvalidArgumentException('Parameter "arguments" must be an object/array for prompts/get.'); + if (null !== $this->arguments) { + $params['arguments'] = $this->arguments; } - return new self($request->id, $params['name'], $arguments, $params['_meta'] ?? null); + return $params; } } diff --git a/src/Schema/Request/InitializeRequest.php b/src/Schema/Request/InitializeRequest.php index 0bd4a14e..702aa7b3 100644 --- a/src/Schema/Request/InitializeRequest.php +++ b/src/Schema/Request/InitializeRequest.php @@ -24,40 +24,24 @@ class InitializeRequest extends Request { /** - * @param string|int $id the ID of the request - * @param string $protocolVersion The latest version of the Model Context Protocol that the client supports. The client MAY decide to support older versions as well. - * @param ClientCapabilities $capabilities the capabilities of the client - * @param Implementation $clientInfo information about the client - * @param ?array $_meta additional metadata about the request + * @param string $protocolVersion The latest version of the Model Context Protocol that the client supports. The client MAY decide to support older versions as well. + * @param ClientCapabilities $capabilities the capabilities of the client + * @param Implementation $clientInfo information about the client */ public function __construct( - string|int $id, public readonly string $protocolVersion, public readonly ClientCapabilities $capabilities, public readonly Implementation $clientInfo, - public readonly ?array $_meta = null, ) { - $params = [ - 'protocolVersion' => $this->protocolVersion, - 'capabilities' => $this->capabilities, - 'clientInfo' => $this->clientInfo, - ]; - - if (null !== $this->_meta) { - $params['_meta'] = $this->_meta; - } - - parent::__construct($id, 'initialize', $params); } - public static function fromRequest(Request $request): self + public static function getMethod(): string { - if ('initialize' !== $request->method) { - throw new InvalidArgumentException('Request is not an initialize request'); - } - - $params = $request->params; + return 'initialize'; + } + protected static function fromParams(?array $params): Request + { if (!isset($params['protocolVersion'])) { throw new InvalidArgumentException('protocolVersion is required'); } @@ -72,6 +56,15 @@ public static function fromRequest(Request $request): self } $clientInfo = Implementation::fromArray($params['clientInfo']); - return new self($request->id, $params['protocolVersion'], $capabilities, $clientInfo, $params['_meta'] ?? null); + return new self($params['protocolVersion'], $capabilities, $clientInfo); + } + + protected function getParams(): ?array + { + return [ + 'protocolVersion' => $this->protocolVersion, + 'capabilities' => $this->capabilities, + 'clientInfo' => $this->clientInfo, + ]; } } diff --git a/src/Schema/Request/ListPromptsRequest.php b/src/Schema/Request/ListPromptsRequest.php index 48afbbda..5f388d66 100644 --- a/src/Schema/Request/ListPromptsRequest.php +++ b/src/Schema/Request/ListPromptsRequest.php @@ -11,7 +11,6 @@ namespace Mcp\Schema\Request; -use Mcp\Exception\InvalidArgumentException; use Mcp\Schema\JsonRpc\Request; /** @@ -24,31 +23,30 @@ class ListPromptsRequest extends Request /** * If provided, the server should return results starting after this cursor. * - * @param string|null $cursor an opaque token representing the current pagination position - * @param array|null $_meta optional metadata to include in the request + * @param string|null $cursor an opaque token representing the current pagination position */ public function __construct( - string|int $id, public readonly ?string $cursor = null, - public readonly ?array $_meta = null, ) { - $params = []; - if (null !== $cursor) { - $params['cursor'] = $cursor; - } - if (null !== $_meta) { - $params['_meta'] = $_meta; - } + } - parent::__construct($id, 'prompts/list', $params); + public static function getMethod(): string + { + return 'prompts/list'; } - public static function fromRequest(Request $request): self + protected static function fromParams(?array $params): Request { - if ('prompts/list' !== $request->method) { - throw new InvalidArgumentException('Request is not a prompts/list request'); + return new self($params['cursor'] ?? null); + } + + protected function getParams(): ?array + { + $params = []; + if (null !== $this->cursor) { + $params['cursor'] = $this->cursor; } - return new self($request->id, $request->params['cursor'] ?? null, $request->params['_meta'] ?? null); + return $params ?: null; } } diff --git a/src/Schema/Request/ListResourceTemplatesRequest.php b/src/Schema/Request/ListResourceTemplatesRequest.php index 327bda2c..bf6da924 100644 --- a/src/Schema/Request/ListResourceTemplatesRequest.php +++ b/src/Schema/Request/ListResourceTemplatesRequest.php @@ -11,7 +11,6 @@ namespace Mcp\Schema\Request; -use Mcp\Exception\InvalidArgumentException; use Mcp\Schema\JsonRpc\Request; /** @@ -25,30 +24,29 @@ class ListResourceTemplatesRequest extends Request * @param string|null $cursor An opaque token representing the current pagination position. * * If provided, the server should return results starting after this cursor. - * @param ?array $_meta optional metadata to include in the request */ public function __construct( - string|int $id, public readonly ?string $cursor = null, - public readonly ?array $_meta = null, ) { - $params = []; - if (null !== $cursor) { - $params['cursor'] = $cursor; - } - if (null !== $_meta) { - $params['_meta'] = $_meta; - } + } - parent::__construct($id, 'resources/templates/list', $params); + public static function getMethod(): string + { + return 'resources/templates/list'; } - public static function fromRequest(Request $request): self + protected static function fromParams(?array $params): Request { - if ('resources/templates/list' !== $request->method) { - throw new InvalidArgumentException('Request is not a list resource templates request'); + return new self($params['cursor'] ?? null); + } + + protected function getParams(): ?array + { + $params = []; + if (null !== $this->cursor) { + $params['cursor'] = $this->cursor; } - return new self($request->id, $request->params['cursor'] ?? null, $request->params['_meta'] ?? null); + return $params ?: null; } } diff --git a/src/Schema/Request/ListResourcesRequest.php b/src/Schema/Request/ListResourcesRequest.php index e698b02a..85527dff 100644 --- a/src/Schema/Request/ListResourcesRequest.php +++ b/src/Schema/Request/ListResourcesRequest.php @@ -11,7 +11,6 @@ namespace Mcp\Schema\Request; -use Mcp\Exception\InvalidArgumentException; use Mcp\Schema\JsonRpc\Request; /** @@ -25,30 +24,29 @@ class ListResourcesRequest extends Request * @param string|null $cursor An opaque token representing the current pagination position. * * If provided, the server should return results starting after this cursor. - * @param ?array $_meta optional metadata to include in the request */ public function __construct( - string|int $id, public readonly ?string $cursor = null, - public readonly ?array $_meta = null, ) { - $params = []; - if (null !== $cursor) { - $params['cursor'] = $cursor; - } - if (null !== $_meta) { - $params['_meta'] = $_meta; - } + } - parent::__construct($id, 'resources/list', $params); + public static function getMethod(): string + { + return 'resources/list'; } - public static function fromRequest(Request $request): self + protected static function fromParams(?array $params): Request { - if ('resources/list' !== $request->method) { - throw new InvalidArgumentException('Request is not a list resources request'); + return new self($params['cursor'] ?? null); + } + + protected function getParams(): ?array + { + $params = []; + if (null !== $this->cursor) { + $params['cursor'] = $this->cursor; } - return new self($request->id, $request->params['cursor'] ?? null, $request->params['_meta'] ?? null); + return $params ?: null; } } diff --git a/src/Schema/Request/ListRootsRequest.php b/src/Schema/Request/ListRootsRequest.php index 7415a985..3e8fd9fe 100644 --- a/src/Schema/Request/ListRootsRequest.php +++ b/src/Schema/Request/ListRootsRequest.php @@ -26,18 +26,22 @@ */ class ListRootsRequest extends Request { - /** - * @param ?array $_meta - */ public function __construct( - string|int $id, - public readonly ?array $_meta = null, ) { - $params = []; - if (null !== $_meta) { - $params['_meta'] = $_meta; - } + } + + public static function getMethod(): string + { + return 'roots/list'; + } + + protected static function fromParams(?array $params): Request + { + return new self(); + } - parent::__construct($id, 'roots/list', $params); + protected function getParams(): ?array + { + return null; } } diff --git a/src/Schema/Request/ListToolsRequest.php b/src/Schema/Request/ListToolsRequest.php index d5f3eaff..56ed591a 100644 --- a/src/Schema/Request/ListToolsRequest.php +++ b/src/Schema/Request/ListToolsRequest.php @@ -11,7 +11,6 @@ namespace Mcp\Schema\Request; -use Mcp\Exception\InvalidArgumentException; use Mcp\Schema\JsonRpc\Request; /** @@ -25,30 +24,29 @@ class ListToolsRequest extends Request * @param string|null $cursor An opaque token representing the current pagination position. * * If provided, the server should return results starting after this cursor. - * @param ?array $_meta optional metadata to include in the request */ public function __construct( - string|int $id, public readonly ?string $cursor = null, - public readonly ?array $_meta = null, ) { - $params = []; - if (null !== $cursor) { - $params['cursor'] = $cursor; - } - if (null !== $_meta) { - $params['_meta'] = $_meta; - } + } - parent::__construct($id, 'tools/list', $params); + public static function getMethod(): string + { + return 'tools/list'; } - public static function fromRequest(Request $request): self + protected static function fromParams(?array $params): Request { - if ('tools/list' !== $request->method) { - throw new InvalidArgumentException('Request is not a list tools request'); + return new self($params['cursor'] ?? null); + } + + protected function getParams(): ?array + { + $params = []; + if (null !== $this->cursor) { + $params['cursor'] = $this->cursor; } - return new self($request->id, $request->params['cursor'] ?? null, $request->params['_meta'] ?? null); + return $params ?: null; } } diff --git a/src/Schema/Request/PingRequest.php b/src/Schema/Request/PingRequest.php index 8db1a53b..13e13202 100644 --- a/src/Schema/Request/PingRequest.php +++ b/src/Schema/Request/PingRequest.php @@ -11,7 +11,6 @@ namespace Mcp\Schema\Request; -use Mcp\Exception\InvalidArgumentException; use Mcp\Schema\JsonRpc\Request; /** @@ -22,27 +21,18 @@ */ class PingRequest extends Request { - /** - * @param ?array $_meta - */ - public function __construct( - string|int $id, - public readonly ?array $_meta = null, - ) { - $params = []; - if (null !== $_meta) { - $params['_meta'] = $_meta; - } - - parent::__construct($id, 'ping', $params); + public static function getMethod(): string + { + return 'ping'; } - public static function fromRequest(Request $request): self + protected static function fromParams(?array $params): Request { - if ('ping' !== $request->method) { - throw new InvalidArgumentException('Request is not a ping request'); - } + return new self(); + } - return new self($request->id, $request->params['_meta'] ?? null); + protected function getParams(): ?array + { + return null; } } diff --git a/src/Schema/Request/ReadResourceRequest.php b/src/Schema/Request/ReadResourceRequest.php index 5e90cd40..4aadbe8d 100644 --- a/src/Schema/Request/ReadResourceRequest.php +++ b/src/Schema/Request/ReadResourceRequest.php @@ -22,37 +22,31 @@ class ReadResourceRequest extends Request { /** - * @param string $uri the URI of the resource to read - * @param ?array $_meta optional metadata to include in the request + * @param string $uri the URI of the resource to read */ public function __construct( - string|int $id, public readonly string $uri, - public readonly ?array $_meta = null, ) { - $params = [ - 'uri' => $uri, - ]; - - if (null !== $_meta) { - $params['_meta'] = $_meta; - } - - parent::__construct($id, 'resources/read', $params); } - public static function fromRequest(Request $request): self + public static function getMethod(): string { - if ('resources/read' !== $request->method) { - throw new InvalidArgumentException('Request is not a read resource request'); - } - - $params = $request->params; + return 'resources/read'; + } + protected static function fromParams(?array $params): Request + { if (!isset($params['uri']) || !\is_string($params['uri']) || empty($params['uri'])) { throw new InvalidArgumentException('Missing or invalid "uri" parameter for resources/read.'); } - return new self($request->id, $params['uri'], $params['_meta'] ?? null); + return new self($params['uri']); + } + + protected function getParams(): ?array + { + return [ + 'uri' => $this->uri, + ]; } } diff --git a/src/Schema/Request/ResourceSubscribeRequest.php b/src/Schema/Request/ResourceSubscribeRequest.php index 01253fa5..0b17b6c1 100644 --- a/src/Schema/Request/ResourceSubscribeRequest.php +++ b/src/Schema/Request/ResourceSubscribeRequest.php @@ -23,34 +23,29 @@ class ResourceSubscribeRequest extends Request { /** - * @param string $uri the URI of the resource to subscribe to - * @param ?array $_meta optional metadata to include in the request + * @param string $uri the URI of the resource to subscribe to */ public function __construct( - string|int $id, public readonly string $uri, - public readonly ?array $_meta = null, ) { - $params = ['uri' => $uri]; - if (null !== $_meta) { - $params['_meta'] = $_meta; - } - - parent::__construct($id, 'resources/subscribe', $params); } - public static function fromRequest(Request $request): self + public static function getMethod(): string { - if ('resources/subscribe' !== $request->method) { - throw new InvalidArgumentException('Request is not a resource subscribe request'); - } - - $params = $request->params; + return 'resources/subscribe'; + } + protected static function fromParams(?array $params): Request + { if (!isset($params['uri']) || !\is_string($params['uri']) || empty($params['uri'])) { throw new InvalidArgumentException('Missing or invalid "uri" parameter for resources/subscribe.'); } - return new self($request->id, $params['uri'], $params['_meta'] ?? null); + return new self($params['uri']); + } + + protected function getParams(): ?array + { + return ['uri' => $this->uri]; } } diff --git a/src/Schema/Request/ResourceUnsubscribeRequest.php b/src/Schema/Request/ResourceUnsubscribeRequest.php index 1257884f..54349ae8 100644 --- a/src/Schema/Request/ResourceUnsubscribeRequest.php +++ b/src/Schema/Request/ResourceUnsubscribeRequest.php @@ -23,34 +23,29 @@ class ResourceUnsubscribeRequest extends Request { /** - * @param string $uri the URI of the resource to unsubscribe from - * @param ?array $_meta optional metadata to include in the request + * @param string $uri the URI of the resource to unsubscribe from */ public function __construct( - string|int $id, public readonly string $uri, - public readonly ?array $_meta = null, ) { - $params = ['uri' => $uri]; - if (null !== $_meta) { - $params['_meta'] = $_meta; - } - - parent::__construct($id, 'resources/unsubscribe', $params); } - public static function fromRequest(Request $request): self + public static function getMethod(): string { - if ('resources/unsubscribe' !== $request->method) { - throw new InvalidArgumentException('Request is not a resource unsubscribe request'); - } - - $params = $request->params; + return 'resources/unsubscribe'; + } + protected static function fromParams(?array $params): Request + { if (!isset($params['uri']) || !\is_string($params['uri']) || empty($params['uri'])) { throw new InvalidArgumentException('Missing or invalid "uri" parameter for resources/unsubscribe.'); } - return new self($request->id, $params['uri'], $params['_meta'] ?? null); + return new self($params['uri']); + } + + protected function getParams(): ?array + { + return ['uri' => $this->uri]; } } diff --git a/src/Schema/Request/SetLogLevelRequest.php b/src/Schema/Request/SetLogLevelRequest.php index bf785a12..610c241d 100644 --- a/src/Schema/Request/SetLogLevelRequest.php +++ b/src/Schema/Request/SetLogLevelRequest.php @@ -23,39 +23,33 @@ class SetLogLevelRequest extends Request { /** - * @param LoggingLevel $level The level of logging that the client wants to receive from the server. The server - * should send all logs at this level and higher (i.e., more severe) to the client as - * notifications/message. - * @param ?array $_meta optional metadata to include in the request + * @param LoggingLevel $level The level of logging that the client wants to receive from the server. The server + * should send all logs at this level and higher (i.e., more severe) to the client as + * notifications/message. */ public function __construct( - string|int $id, public readonly LoggingLevel $level, - public readonly ?array $_meta = null, ) { - $params = [ - 'level' => $level->value, - ]; - - if (null !== $_meta) { - $params['_meta'] = $_meta; - } - - parent::__construct($id, 'logging/setLevel', $params); } - public static function fromRequest(Request $request): self + public static function getMethod(): string { - if ('logging/setLevel' !== $request->method) { - throw new InvalidArgumentException('Request is not a logging/setLevel request'); - } - - $params = $request->params; + return 'logging/setLevel'; + } + protected static function fromParams(?array $params): self + { if (!isset($params['level']) || !\is_string($params['level']) || empty($params['level'])) { - throw new InvalidArgumentException('Missing or invalid "level" parameter for logging/setLevel.'); + throw new InvalidArgumentException('Missing or invalid "level" parameter for "logging/setLevel".'); } - return new self($request->id, LoggingLevel::from($params['level']), $params['_meta'] ?? null); + return new self(LoggingLevel::from($params['level'])); + } + + protected function getParams(): ?array + { + return [ + 'level' => $this->level->value, + ]; } } diff --git a/src/Schema/Result/CallToolResult.php b/src/Schema/Result/CallToolResult.php index ee98bd67..4345f81b 100644 --- a/src/Schema/Result/CallToolResult.php +++ b/src/Schema/Result/CallToolResult.php @@ -105,11 +105,6 @@ public static function fromArray(array $data): self return new self($contents, $data['isError'] ?? false); } - public static function fromResponse(Response $response): self - { - return self::fromArray($response->result); - } - /** * @return array{ * content: array, diff --git a/src/Schema/Result/EmptyResult.php b/src/Schema/Result/EmptyResult.php index a76869ce..67080cc1 100644 --- a/src/Schema/Result/EmptyResult.php +++ b/src/Schema/Result/EmptyResult.php @@ -32,8 +32,11 @@ public static function fromArray(): self return new self(); } - public function jsonSerialize(): \stdClass + /** + * @return array{} + */ + public function jsonSerialize(): array { - return new \stdClass(); + return []; } } diff --git a/src/Schema/Result/GetPromptResult.php b/src/Schema/Result/GetPromptResult.php index d70d34ab..40277f8f 100644 --- a/src/Schema/Result/GetPromptResult.php +++ b/src/Schema/Result/GetPromptResult.php @@ -13,7 +13,6 @@ use Mcp\Exception\InvalidArgumentException; use Mcp\Schema\Content\PromptMessage; -use Mcp\Schema\JsonRpc\Response; use Mcp\Schema\JsonRpc\ResultInterface; /** @@ -60,11 +59,6 @@ public static function fromArray(array $data): self return new self($messages, $data['description'] ?? null); } - public static function fromResponse(Response $response): self - { - return self::fromArray($response->result); - } - /** * @return array{ * messages: array, diff --git a/src/Schema/Result/InitializeResult.php b/src/Schema/Result/InitializeResult.php index 6c3bedef..c6e753bc 100644 --- a/src/Schema/Result/InitializeResult.php +++ b/src/Schema/Result/InitializeResult.php @@ -12,8 +12,8 @@ namespace Mcp\Schema\Result; use Mcp\Exception\InvalidArgumentException; -use Mcp\Schema\Constants; use Mcp\Schema\Implementation; +use Mcp\Schema\JsonRpc\MessageInterface; use Mcp\Schema\JsonRpc\Response; use Mcp\Schema\JsonRpc\ResultInterface; use Mcp\Schema\ServerCapabilities; @@ -70,11 +70,6 @@ public static function fromArray(array $data): self ); } - public static function fromResponse(Response $response): self - { - return self::fromArray($response->result); - } - /** * @return array{ * protocolVersion: string, @@ -87,7 +82,7 @@ public static function fromResponse(Response $response): self public function jsonSerialize(): array { $data = [ - 'protocolVersion' => Constants::JSONRPC_VERSION, + 'protocolVersion' => MessageInterface::JSONRPC_VERSION, 'capabilities' => $this->capabilities, 'serverInfo' => $this->serverInfo, ]; diff --git a/src/Schema/Result/ListPromptsResult.php b/src/Schema/Result/ListPromptsResult.php index 99cb9e56..7c22dd3a 100644 --- a/src/Schema/Result/ListPromptsResult.php +++ b/src/Schema/Result/ListPromptsResult.php @@ -55,11 +55,6 @@ public static function fromArray(array $data): self ); } - public static function fromResponse(Response $response): self - { - return self::fromArray($response->result); - } - /** * @return array{ * prompts: array, diff --git a/src/Schema/Result/ListResourceTemplatesResult.php b/src/Schema/Result/ListResourceTemplatesResult.php index 1c9a57fc..186e1894 100644 --- a/src/Schema/Result/ListResourceTemplatesResult.php +++ b/src/Schema/Result/ListResourceTemplatesResult.php @@ -55,11 +55,6 @@ public static function fromArray(array $data): self ); } - public static function fromResponse(Response $response): self - { - return self::fromArray($response->result); - } - /** * @return array{ * resourceTemplates: array, diff --git a/src/Schema/Result/ListResourcesResult.php b/src/Schema/Result/ListResourcesResult.php index 97559da5..3b4fc6e5 100644 --- a/src/Schema/Result/ListResourcesResult.php +++ b/src/Schema/Result/ListResourcesResult.php @@ -55,11 +55,6 @@ public static function fromArray(array $data): self ); } - public static function fromResponse(Response $response): self - { - return self::fromArray($response->result); - } - /** * @return array{ * resources: array, diff --git a/src/Schema/Result/ListToolsResult.php b/src/Schema/Result/ListToolsResult.php index d4992010..6dde692e 100644 --- a/src/Schema/Result/ListToolsResult.php +++ b/src/Schema/Result/ListToolsResult.php @@ -55,11 +55,6 @@ public static function fromArray(array $data): self ); } - public static function fromResponse(Response $response): self - { - return self::fromArray($response->result); - } - /** * @return array{ * tools: array, diff --git a/src/Schema/Result/ReadResourceResult.php b/src/Schema/Result/ReadResourceResult.php index ce97cd1b..aa2d32af 100644 --- a/src/Schema/Result/ReadResourceResult.php +++ b/src/Schema/Result/ReadResourceResult.php @@ -62,11 +62,6 @@ public static function fromArray(array $data): self return new self($contents); } - public static function fromResponse(Response $response): self - { - return self::fromArray($response->result); - } - /** * @return array{ * contents: array, diff --git a/src/Server.php b/src/Server.php index 5ae10af2..26a27783 100644 --- a/src/Server.php +++ b/src/Server.php @@ -11,7 +11,7 @@ namespace Mcp; -use Mcp\Server\JsonRpcHandler; +use Mcp\JsonRpc\Handler; use Mcp\Server\TransportInterface; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; @@ -22,7 +22,7 @@ final class Server { public function __construct( - private readonly JsonRpcHandler $jsonRpcHandler, + private readonly Handler $jsonRpcHandler, private readonly LoggerInterface $logger = new NullLogger(), ) { } diff --git a/src/Server/RequestHandlerInterface.php b/src/Server/MethodHandlerInterface.php similarity index 61% rename from src/Server/RequestHandlerInterface.php rename to src/Server/MethodHandlerInterface.php index af69b022..7f949bb1 100644 --- a/src/Server/RequestHandlerInterface.php +++ b/src/Server/MethodHandlerInterface.php @@ -12,19 +12,20 @@ namespace Mcp\Server; use Mcp\Exception\ExceptionInterface; -use Mcp\Message\Error; -use Mcp\Message\Request; -use Mcp\Message\Response; +use Mcp\Schema\JsonRpc\Error; +use Mcp\Schema\JsonRpc\HasMethodInterface; +use Mcp\Schema\JsonRpc\Request; +use Mcp\Schema\JsonRpc\Response; /** * @author Christopher Hertel */ -interface RequestHandlerInterface +interface MethodHandlerInterface { - public function supports(Request $message): bool; + public function supports(HasMethodInterface $message): bool; /** * @throws ExceptionInterface When the handler encounters an error processing the request */ - public function createResponse(Request $message): Response|Error; + public function handle(HasMethodInterface $message): Response|Error|null; } diff --git a/src/Server/NotificationHandler/BaseNotificationHandler.php b/src/Server/NotificationHandler/BaseNotificationHandler.php deleted file mode 100644 index 0fdcf6b3..00000000 --- a/src/Server/NotificationHandler/BaseNotificationHandler.php +++ /dev/null @@ -1,28 +0,0 @@ - - */ -abstract class BaseNotificationHandler implements NotificationHandlerInterface -{ - public function supports(Notification $message): bool - { - return $message->method === \sprintf('notifications/%s', $this->supportedNotification()); - } - - abstract protected function supportedNotification(): string; -} diff --git a/src/Server/NotificationHandler/InitializedHandler.php b/src/Server/NotificationHandler/InitializedHandler.php index 33e4651e..f04a08a9 100644 --- a/src/Server/NotificationHandler/InitializedHandler.php +++ b/src/Server/NotificationHandler/InitializedHandler.php @@ -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 */ -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; } } diff --git a/src/Server/NotificationHandlerInterface.php b/src/Server/NotificationHandlerInterface.php deleted file mode 100644 index 5113aff6..00000000 --- a/src/Server/NotificationHandlerInterface.php +++ /dev/null @@ -1,28 +0,0 @@ - - */ -interface NotificationHandlerInterface -{ - public function supports(Notification $message): bool; - - /** - * @throws ExceptionInterface When the handler encounters an error processing the notification - */ - public function handle(Notification $notification): void; -} diff --git a/src/Server/RequestHandler/BaseRequestHandler.php b/src/Server/RequestHandler/BaseRequestHandler.php deleted file mode 100644 index 5f44648d..00000000 --- a/src/Server/RequestHandler/BaseRequestHandler.php +++ /dev/null @@ -1,28 +0,0 @@ - - */ -abstract class BaseRequestHandler implements RequestHandlerInterface -{ - public function supports(Request $message): bool - { - return $message->method === $this->supportedMethod(); - } - - abstract protected function supportedMethod(): string; -} diff --git a/src/Server/RequestHandler/CallToolHandler.php b/src/Server/RequestHandler/CallToolHandler.php new file mode 100644 index 00000000..64851d7d --- /dev/null +++ b/src/Server/RequestHandler/CallToolHandler.php @@ -0,0 +1,50 @@ + + * @author Tobias Nyholm + */ +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); + } +} diff --git a/src/Server/RequestHandler/GetPromptHandler.php b/src/Server/RequestHandler/GetPromptHandler.php new file mode 100644 index 00000000..8ac82842 --- /dev/null +++ b/src/Server/RequestHandler/GetPromptHandler.php @@ -0,0 +1,49 @@ + + */ +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); + } +} diff --git a/src/Server/RequestHandler/InitializeHandler.php b/src/Server/RequestHandler/InitializeHandler.php index 85c44882..11d9b0ab 100644 --- a/src/Server/RequestHandler/InitializeHandler.php +++ b/src/Server/RequestHandler/InitializeHandler.php @@ -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 */ -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), + ); } } diff --git a/src/Server/RequestHandler/ListPromptsHandler.php b/src/Server/RequestHandler/ListPromptsHandler.php new file mode 100644 index 00000000..d58a92f9 --- /dev/null +++ b/src/Server/RequestHandler/ListPromptsHandler.php @@ -0,0 +1,64 @@ + + */ +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), + ); + } +} diff --git a/src/Server/RequestHandler/ListResourcesHandler.php b/src/Server/RequestHandler/ListResourcesHandler.php new file mode 100644 index 00000000..2be0a86d --- /dev/null +++ b/src/Server/RequestHandler/ListResourcesHandler.php @@ -0,0 +1,66 @@ + + */ +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), + ); + } +} diff --git a/src/Server/RequestHandler/ListToolsHandler.php b/src/Server/RequestHandler/ListToolsHandler.php new file mode 100644 index 00000000..bbbd8e7e --- /dev/null +++ b/src/Server/RequestHandler/ListToolsHandler.php @@ -0,0 +1,69 @@ + + * @author Tobias Nyholm + */ +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), + ); + } +} diff --git a/src/Server/RequestHandler/PingHandler.php b/src/Server/RequestHandler/PingHandler.php index ee8ec4d4..2cf8ec91 100644 --- a/src/Server/RequestHandler/PingHandler.php +++ b/src/Server/RequestHandler/PingHandler.php @@ -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 */ -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()); } } diff --git a/src/Server/RequestHandler/PromptGetHandler.php b/src/Server/RequestHandler/PromptGetHandler.php deleted file mode 100644 index a7d0cdcd..00000000 --- a/src/Server/RequestHandler/PromptGetHandler.php +++ /dev/null @@ -1,83 +0,0 @@ - - */ -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'; - } -} diff --git a/src/Server/RequestHandler/PromptListHandler.php b/src/Server/RequestHandler/PromptListHandler.php deleted file mode 100644 index b666fb49..00000000 --- a/src/Server/RequestHandler/PromptListHandler.php +++ /dev/null @@ -1,85 +0,0 @@ - - */ -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'; - } -} diff --git a/src/Server/RequestHandler/ReadResourceHandler.php b/src/Server/RequestHandler/ReadResourceHandler.php new file mode 100644 index 00000000..42b73681 --- /dev/null +++ b/src/Server/RequestHandler/ReadResourceHandler.php @@ -0,0 +1,52 @@ + + */ +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); + } +} diff --git a/src/Server/RequestHandler/ResourceListHandler.php b/src/Server/RequestHandler/ResourceListHandler.php deleted file mode 100644 index f91dc7dc..00000000 --- a/src/Server/RequestHandler/ResourceListHandler.php +++ /dev/null @@ -1,79 +0,0 @@ - - */ -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'; - } -} diff --git a/src/Server/RequestHandler/ResourceReadHandler.php b/src/Server/RequestHandler/ResourceReadHandler.php deleted file mode 100644 index 46750b4a..00000000 --- a/src/Server/RequestHandler/ResourceReadHandler.php +++ /dev/null @@ -1,59 +0,0 @@ - - */ -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'; - } -} diff --git a/src/Server/RequestHandler/ToolCallHandler.php b/src/Server/RequestHandler/ToolCallHandler.php deleted file mode 100644 index 984f3e83..00000000 --- a/src/Server/RequestHandler/ToolCallHandler.php +++ /dev/null @@ -1,76 +0,0 @@ - - * @author Tobias Nyholm - */ -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'; - } -} diff --git a/src/Server/RequestHandler/ToolListHandler.php b/src/Server/RequestHandler/ToolListHandler.php deleted file mode 100644 index 5e09a704..00000000 --- a/src/Server/RequestHandler/ToolListHandler.php +++ /dev/null @@ -1,68 +0,0 @@ - - * @author Tobias Nyholm - */ -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'; - } -} diff --git a/tests/Fixtures/InMemoryTransport.php b/src/Server/Transport/InMemoryTransport.php similarity index 90% rename from tests/Fixtures/InMemoryTransport.php rename to src/Server/Transport/InMemoryTransport.php index 046abd56..015c70c5 100644 --- a/tests/Fixtures/InMemoryTransport.php +++ b/src/Server/Transport/InMemoryTransport.php @@ -9,10 +9,13 @@ * file that was distributed with this source code. */ -namespace Mcp\Tests\Fixtures; +namespace Mcp\Server\Transport; use Mcp\Server\TransportInterface; +/** + * @author Tobias Nyholm + */ class InMemoryTransport implements TransportInterface { private bool $connected = true; diff --git a/tests/Server/JsonRpcHandlerTest.php b/tests/JsonRpc/HandlerTest.php similarity index 50% rename from tests/Server/JsonRpcHandlerTest.php rename to tests/JsonRpc/HandlerTest.php index ef1d5dcc..a2fdeec9 100644 --- a/tests/Server/JsonRpcHandlerTest.php +++ b/tests/JsonRpc/HandlerTest.php @@ -9,81 +9,75 @@ * file that was distributed with this source code. */ -namespace Mcp\Tests\Server; +namespace Mcp\Tests\JsonRpc; -use Mcp\Message\Factory; -use Mcp\Message\Response; -use Mcp\Server\JsonRpcHandler; -use Mcp\Server\NotificationHandlerInterface; -use Mcp\Server\RequestHandlerInterface; -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\Small; +use Mcp\JsonRpc\Handler; +use Mcp\JsonRpc\MessageFactory; +use Mcp\Schema\JsonRpc\Response; +use Mcp\Server\MethodHandlerInterface; use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; -use Psr\Log\NullLogger; -#[Small] -#[CoversClass(JsonRpcHandler::class)] -class JsonRpcHandlerTest extends TestCase +class HandlerTest extends TestCase { #[TestDox('Make sure a single notification can be handled by multiple handlers.')] - public function testHandleMultipleNotifications(): void + public function testHandleMultipleNotifications() { - $handlerA = $this->getMockBuilder(NotificationHandlerInterface::class) + $handlerA = $this->getMockBuilder(MethodHandlerInterface::class) ->disableOriginalConstructor() ->onlyMethods(['supports', 'handle']) ->getMock(); $handlerA->method('supports')->willReturn(true); $handlerA->expects($this->once())->method('handle'); - $handlerB = $this->getMockBuilder(NotificationHandlerInterface::class) + $handlerB = $this->getMockBuilder(MethodHandlerInterface::class) ->disableOriginalConstructor() ->onlyMethods(['supports', 'handle']) ->getMock(); $handlerB->method('supports')->willReturn(false); $handlerB->expects($this->never())->method('handle'); - $handlerC = $this->getMockBuilder(NotificationHandlerInterface::class) + $handlerC = $this->getMockBuilder(MethodHandlerInterface::class) ->disableOriginalConstructor() ->onlyMethods(['supports', 'handle']) ->getMock(); $handlerC->method('supports')->willReturn(true); $handlerC->expects($this->once())->method('handle'); - $jsonRpc = new JsonRpcHandler(new Factory(), [], [$handlerA, $handlerB, $handlerC], new NullLogger()); + $jsonRpc = new Handler(MessageFactory::make(), [$handlerA, $handlerB, $handlerC]); $result = $jsonRpc->process( - '{"jsonrpc": "2.0", "id": 1, "method": "notifications/foobar"}' + '{"jsonrpc": "2.0", "method": "notifications/initialized"}' ); iterator_to_array($result); } #[TestDox('Make sure a single request can NOT be handled by multiple handlers.')] - public function testHandleMultipleRequests(): void + public function testHandleMultipleRequests() { - $handlerA = $this->getMockBuilder(RequestHandlerInterface::class) + $handlerA = $this->getMockBuilder(MethodHandlerInterface::class) ->disableOriginalConstructor() - ->onlyMethods(['supports', 'createResponse']) + ->onlyMethods(['supports', 'handle']) ->getMock(); $handlerA->method('supports')->willReturn(true); - $handlerA->expects($this->once())->method('createResponse')->willReturn(new Response(1)); + $handlerA->expects($this->once())->method('handle')->willReturn(new Response(1, ['result' => 'success'])); - $handlerB = $this->getMockBuilder(RequestHandlerInterface::class) + $handlerB = $this->getMockBuilder(MethodHandlerInterface::class) ->disableOriginalConstructor() - ->onlyMethods(['supports', 'createResponse']) + ->onlyMethods(['supports', 'handle']) ->getMock(); $handlerB->method('supports')->willReturn(false); - $handlerB->expects($this->never())->method('createResponse'); + $handlerB->expects($this->never())->method('handle'); - $handlerC = $this->getMockBuilder(RequestHandlerInterface::class) + $handlerC = $this->getMockBuilder(MethodHandlerInterface::class) ->disableOriginalConstructor() - ->onlyMethods(['supports', 'createResponse']) + ->onlyMethods(['supports', 'handle']) ->getMock(); $handlerC->method('supports')->willReturn(true); - $handlerC->expects($this->never())->method('createResponse'); + $handlerC->expects($this->never())->method('handle'); - $jsonRpc = new JsonRpcHandler(new Factory(), [$handlerA, $handlerB, $handlerC], [], new NullLogger()); + $jsonRpc = new Handler(MessageFactory::make(), [$handlerA, $handlerB, $handlerC]); $result = $jsonRpc->process( - '{"jsonrpc": "2.0", "id": 1, "method": "request/foobar"}' + '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}' ); iterator_to_array($result); } diff --git a/tests/JsonRpc/MessageFactoryTest.php b/tests/JsonRpc/MessageFactoryTest.php new file mode 100644 index 00000000..9f43aad6 --- /dev/null +++ b/tests/JsonRpc/MessageFactoryTest.php @@ -0,0 +1,95 @@ +factory = new MessageFactory([ + CancelledNotification::class, + InitializedNotification::class, + GetPromptRequest::class, + ]); + } + + public function testCreateRequest() + { + $json = '{"jsonrpc": "2.0", "method": "prompts/get", "params": {"name": "create_story"}, "id": 123}'; + + $result = $this->first($this->factory->create($json)); + + $this->assertInstanceOf(GetPromptRequest::class, $result); + $this->assertSame('prompts/get', $result::getMethod()); + $this->assertSame('create_story', $result->name); + $this->assertSame(123, $result->getId()); + } + + public function testCreateNotification() + { + $json = '{"jsonrpc": "2.0", "method": "notifications/cancelled", "params": {"requestId": 12345}}'; + + $result = $this->first($this->factory->create($json)); + + $this->assertInstanceOf(CancelledNotification::class, $result); + $this->assertSame('notifications/cancelled', $result::getMethod()); + $this->assertSame(12345, $result->requestId); + } + + public function testInvalidJson() + { + $this->expectException(\JsonException::class); + + $this->first($this->factory->create('invalid json')); + } + + public function testMissingMethod() + { + $result = $this->first($this->factory->create('{"jsonrpc": "2.0", "params": {}, "id": 1}')); + $this->assertInstanceOf(InvalidInputMessageException::class, $result); + $this->assertEquals('Invalid JSON-RPC request, missing valid "method".', $result->getMessage()); + } + + public function testBatchMissingMethod() + { + $results = $this->factory->create('[{"jsonrpc": "2.0", "params": {}, "id": 1}, {"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}}]'); + + $results = iterator_to_array($results); + $result = array_shift($results); + $this->assertInstanceOf(InvalidInputMessageException::class, $result); + $this->assertEquals('Invalid JSON-RPC request, missing valid "method".', $result->getMessage()); + + $result = array_shift($results); + $this->assertInstanceOf(InitializedNotification::class, $result); + } + + /** + * @param iterable $items + */ + private function first(iterable $items): mixed + { + foreach ($items as $item) { + return $item; + } + + return null; + } +} diff --git a/tests/Message/ErrorTest.php b/tests/Message/ErrorTest.php deleted file mode 100644 index 99d0763a..00000000 --- a/tests/Message/ErrorTest.php +++ /dev/null @@ -1,52 +0,0 @@ - '2.0', - 'id' => 1, - 'error' => [ - 'code' => -32602, - 'message' => 'Another error occurred', - ], - ]; - - $this->assertSame($expected, $error->jsonSerialize()); - } - - public function testWithStringId(): void - { - $error = new Error('abc', -32602, 'Another error occurred'); - $expected = [ - 'jsonrpc' => '2.0', - 'id' => 'abc', - 'error' => [ - 'code' => -32602, - 'message' => 'Another error occurred', - ], - ]; - - $this->assertSame($expected, $error->jsonSerialize()); - } -} diff --git a/tests/Message/FactoryTest.php b/tests/Message/FactoryTest.php deleted file mode 100644 index e1e13ddb..00000000 --- a/tests/Message/FactoryTest.php +++ /dev/null @@ -1,94 +0,0 @@ -factory = new Factory(); - } - - /** - * @param iterable $items - */ - private function first(iterable $items): mixed - { - foreach ($items as $item) { - return $item; - } - - return null; - } - - public function testCreateRequest(): void - { - $json = '{"jsonrpc": "2.0", "method": "test_method", "params": {"foo": "bar"}, "id": 123}'; - - $result = $this->first($this->factory->create($json)); - - $this->assertInstanceOf(Request::class, $result); - $this->assertSame('test_method', $result->method); - $this->assertSame(['foo' => 'bar'], $result->params); - $this->assertSame(123, $result->id); - } - - public function testCreateNotification(): void - { - $json = '{"jsonrpc": "2.0", "method": "notifications/test_event", "params": {"foo": "bar"}}'; - - $result = $this->first($this->factory->create($json)); - - $this->assertInstanceOf(Notification::class, $result); - $this->assertSame('notifications/test_event', $result->method); - $this->assertSame(['foo' => 'bar'], $result->params); - } - - public function testInvalidJson(): void - { - $this->expectException(\JsonException::class); - - $this->first($this->factory->create('invalid json')); - } - - public function testMissingMethod(): void - { - $result = $this->first($this->factory->create('{"jsonrpc": "2.0", "params": {}, "id": 1}')); - $this->assertInstanceOf(InvalidInputMessageException::class, $result); - $this->assertEquals('Invalid JSON-RPC request, missing "method".', $result->getMessage()); - } - - public function testBatchMissingMethod(): void - { - $results = $this->factory->create('[{"jsonrpc": "2.0", "params": {}, "id": 1}, {"jsonrpc": "2.0", "method": "notifications/test_event", "params": {}, "id": 2}]'); - - $results = iterator_to_array($results); - $result = array_shift($results); - $this->assertInstanceOf(InvalidInputMessageException::class, $result); - $this->assertEquals('Invalid JSON-RPC request, missing "method".', $result->getMessage()); - - $result = array_shift($results); - $this->assertInstanceOf(Notification::class, $result); - } -} diff --git a/tests/Message/ResponseTest.php b/tests/Message/ResponseTest.php deleted file mode 100644 index e087d2d3..00000000 --- a/tests/Message/ResponseTest.php +++ /dev/null @@ -1,46 +0,0 @@ - 'bar']); - $expected = [ - 'jsonrpc' => '2.0', - 'id' => 1, - 'result' => ['foo' => 'bar'], - ]; - - $this->assertSame($expected, $response->jsonSerialize()); - } - - public function testWithStringId(): void - { - $response = new Response('abc', ['foo' => 'bar']); - $expected = [ - 'jsonrpc' => '2.0', - 'id' => 'abc', - 'result' => ['foo' => 'bar'], - ]; - - $this->assertSame($expected, $response->jsonSerialize()); - } -} diff --git a/tests/Schema/JsonRpc/NotificationTest.php b/tests/Schema/JsonRpc/NotificationTest.php new file mode 100644 index 00000000..c2ab0fe5 --- /dev/null +++ b/tests/Schema/JsonRpc/NotificationTest.php @@ -0,0 +1,56 @@ + '2.0', + 'method' => 'notifications/dummy', + 'params' => [ + '_meta' => ['key' => 'value'], + ], + ]); + + $expectedMeta = [ + 'jsonrpc' => '2.0', + 'method' => 'notifications/dummy', + 'params' => [ + '_meta' => ['key' => 'value'], + ], + ]; + + $this->assertSame($expectedMeta, $notification->jsonSerialize()); + } +} diff --git a/tests/Schema/JsonRpc/RequestTest.php b/tests/Schema/JsonRpc/RequestTest.php new file mode 100644 index 00000000..a22b9272 --- /dev/null +++ b/tests/Schema/JsonRpc/RequestTest.php @@ -0,0 +1,58 @@ + '2.0', + 'id' => '12345', + 'method' => 'foo/bar', + 'params' => [ + '_meta' => ['key' => 'value'], + ], + ]); + + $expectedMeta = [ + 'jsonrpc' => '2.0', + 'id' => '12345', + 'method' => 'foo/bar', + 'params' => [ + '_meta' => ['key' => 'value'], + ], + ]; + + $this->assertSame($expectedMeta, $notification->jsonSerialize()); + } +} diff --git a/tests/Server/RequestHandler/PromptListHandlerTest.php b/tests/Server/RequestHandler/PromptListHandlerTest.php index ce522c69..0825876c 100644 --- a/tests/Server/RequestHandler/PromptListHandlerTest.php +++ b/tests/Server/RequestHandler/PromptListHandlerTest.php @@ -13,43 +13,50 @@ use Mcp\Capability\Prompt\MetadataInterface; use Mcp\Capability\PromptChain; -use Mcp\Message\Request; -use Mcp\Server\RequestHandler\PromptListHandler; -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\Small; +use Mcp\Schema\Request\ListPromptsRequest; +use Mcp\Schema\Result\ListPromptsResult; +use Mcp\Server\RequestHandler\ListPromptsHandler; +use Nyholm\NSA; use PHPUnit\Framework\TestCase; -#[Small] -#[CoversClass(PromptListHandler::class)] class PromptListHandlerTest extends TestCase { - public function testHandleEmpty(): void + public function testHandleEmpty() { - $handler = new PromptListHandler(new PromptChain([])); - $message = new Request(1, 'prompts/list', []); - $response = $handler->createResponse($message); - $this->assertEquals(1, $response->id); - $this->assertEquals(['prompts' => []], $response->result); + $handler = new ListPromptsHandler(new PromptChain([])); + $request = new ListPromptsRequest(); + NSA::setProperty($request, 'id', 1); + $response = $handler->handle($request); + + $this->assertInstanceOf(ListPromptsResult::class, $response->result); + $this->assertEquals(1, $response->getId()); + $this->assertEquals([], $response->result->prompts); } - public function testHandleReturnAll(): void + public function testHandleReturnAll() { $item = self::createMetadataItem(); - $handler = new PromptListHandler(new PromptChain([$item])); - $message = new Request(1, 'prompts/list', []); - $response = $handler->createResponse($message); - $this->assertCount(1, $response->result['prompts']); - $this->assertArrayNotHasKey('nextCursor', $response->result); + $handler = new ListPromptsHandler(new PromptChain([$item])); + $request = new ListPromptsRequest(); + NSA::setProperty($request, 'id', 1); + $response = $handler->handle($request); + + $this->assertInstanceOf(ListPromptsResult::class, $response->result); + $this->assertCount(1, $response->result->prompts); + $this->assertNull($response->result->nextCursor); } - public function testHandlePagination(): void + public function testHandlePagination() { $item = self::createMetadataItem(); - $handler = new PromptListHandler(new PromptChain([$item, $item]), 2); - $message = new Request(1, 'prompts/list', []); - $response = $handler->createResponse($message); - $this->assertCount(2, $response->result['prompts']); - $this->assertArrayHasKey('nextCursor', $response->result); + $handler = new ListPromptsHandler(new PromptChain([$item, $item]), 2); + $request = new ListPromptsRequest(); + NSA::setProperty($request, 'id', 1); + $response = $handler->handle($request); + + $this->assertInstanceOf(ListPromptsResult::class, $response->result); + $this->assertCount(2, $response->result->prompts); + $this->assertNotNull($response->result->nextCursor); } private static function createMetadataItem(): MetadataInterface diff --git a/tests/Server/RequestHandler/ResourceListHandlerTest.php b/tests/Server/RequestHandler/ResourceListHandlerTest.php index a2cca7d8..3ac2d451 100644 --- a/tests/Server/RequestHandler/ResourceListHandlerTest.php +++ b/tests/Server/RequestHandler/ResourceListHandlerTest.php @@ -13,18 +13,16 @@ use Mcp\Capability\Resource\CollectionInterface; use Mcp\Capability\Resource\MetadataInterface; -use Mcp\Message\Request; -use Mcp\Server\RequestHandler\ResourceListHandler; -use PHPUnit\Framework\Attributes\CoversClass; +use Mcp\Schema\Request\ListResourcesRequest; +use Mcp\Schema\Result\ListResourcesResult; +use Mcp\Server\RequestHandler\ListResourcesHandler; +use Nyholm\NSA; use PHPUnit\Framework\Attributes\DataProvider; -use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\TestCase; -#[Small] -#[CoversClass(ResourceListHandler::class)] class ResourceListHandlerTest extends TestCase { - public function testHandleEmpty(): void + public function testHandleEmpty() { $collection = $this->getMockBuilder(CollectionInterface::class) ->disableOriginalConstructor() @@ -32,29 +30,36 @@ public function testHandleEmpty(): void ->getMock(); $collection->expects($this->once())->method('getMetadata')->willReturn([]); - $handler = new ResourceListHandler($collection); - $message = new Request(1, 'resources/list', []); - $response = $handler->createResponse($message); + $handler = new ListResourcesHandler($collection); + $request = new ListResourcesRequest(); + NSA::setProperty($request, 'id', 1); + $response = $handler->handle($request); + + $this->assertInstanceOf(ListResourcesResult::class, $response->result); $this->assertEquals(1, $response->id); - $this->assertEquals(['resources' => []], $response->result); + $this->assertEquals([], $response->result->resources); } /** * @param iterable $metadataList */ #[DataProvider('metadataProvider')] - public function testHandleReturnAll(iterable $metadataList): void + public function testHandleReturnAll(iterable $metadataList) { $collection = $this->getMockBuilder(CollectionInterface::class) ->disableOriginalConstructor() ->onlyMethods(['getMetadata']) ->getMock(); $collection->expects($this->once())->method('getMetadata')->willReturn($metadataList); - $handler = new ResourceListHandler($collection); - $message = new Request(1, 'resources/list', []); - $response = $handler->createResponse($message); - $this->assertCount(1, $response->result['resources']); - $this->assertArrayNotHasKey('nextCursor', $response->result); + + $handler = new ListResourcesHandler($collection); + $request = new ListResourcesRequest(); + NSA::setProperty($request, 'id', 1); + $response = $handler->handle($request); + + $this->assertInstanceOf(ListResourcesResult::class, $response->result); + $this->assertCount(1, $response->result->resources); + $this->assertNull($response->result->nextCursor); } /** @@ -70,7 +75,7 @@ public static function metadataProvider(): array ]; } - public function testHandlePagination(): void + public function testHandlePagination() { $item = self::createMetadataItem(); $collection = $this->getMockBuilder(CollectionInterface::class) @@ -78,11 +83,15 @@ public function testHandlePagination(): void ->onlyMethods(['getMetadata']) ->getMock(); $collection->expects($this->once())->method('getMetadata')->willReturn([$item, $item]); - $handler = new ResourceListHandler($collection, 2); - $message = new Request(1, 'resources/list', []); - $response = $handler->createResponse($message); - $this->assertCount(2, $response->result['resources']); - $this->assertArrayHasKey('nextCursor', $response->result); + + $handler = new ListResourcesHandler($collection, 2); + $request = new ListResourcesRequest(); + NSA::setProperty($request, 'id', 1); + $response = $handler->handle($request); + + $this->assertInstanceOf(ListResourcesResult::class, $response->result); + $this->assertCount(2, $response->result->resources); + $this->assertNotNull($response->result->nextCursor); } private static function createMetadataItem(): MetadataInterface @@ -95,7 +104,7 @@ public function getUri(): string public function getName(): string { - return 'src/SomeFile.php'; + return 'SomeFile'; } public function getDescription(): string diff --git a/tests/Server/RequestHandler/ToolListHandlerTest.php b/tests/Server/RequestHandler/ToolListHandlerTest.php index 00a49064..72347107 100644 --- a/tests/Server/RequestHandler/ToolListHandlerTest.php +++ b/tests/Server/RequestHandler/ToolListHandlerTest.php @@ -13,19 +13,16 @@ use Mcp\Capability\Tool\CollectionInterface; use Mcp\Capability\Tool\MetadataInterface; -use Mcp\Message\Request; -use Mcp\Server\RequestHandler\ToolListHandler; -use PHPUnit\Framework\Attributes\CoversClass; +use Mcp\Schema\Request\ListToolsRequest; +use Mcp\Schema\Result\ListToolsResult; +use Mcp\Server\RequestHandler\ListToolsHandler; +use Nyholm\NSA; use PHPUnit\Framework\Attributes\DataProvider; -use PHPUnit\Framework\Attributes\Small; -use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; -#[Small] -#[CoversClass(ToolListHandler::class)] class ToolListHandlerTest extends TestCase { - public function testHandleEmpty(): void + public function testHandleEmpty() { $collection = $this->getMockBuilder(CollectionInterface::class) ->disableOriginalConstructor() @@ -33,29 +30,35 @@ public function testHandleEmpty(): void ->getMock(); $collection->expects($this->once())->method('getMetadata')->willReturn([]); - $handler = new ToolListHandler($collection); - $message = new Request(1, 'tools/list', []); - $response = $handler->createResponse($message); - $this->assertEquals(1, $response->id); - $this->assertEquals(['tools' => []], $response->result); + $handler = new ListToolsHandler($collection); + $request = new ListToolsRequest(); + NSA::setProperty($request, 'id', 1); + $response = $handler->handle($request); + + $this->assertInstanceOf(ListToolsResult::class, $response->result); + $this->assertSame(1, $response->id); + $this->assertSame([], $response->result->tools); } /** * @param iterable $metadataList */ #[DataProvider('metadataProvider')] - public function testHandleReturnAll(iterable $metadataList): void + public function testHandleReturnAll(iterable $metadataList) { $collection = $this->getMockBuilder(CollectionInterface::class) ->disableOriginalConstructor() ->onlyMethods(['getMetadata']) ->getMock(); $collection->expects($this->once())->method('getMetadata')->willReturn($metadataList); - $handler = new ToolListHandler($collection); - $message = new Request(1, 'tools/list', []); - $response = $handler->createResponse($message); - $this->assertCount(1, $response->result['tools']); - $this->assertArrayNotHasKey('nextCursor', $response->result); + $handler = new ListToolsHandler($collection); + $request = new ListToolsRequest(); + NSA::setProperty($request, 'id', 1); + $response = $handler->handle($request); + + $this->assertInstanceOf(ListToolsResult::class, $response->result); + $this->assertCount(1, $response->result->tools); + $this->assertNull($response->result->nextCursor); } /** @@ -71,8 +74,7 @@ public static function metadataProvider(): array ]; } - #[Test] - public function handlePagination(): void + public function testHandlePagination() { $item = self::createMetadataItem(); $collection = $this->getMockBuilder(CollectionInterface::class) @@ -80,11 +82,14 @@ public function handlePagination(): void ->onlyMethods(['getMetadata']) ->getMock(); $collection->expects($this->once())->method('getMetadata')->willReturn([$item, $item]); - $handler = new ToolListHandler($collection, 2); - $message = new Request(1, 'tools/list', []); - $response = $handler->createResponse($message); - $this->assertCount(2, $response->result['tools']); - $this->assertArrayHasKey('nextCursor', $response->result); + $handler = new ListToolsHandler($collection, 2); + $request = new ListToolsRequest(); + NSA::setProperty($request, 'id', 1); + $response = $handler->handle($request); + + $this->assertInstanceOf(ListToolsResult::class, $response->result); + $this->assertCount(2, $response->result->tools); + $this->assertNotNull($response->result->nextCursor); } private static function createMetadataItem(): MetadataInterface diff --git a/tests/ServerTest.php b/tests/ServerTest.php index e656cc77..2c2129e0 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -11,20 +11,16 @@ namespace Mcp\Tests; +use Mcp\JsonRpc\Handler; use Mcp\Server; -use Mcp\Server\JsonRpcHandler; -use Mcp\Tests\Fixtures\InMemoryTransport; -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\Small; +use Mcp\Server\Transport\InMemoryTransport; use PHPUnit\Framework\MockObject\Stub\Exception; use PHPUnit\Framework\TestCase; use Psr\Log\NullLogger; -#[Small] -#[CoversClass(Server::class)] class ServerTest extends TestCase { - public function testJsonExceptions(): void + public function testJsonExceptions() { $logger = $this->getMockBuilder(NullLogger::class) ->disableOriginalConstructor() @@ -32,7 +28,7 @@ public function testJsonExceptions(): void ->getMock(); $logger->expects($this->once())->method('error'); - $handler = $this->getMockBuilder(JsonRpcHandler::class) + $handler = $this->getMockBuilder(Handler::class) ->disableOriginalConstructor() ->onlyMethods(['process']) ->getMock();