From c16660d6e257aeb7bd252fff0110f4dff0a2051b Mon Sep 17 00:00:00 2001 From: Simon Chrzanowski Date: Wed, 11 Mar 2026 09:57:41 +0100 Subject: [PATCH 1/5] feat: add setReferenceHandler to Builder Allow consumers to provide a custom ReferenceHandlerInterface implementation (e.g. a security-aware decorator) instead of always using the default ReferenceHandler. --- src/Server/Builder.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Server/Builder.php b/src/Server/Builder.php index f97dd509..be0866cd 100644 --- a/src/Server/Builder.php +++ b/src/Server/Builder.php @@ -22,6 +22,7 @@ use Mcp\Capability\Registry\Loader\DiscoveryLoader; use Mcp\Capability\Registry\Loader\LoaderInterface; use Mcp\Capability\Registry\ReferenceHandler; +use Mcp\Capability\Registry\ReferenceHandlerInterface; use Mcp\Capability\RegistryInterface; use Mcp\Exception\InvalidArgumentException; use Mcp\JsonRpc\MessageFactory; @@ -69,6 +70,8 @@ final class Builder private ?SchemaGeneratorInterface $schemaGenerator = null; + private ?ReferenceHandlerInterface $referenceHandler = null; + private ?DiscovererInterface $discoverer = null; private ?SessionManagerInterface $sessionManager = null; @@ -305,6 +308,13 @@ public function setSchemaGenerator(SchemaGeneratorInterface $schemaGenerator): s return $this; } + public function setReferenceHandler(ReferenceHandlerInterface $referenceHandler): self + { + $this->referenceHandler = $referenceHandler; + + return $this; + } + public function setDiscoverer(DiscovererInterface $discoverer): self { $this->discoverer = $discoverer; @@ -537,7 +547,7 @@ public function build(): Server $serverInfo = $this->serverInfo ?? new Implementation(); $configuration = new Configuration($serverInfo, $capabilities, $this->paginationLimit, $this->instructions, $this->protocolVersion); - $referenceHandler = new ReferenceHandler($container); + $referenceHandler = $this->referenceHandler ?? new ReferenceHandler($container); $requestHandlers = array_merge($this->requestHandlers, [ new Handler\Request\CallToolHandler($registry, $referenceHandler, $logger), From 4c492aed7d5cfd938057a033abd93d6fdcb7a30f Mon Sep 17 00:00:00 2001 From: Simon Chrzanowski Date: Wed, 11 Mar 2026 10:05:36 +0100 Subject: [PATCH 2/5] test: add BuilderTest for setReferenceHandler - Fluent API returns builder instance - build() succeeds with and without custom handler - Integration test verifies custom handler intercepts tool calls --- tests/Unit/Server/BuilderTest.php | 107 ++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 tests/Unit/Server/BuilderTest.php diff --git a/tests/Unit/Server/BuilderTest.php b/tests/Unit/Server/BuilderTest.php new file mode 100644 index 00000000..8858c4d4 --- /dev/null +++ b/tests/Unit/Server/BuilderTest.php @@ -0,0 +1,107 @@ +createStub(ReferenceHandlerInterface::class); + + $builder = Server::builder(); + $result = $builder->setReferenceHandler($referenceHandler); + + $this->assertSame($builder, $result); + } + + #[TestDox('build() succeeds with a custom ReferenceHandler')] + public function testBuildWithCustomReferenceHandler(): void + { + $referenceHandler = $this->createStub(ReferenceHandlerInterface::class); + + $server = Server::builder() + ->setServerInfo('test', '1.0.0') + ->setReferenceHandler($referenceHandler) + ->build(); + + $this->assertInstanceOf(Server::class, $server); + } + + #[TestDox('build() succeeds without a custom ReferenceHandler (uses default)')] + public function testBuildWithoutCustomReferenceHandler(): void + { + $server = Server::builder() + ->setServerInfo('test', '1.0.0') + ->build(); + + $this->assertInstanceOf(Server::class, $server); + } + + #[TestDox('Custom ReferenceHandler is used when calling a tool')] + public function testCustomReferenceHandlerIsUsedForToolCalls(): void + { + $referenceHandler = $this->createMock(ReferenceHandlerInterface::class); + $referenceHandler->expects($this->once()) + ->method('handle') + ->willReturnCallback(function (ElementReference $reference, array $arguments): string { + return 'intercepted'; + }); + + $server = Server::builder() + ->setServerInfo('test', '1.0.0') + ->setReferenceHandler($referenceHandler) + ->addTool(static fn (): string => 'original', 'test_tool', 'A test tool') + ->build(); + + $result = $this->callTool($server, 'test_tool'); + + $this->assertSame('intercepted', $result); + } + + private function callTool(Server $server, string $toolName): mixed + { + $protocol = (new \ReflectionClass($server))->getProperty('protocol')->getValue($server); + $requestHandlers = (new \ReflectionClass($protocol))->getProperty('requestHandlers')->getValue($protocol); + + foreach ($requestHandlers as $handler) { + if ($handler instanceof \Mcp\Server\Handler\Request\CallToolHandler) { + $request = \Mcp\Schema\Request\CallToolRequest::fromArray([ + 'jsonrpc' => '2.0', + 'method' => 'tools/call', + 'id' => 'test-1', + 'params' => ['name' => $toolName, 'arguments' => []], + ]); + $session = $this->createStub(\Mcp\Server\Session\SessionInterface::class); + + $response = $handler->handle($request, $session); + + if ($response instanceof \Mcp\Schema\JsonRpc\Response) { + $content = $response->result->content[0] ?? null; + + return $content instanceof \Mcp\Schema\Content\TextContent ? $content->text : null; + } + + $this->fail('Expected Response, got ' . $response::class); + } + } + + $this->fail('CallToolHandler not found in request handlers'); + } +} From ec461077841e2ca58cbd1622fc6e3abdedbd39fa Mon Sep 17 00:00:00 2001 From: Simon Chrzanowski Date: Wed, 11 Mar 2026 10:59:01 +0100 Subject: [PATCH 3/5] style: fix codestyle in BuilderTest --- tests/Unit/Server/BuilderTest.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/Unit/Server/BuilderTest.php b/tests/Unit/Server/BuilderTest.php index 8858c4d4..5788e3c3 100644 --- a/tests/Unit/Server/BuilderTest.php +++ b/tests/Unit/Server/BuilderTest.php @@ -14,7 +14,6 @@ use Mcp\Capability\Registry\ElementReference; use Mcp\Capability\Registry\ReferenceHandlerInterface; use Mcp\Server; -use Mcp\Server\Builder; use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; @@ -60,7 +59,7 @@ public function testCustomReferenceHandlerIsUsedForToolCalls(): void $referenceHandler = $this->createMock(ReferenceHandlerInterface::class); $referenceHandler->expects($this->once()) ->method('handle') - ->willReturnCallback(function (ElementReference $reference, array $arguments): string { + ->willReturnCallback(static function (ElementReference $reference, array $arguments): string { return 'intercepted'; }); @@ -81,14 +80,14 @@ private function callTool(Server $server, string $toolName): mixed $requestHandlers = (new \ReflectionClass($protocol))->getProperty('requestHandlers')->getValue($protocol); foreach ($requestHandlers as $handler) { - if ($handler instanceof \Mcp\Server\Handler\Request\CallToolHandler) { + if ($handler instanceof Server\Handler\Request\CallToolHandler) { $request = \Mcp\Schema\Request\CallToolRequest::fromArray([ 'jsonrpc' => '2.0', 'method' => 'tools/call', 'id' => 'test-1', 'params' => ['name' => $toolName, 'arguments' => []], ]); - $session = $this->createStub(\Mcp\Server\Session\SessionInterface::class); + $session = $this->createStub(Server\Session\SessionInterface::class); $response = $handler->handle($request, $session); @@ -98,7 +97,7 @@ private function callTool(Server $server, string $toolName): mixed return $content instanceof \Mcp\Schema\Content\TextContent ? $content->text : null; } - $this->fail('Expected Response, got ' . $response::class); + $this->fail('Expected Response, got '.$response::class); } } From 339f5d461eb9f6a379e596acf66c94b6bce7f6c5 Mon Sep 17 00:00:00 2001 From: Simon Chrzanowski Date: Wed, 11 Mar 2026 11:00:01 +0100 Subject: [PATCH 4/5] style: use imports instead of inline FQCNs in BuilderTest --- tests/Unit/Server/BuilderTest.php | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/Unit/Server/BuilderTest.php b/tests/Unit/Server/BuilderTest.php index 5788e3c3..e8e54af6 100644 --- a/tests/Unit/Server/BuilderTest.php +++ b/tests/Unit/Server/BuilderTest.php @@ -13,7 +13,12 @@ use Mcp\Capability\Registry\ElementReference; use Mcp\Capability\Registry\ReferenceHandlerInterface; +use Mcp\Schema\Content\TextContent; +use Mcp\Schema\JsonRpc\Response; +use Mcp\Schema\Request\CallToolRequest; use Mcp\Server; +use Mcp\Server\Handler\Request\CallToolHandler; +use Mcp\Server\Session\SessionInterface; use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; @@ -80,21 +85,21 @@ private function callTool(Server $server, string $toolName): mixed $requestHandlers = (new \ReflectionClass($protocol))->getProperty('requestHandlers')->getValue($protocol); foreach ($requestHandlers as $handler) { - if ($handler instanceof Server\Handler\Request\CallToolHandler) { - $request = \Mcp\Schema\Request\CallToolRequest::fromArray([ + if ($handler instanceof CallToolHandler) { + $request = CallToolRequest::fromArray([ 'jsonrpc' => '2.0', 'method' => 'tools/call', 'id' => 'test-1', 'params' => ['name' => $toolName, 'arguments' => []], ]); - $session = $this->createStub(Server\Session\SessionInterface::class); + $session = $this->createStub(SessionInterface::class); $response = $handler->handle($request, $session); - if ($response instanceof \Mcp\Schema\JsonRpc\Response) { + if ($response instanceof Response) { $content = $response->result->content[0] ?? null; - return $content instanceof \Mcp\Schema\Content\TextContent ? $content->text : null; + return $content instanceof TextContent ? $content->text : null; } $this->fail('Expected Response, got '.$response::class); From 6d61987402e3f5cd939d5f6f166386a504895f0e Mon Sep 17 00:00:00 2001 From: Simon Chrzanowski Date: Wed, 11 Mar 2026 11:00:55 +0100 Subject: [PATCH 5/5] docs: add changelog entry for setReferenceHandler --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c3e7037..def81eab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to `mcp/sdk` will be documented in this file. * Add built-in authentication middleware for HTTP transport using OAuth * Add client component for building MCP clients +* Add `Builder::setReferenceHandler()` to allow custom `ReferenceHandlerInterface` implementations (e.g. authorization decorators) 0.4.0 -----