From 3f85c0585673faf8f18c622be4d513a6c14e987b Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Sat, 22 Aug 2026 04:03:29 +0200 Subject: [PATCH 1/2] Rebase concrete exceptions onto Mcp\Exception base classes --- src/Exception/ClientRegistrationException.php | 2 +- src/Exception/ElicitationException.php | 2 +- src/Exception/HandlerNotFoundException.php | 2 +- src/Exception/InvalidCursorException.php | 2 +- .../InvalidInputMessageException.php | 2 +- ...ssingRequiredClientCapabilityException.php | 2 +- src/Exception/PromptGetException.php | 2 +- src/Exception/PromptNotFoundException.php | 2 +- src/Exception/RegistryException.php | 2 +- src/Exception/ResourceNotFoundException.php | 2 +- src/Exception/ResourceReadException.php | 2 +- src/Exception/RootsException.php | 2 +- src/Exception/RuntimeException.php | 2 +- src/Exception/SamplingException.php | 2 +- src/Exception/ToolCallException.php | 2 +- src/Exception/ToolNotFoundException.php | 2 +- .../Unit/Exception/ExceptionHierarchyTest.php | 71 +++++++++++++++++++ 17 files changed, 87 insertions(+), 16 deletions(-) create mode 100644 tests/Unit/Exception/ExceptionHierarchyTest.php diff --git a/src/Exception/ClientRegistrationException.php b/src/Exception/ClientRegistrationException.php index 38711468..6c3f47f0 100644 --- a/src/Exception/ClientRegistrationException.php +++ b/src/Exception/ClientRegistrationException.php @@ -11,7 +11,7 @@ namespace Mcp\Exception; -final class ClientRegistrationException extends \RuntimeException implements ExceptionInterface +final class ClientRegistrationException extends RuntimeException { public function __construct( string $message, diff --git a/src/Exception/ElicitationException.php b/src/Exception/ElicitationException.php index 0ef784e1..8097881f 100644 --- a/src/Exception/ElicitationException.php +++ b/src/Exception/ElicitationException.php @@ -19,6 +19,6 @@ * * @author Johannes Wachter */ -final class ElicitationException extends \RuntimeException implements ExceptionInterface +final class ElicitationException extends RuntimeException { } diff --git a/src/Exception/HandlerNotFoundException.php b/src/Exception/HandlerNotFoundException.php index ef222eda..a0578dcb 100644 --- a/src/Exception/HandlerNotFoundException.php +++ b/src/Exception/HandlerNotFoundException.php @@ -14,6 +14,6 @@ /** * @author Tobias Nyholm */ -class HandlerNotFoundException extends \InvalidArgumentException implements NotFoundExceptionInterface +class HandlerNotFoundException extends InvalidArgumentException implements NotFoundExceptionInterface { } diff --git a/src/Exception/InvalidCursorException.php b/src/Exception/InvalidCursorException.php index 5b08743a..117d4aac 100644 --- a/src/Exception/InvalidCursorException.php +++ b/src/Exception/InvalidCursorException.php @@ -14,7 +14,7 @@ /** * @author Tobias Nyholm */ -final class InvalidCursorException extends \InvalidArgumentException implements ExceptionInterface +final class InvalidCursorException extends InvalidArgumentException { public function __construct( public readonly string $cursor, diff --git a/src/Exception/InvalidInputMessageException.php b/src/Exception/InvalidInputMessageException.php index 0f961224..5de3db6f 100644 --- a/src/Exception/InvalidInputMessageException.php +++ b/src/Exception/InvalidInputMessageException.php @@ -14,7 +14,7 @@ /** * @author Tobias Nyholm */ -class InvalidInputMessageException extends \InvalidArgumentException implements ExceptionInterface +class InvalidInputMessageException extends InvalidArgumentException { private string|int|null $requestId = null; diff --git a/src/Exception/MissingRequiredClientCapabilityException.php b/src/Exception/MissingRequiredClientCapabilityException.php index aa8b0315..edb9dd74 100644 --- a/src/Exception/MissingRequiredClientCapabilityException.php +++ b/src/Exception/MissingRequiredClientCapabilityException.php @@ -22,7 +22,7 @@ * * @author Christopher Hertel */ -class MissingRequiredClientCapabilityException extends \RuntimeException implements ExceptionInterface +class MissingRequiredClientCapabilityException extends RuntimeException { public function __construct( public readonly ClientCapabilities $requiredCapabilities, diff --git a/src/Exception/PromptGetException.php b/src/Exception/PromptGetException.php index 7eec0daf..33fc3719 100644 --- a/src/Exception/PromptGetException.php +++ b/src/Exception/PromptGetException.php @@ -14,6 +14,6 @@ /** * @author Tobias Nyholm */ -final class PromptGetException extends \RuntimeException implements ExceptionInterface +final class PromptGetException extends RuntimeException { } diff --git a/src/Exception/PromptNotFoundException.php b/src/Exception/PromptNotFoundException.php index 81b7c6e5..6febf042 100644 --- a/src/Exception/PromptNotFoundException.php +++ b/src/Exception/PromptNotFoundException.php @@ -14,7 +14,7 @@ /** * @author Tobias Nyholm */ -final class PromptNotFoundException extends \RuntimeException implements NotFoundExceptionInterface +final class PromptNotFoundException extends RuntimeException implements NotFoundExceptionInterface { public function __construct( public readonly string $name, diff --git a/src/Exception/RegistryException.php b/src/Exception/RegistryException.php index c483a01e..54ca8a66 100644 --- a/src/Exception/RegistryException.php +++ b/src/Exception/RegistryException.php @@ -13,7 +13,7 @@ use Mcp\Schema\JsonRpc\Error; -final class RegistryException extends \Exception implements ExceptionInterface +final class RegistryException extends Exception { public static function invalidParams(string $message = 'Invalid params', ?\Throwable $previous = null): self { diff --git a/src/Exception/ResourceNotFoundException.php b/src/Exception/ResourceNotFoundException.php index 420ac1a8..d667e45c 100644 --- a/src/Exception/ResourceNotFoundException.php +++ b/src/Exception/ResourceNotFoundException.php @@ -14,7 +14,7 @@ /** * @author Tobias Nyholm */ -final class ResourceNotFoundException extends \RuntimeException implements NotFoundExceptionInterface +final class ResourceNotFoundException extends RuntimeException implements NotFoundExceptionInterface { public function __construct( public readonly string $uri, diff --git a/src/Exception/ResourceReadException.php b/src/Exception/ResourceReadException.php index a89dec8e..b040fa31 100644 --- a/src/Exception/ResourceReadException.php +++ b/src/Exception/ResourceReadException.php @@ -14,6 +14,6 @@ /** * @author Tobias Nyholm */ -final class ResourceReadException extends \RuntimeException implements ExceptionInterface +final class ResourceReadException extends RuntimeException { } diff --git a/src/Exception/RootsException.php b/src/Exception/RootsException.php index 8d12cb06..676c8236 100644 --- a/src/Exception/RootsException.php +++ b/src/Exception/RootsException.php @@ -19,6 +19,6 @@ * * @author Johannes Wachter */ -final class RootsException extends \RuntimeException implements ExceptionInterface +final class RootsException extends RuntimeException { } diff --git a/src/Exception/RuntimeException.php b/src/Exception/RuntimeException.php index b68a1bf4..ca379bfc 100644 --- a/src/Exception/RuntimeException.php +++ b/src/Exception/RuntimeException.php @@ -14,6 +14,6 @@ /** * @author Christopher Hertel */ -final class RuntimeException extends \RuntimeException implements ExceptionInterface +class RuntimeException extends \RuntimeException implements ExceptionInterface { } diff --git a/src/Exception/SamplingException.php b/src/Exception/SamplingException.php index 17abcebc..a4362d0c 100644 --- a/src/Exception/SamplingException.php +++ b/src/Exception/SamplingException.php @@ -19,6 +19,6 @@ * * @author Kyrian Obikwelu */ -final class SamplingException extends \RuntimeException implements ExceptionInterface +final class SamplingException extends RuntimeException { } diff --git a/src/Exception/ToolCallException.php b/src/Exception/ToolCallException.php index 01ba9f45..c872509d 100644 --- a/src/Exception/ToolCallException.php +++ b/src/Exception/ToolCallException.php @@ -14,6 +14,6 @@ /** * @author Tobias Nyholm */ -final class ToolCallException extends \RuntimeException implements ExceptionInterface +final class ToolCallException extends RuntimeException { } diff --git a/src/Exception/ToolNotFoundException.php b/src/Exception/ToolNotFoundException.php index 0a864e75..c57f95ed 100644 --- a/src/Exception/ToolNotFoundException.php +++ b/src/Exception/ToolNotFoundException.php @@ -14,7 +14,7 @@ /** * @author Tobias Nyholm */ -final class ToolNotFoundException extends \RuntimeException implements NotFoundExceptionInterface +final class ToolNotFoundException extends RuntimeException implements NotFoundExceptionInterface { public function __construct( public readonly string $name, diff --git a/tests/Unit/Exception/ExceptionHierarchyTest.php b/tests/Unit/Exception/ExceptionHierarchyTest.php new file mode 100644 index 00000000..920c7eab --- /dev/null +++ b/tests/Unit/Exception/ExceptionHierarchyTest.php @@ -0,0 +1,71 @@ +assertTrue( + is_subclass_of($class, ExceptionInterface::class), + \sprintf('"%s" must implement "%s".', $class, ExceptionInterface::class), + ); + + foreach (self::BASE_CLASSES as $base) { + if (is_subclass_of($class, $base)) { + $this->addToAssertionCount(1); + + return; + } + } + + $this->fail(\sprintf('"%s" must extend one of the Mcp\Exception base classes, so it is catchable through them.', $class)); + } + + /** + * @return iterable + */ + public static function provideConcreteExceptions(): iterable + { + foreach (glob(\dirname(__DIR__, 3).'/src/Exception/*.php') ?: [] as $file) { + $class = 'Mcp\Exception\\'.basename($file, '.php'); + + if (!class_exists($class) || \in_array($class, self::BASE_CLASSES, true)) { + continue; + } + + yield $class => [$class]; + } + } +} From 2feb332b522d3d9aced9c61e5998cd268b1fe0ed Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Sat, 22 Aug 2026 04:06:08 +0200 Subject: [PATCH 2/2] Re-throw InvalidInputMessageException unchanged in MessageFactory --- src/JsonRpc/MessageFactory.php | 2 ++ tests/Unit/JsonRpc/MessageFactoryTest.php | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/JsonRpc/MessageFactory.php b/src/JsonRpc/MessageFactory.php index 27860a2c..279b17d5 100644 --- a/src/JsonRpc/MessageFactory.php +++ b/src/JsonRpc/MessageFactory.php @@ -191,6 +191,8 @@ private function createMessage(array $data): MessageInterface $messageClass = $this->findMessageClassByMethod($data['method']); return $messageClass::fromArray($data); + } catch (InvalidInputMessageException $e) { + throw $e; } catch (InvalidArgumentException $e) { throw new InvalidInputMessageException($e->getMessage(), 0, $e); } diff --git a/tests/Unit/JsonRpc/MessageFactoryTest.php b/tests/Unit/JsonRpc/MessageFactoryTest.php index fe2c0d2a..fe25900b 100644 --- a/tests/Unit/JsonRpc/MessageFactoryTest.php +++ b/tests/Unit/JsonRpc/MessageFactoryTest.php @@ -241,6 +241,18 @@ public function testMissingAllIdentifyingFields(): void $this->assertStringContainsString('missing', $results[0]->getMessage()); } + public function testInvalidInputMessageExceptionIsNotReWrapped(): void + { + $missingMethod = $this->factory->create('{"jsonrpc": "2.0", "params": {}}'); + $unknownMethod = $this->factory->create('{"jsonrpc": "2.0", "method": "unknown/method", "id": 1}'); + + foreach ([$missingMethod[0], $unknownMethod[0]] as $exception) { + $this->assertInstanceOf(InvalidInputMessageException::class, $exception); + $this->assertSame(InvalidInputMessageException::class, $exception::class); + $this->assertNull($exception->getPrevious()); + } + } + public function testUnknownMethod(): void { $json = '{"jsonrpc": "2.0", "method": "unknown/method", "id": 1}';