Skip to content

Commit faac4a5

Browse files
authored
[Server] add a reference handler setter to server builder (#265)
* 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. * 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 * style: fix codestyle in BuilderTest * style: use imports instead of inline FQCNs in BuilderTest * docs: add changelog entry for setReferenceHandler
1 parent eac6f01 commit faac4a5

File tree

3 files changed

+123
-1
lines changed

3 files changed

+123
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ All notable changes to `mcp/sdk` will be documented in this file.
77

88
* Add built-in authentication middleware for HTTP transport using OAuth
99
* Add client component for building MCP clients
10+
* Add `Builder::setReferenceHandler()` to allow custom `ReferenceHandlerInterface` implementations (e.g. authorization decorators)
1011

1112
0.4.0
1213
-----

src/Server/Builder.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Mcp\Capability\Registry\Loader\DiscoveryLoader;
2323
use Mcp\Capability\Registry\Loader\LoaderInterface;
2424
use Mcp\Capability\Registry\ReferenceHandler;
25+
use Mcp\Capability\Registry\ReferenceHandlerInterface;
2526
use Mcp\Capability\RegistryInterface;
2627
use Mcp\Exception\InvalidArgumentException;
2728
use Mcp\JsonRpc\MessageFactory;
@@ -69,6 +70,8 @@ final class Builder
6970

7071
private ?SchemaGeneratorInterface $schemaGenerator = null;
7172

73+
private ?ReferenceHandlerInterface $referenceHandler = null;
74+
7275
private ?DiscovererInterface $discoverer = null;
7376

7477
private ?SessionManagerInterface $sessionManager = null;
@@ -305,6 +308,13 @@ public function setSchemaGenerator(SchemaGeneratorInterface $schemaGenerator): s
305308
return $this;
306309
}
307310

311+
public function setReferenceHandler(ReferenceHandlerInterface $referenceHandler): self
312+
{
313+
$this->referenceHandler = $referenceHandler;
314+
315+
return $this;
316+
}
317+
308318
public function setDiscoverer(DiscovererInterface $discoverer): self
309319
{
310320
$this->discoverer = $discoverer;
@@ -537,7 +547,7 @@ public function build(): Server
537547

538548
$serverInfo = $this->serverInfo ?? new Implementation();
539549
$configuration = new Configuration($serverInfo, $capabilities, $this->paginationLimit, $this->instructions, $this->protocolVersion);
540-
$referenceHandler = new ReferenceHandler($container);
550+
$referenceHandler = $this->referenceHandler ?? new ReferenceHandler($container);
541551

542552
$requestHandlers = array_merge($this->requestHandlers, [
543553
new Handler\Request\CallToolHandler($registry, $referenceHandler, $logger),

tests/Unit/Server/BuilderTest.php

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Mcp\Tests\Unit\Server;
13+
14+
use Mcp\Capability\Registry\ElementReference;
15+
use Mcp\Capability\Registry\ReferenceHandlerInterface;
16+
use Mcp\Schema\Content\TextContent;
17+
use Mcp\Schema\JsonRpc\Response;
18+
use Mcp\Schema\Request\CallToolRequest;
19+
use Mcp\Server;
20+
use Mcp\Server\Handler\Request\CallToolHandler;
21+
use Mcp\Server\Session\SessionInterface;
22+
use PHPUnit\Framework\Attributes\TestDox;
23+
use PHPUnit\Framework\TestCase;
24+
25+
final class BuilderTest extends TestCase
26+
{
27+
#[TestDox('setReferenceHandler() returns the builder for fluent chaining')]
28+
public function testSetReferenceHandlerReturnsSelf(): void
29+
{
30+
$referenceHandler = $this->createStub(ReferenceHandlerInterface::class);
31+
32+
$builder = Server::builder();
33+
$result = $builder->setReferenceHandler($referenceHandler);
34+
35+
$this->assertSame($builder, $result);
36+
}
37+
38+
#[TestDox('build() succeeds with a custom ReferenceHandler')]
39+
public function testBuildWithCustomReferenceHandler(): void
40+
{
41+
$referenceHandler = $this->createStub(ReferenceHandlerInterface::class);
42+
43+
$server = Server::builder()
44+
->setServerInfo('test', '1.0.0')
45+
->setReferenceHandler($referenceHandler)
46+
->build();
47+
48+
$this->assertInstanceOf(Server::class, $server);
49+
}
50+
51+
#[TestDox('build() succeeds without a custom ReferenceHandler (uses default)')]
52+
public function testBuildWithoutCustomReferenceHandler(): void
53+
{
54+
$server = Server::builder()
55+
->setServerInfo('test', '1.0.0')
56+
->build();
57+
58+
$this->assertInstanceOf(Server::class, $server);
59+
}
60+
61+
#[TestDox('Custom ReferenceHandler is used when calling a tool')]
62+
public function testCustomReferenceHandlerIsUsedForToolCalls(): void
63+
{
64+
$referenceHandler = $this->createMock(ReferenceHandlerInterface::class);
65+
$referenceHandler->expects($this->once())
66+
->method('handle')
67+
->willReturnCallback(static function (ElementReference $reference, array $arguments): string {
68+
return 'intercepted';
69+
});
70+
71+
$server = Server::builder()
72+
->setServerInfo('test', '1.0.0')
73+
->setReferenceHandler($referenceHandler)
74+
->addTool(static fn (): string => 'original', 'test_tool', 'A test tool')
75+
->build();
76+
77+
$result = $this->callTool($server, 'test_tool');
78+
79+
$this->assertSame('intercepted', $result);
80+
}
81+
82+
private function callTool(Server $server, string $toolName): mixed
83+
{
84+
$protocol = (new \ReflectionClass($server))->getProperty('protocol')->getValue($server);
85+
$requestHandlers = (new \ReflectionClass($protocol))->getProperty('requestHandlers')->getValue($protocol);
86+
87+
foreach ($requestHandlers as $handler) {
88+
if ($handler instanceof CallToolHandler) {
89+
$request = CallToolRequest::fromArray([
90+
'jsonrpc' => '2.0',
91+
'method' => 'tools/call',
92+
'id' => 'test-1',
93+
'params' => ['name' => $toolName, 'arguments' => []],
94+
]);
95+
$session = $this->createStub(SessionInterface::class);
96+
97+
$response = $handler->handle($request, $session);
98+
99+
if ($response instanceof Response) {
100+
$content = $response->result->content[0] ?? null;
101+
102+
return $content instanceof TextContent ? $content->text : null;
103+
}
104+
105+
$this->fail('Expected Response, got '.$response::class);
106+
}
107+
}
108+
109+
$this->fail('CallToolHandler not found in request handlers');
110+
}
111+
}

0 commit comments

Comments
 (0)