Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ All notable changes to `mcp/sdk` will be documented in this file.

### JSON-RPC

* Add `UnknownMethodException`, a subclass of `InvalidInputMessageException` for a method the factory has not registered.
* [BC Break] `MessageFactory::create()` returns one `MessageInterface` and throws `InvalidInputMessageException` for invalid input, instead of returning a list holding messages and exceptions.
* [BC Break] Remove `MessageFactory::DEFAULT_MAX_BATCH_SIZE` and the `$maxBatchSize` parameter of `MessageFactory::__construct()` and `MessageFactory::make()`, whose first parameter is now `$additional`.

Expand Down
19 changes: 19 additions & 0 deletions src/Exception/UnknownMethodException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

/*
* This file is part of the official PHP MCP SDK.
*
* A collaboration between Symfony and the PHP Foundation.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Mcp\Exception;

/**
* @author Christopher Hertel <mail@christopher-hertel.de>
*/
final class UnknownMethodException extends InvalidInputMessageException
{
}
6 changes: 4 additions & 2 deletions src/JsonRpc/MessageFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use Mcp\Exception\InvalidArgumentException;
use Mcp\Exception\InvalidInputMessageException;
use Mcp\Exception\UnknownMethodException;
use Mcp\Schema;
use Mcp\Schema\JsonRpc\Error;
use Mcp\Schema\JsonRpc\MessageInterface;
Expand Down Expand Up @@ -95,6 +96,7 @@ public static function make(array $additional = []): self
* Creates a message object from JSON input.
*
* @throws \JsonException When the input string is not valid JSON
* @throws UnknownMethodException When the method is not registered
* @throws InvalidInputMessageException When the input is not a valid message
*/
public function create(string $input): MessageInterface
Expand Down Expand Up @@ -177,7 +179,7 @@ private function createMessage(\stdClass|array $message): MessageInterface
*
* @return class-string<Request>|class-string<Notification>
*
* @throws InvalidInputMessageException
* @throws UnknownMethodException
*/
private function findMessageClassByMethod(string $method): string
{
Expand All @@ -187,6 +189,6 @@ private function findMessageClassByMethod(string $method): string
}
}

throw new InvalidInputMessageException(\sprintf('Unknown method "%s".', $method));
throw new UnknownMethodException(\sprintf('Unknown method "%s".', $method));
}
}
13 changes: 4 additions & 9 deletions src/Server/Stateless/StatelessProtocol.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use Mcp\Exception\MissingRequestMetaException;
use Mcp\Exception\MissingRequiredClientCapabilityException;
use Mcp\Exception\RequestStateException;
use Mcp\Exception\UnknownMethodException;
use Mcp\JsonRpc\MessageFactory;
use Mcp\Schema\Enum\ProtocolVersion;
use Mcp\Schema\JsonRpc\Error;
Expand Down Expand Up @@ -458,16 +459,10 @@ private function dispatch(string $method, ?array $params, string $body, RequestM
{
try {
$request = $this->messageFactory->create($body);
} catch (UnknownMethodException) {
return StatelessResult::error($this->unknownMethod($method, $id), 404);
} catch (InvalidInputMessageException $e) {
// An unknown method (-32601) tells the client to stop asking; a malformed request (-32600) does not.
$unknownMethod = \sprintf('Unknown method "%s".', $method) === $e->getMessage();

return StatelessResult::error(
$unknownMethod
? $this->unknownMethod($method, $id)
: Error::forInvalidRequest($e->getMessage(), $id),
$unknownMethod ? 404 : 400,
);
return StatelessResult::error(Error::forInvalidRequest($e->getMessage(), $id), 400);
} catch (\Throwable $e) {
$this->logger->warning('Rejected an unparseable modern-era request.', ['method' => $method, 'exception' => $e]);

Expand Down
3 changes: 2 additions & 1 deletion tests/Unit/JsonRpc/MessageFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Mcp\Tests\Unit\JsonRpc;

use Mcp\Exception\InvalidInputMessageException;
use Mcp\Exception\UnknownMethodException;
use Mcp\JsonRpc\MessageFactory;
use Mcp\Schema\JsonRpc\Error;
use Mcp\Schema\JsonRpc\Response;
Expand Down Expand Up @@ -280,7 +281,7 @@ public function testUnknownMethod(): void
{
$json = '{"jsonrpc": "2.0", "method": "unknown/method", "id": 1}';

$this->expectException(InvalidInputMessageException::class);
$this->expectException(UnknownMethodException::class);
$this->expectExceptionMessage('Unknown method');

$this->factory->create($json);
Expand Down
29 changes: 29 additions & 0 deletions tests/Unit/Server/Stateless/StatelessProtocolTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
use Mcp\Event\RequestEvent;
use Mcp\Event\ResponseEvent;
use Mcp\Exception\MissingRequiredClientCapabilityException;
use Mcp\JsonRpc\MessageFactory;
use Mcp\Schema\ClientCapabilities;
use Mcp\Schema\Content\TextResourceContents;
use Mcp\Schema\Elicitation\ElicitationSchema;
use Mcp\Schema\Elicitation\StringSchemaDefinition;
use Mcp\Schema\Enum\CacheScope;
use Mcp\Schema\Enum\LoggingLevel;
use Mcp\Schema\Enum\ProtocolVersion;
use Mcp\Schema\Implementation;
use Mcp\Schema\JsonRpc\Error;
use Mcp\Schema\JsonRpc\Request;
use Mcp\Schema\JsonRpc\Response;
Expand All @@ -38,6 +40,7 @@
use Mcp\Schema\ServerCapabilities;
use Mcp\Server;
use Mcp\Server\Authorization\AccessToken;
use Mcp\Server\Configuration;
use Mcp\Server\Handler\Request\RequestHandlerInterface;
use Mcp\Server\RequestContext;
use Mcp\Server\Session\SessionInterface;
Expand All @@ -49,6 +52,7 @@
use Mcp\Server\Subscription\InMemoryNotificationBus;
use Mcp\Server\Suspension\NotificationSuspension;
use Mcp\Server\Wire\CachePolicy;
use Mcp\Tests\Unit\Fixtures\ThrowingRequest;
use Mcp\Tests\Unit\Server\Extension\ThingExtension;
use Mcp\Tests\Unit\Server\Extension\UnservedThingExtension;
use PHPUnit\Framework\Attributes\DataProvider;
Expand Down Expand Up @@ -319,6 +323,31 @@ public function testRemovedMethodsAreUnknown(string $method, array $params): voi
$this->assertSame(Error::METHOD_NOT_FOUND, $answer['body']['error']['code']);
}

#[TestDox('a known method with malformed params is an invalid request, not an unknown method')]
public function testMalformedRequestIsNotAnUnknownMethod(): void
{
$answer = self::call(self::protocol(), 'tools/list', ['cursor' => [1]]);

$this->assertSame(400, $answer['status']);
$this->assertSame(Error::INVALID_REQUEST, $answer['body']['error']['code']);
}

#[TestDox('a request class that crashes while hydrating is answered without leaking the failure')]
public function testCrashWhileHydratingIsAnswered(): void
{
$protocol = new StatelessProtocol(
[],
new MessageFactory([ThrowingRequest::class]),
new Configuration(new Implementation('test-server', '1.0.0'), new ServerCapabilities()),
);

$answer = self::call($protocol, 'test/throwing');

$this->assertSame(404, $answer['status']);
$this->assertSame(Error::METHOD_NOT_FOUND, $answer['body']['error']['code']);
$this->assertStringNotContainsString('must not leak', json_encode($answer['body'], \JSON_THROW_ON_ERROR));
}

#[TestDox('a handshake-era client opening with "initialize" is told which revisions are served')]
public function testInitializeNamesTheServedRevisions(): void
{
Expand Down
Loading