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(static function (ElementReference $reference, array $arguments): string { return 'intercepted'; }); $server = Server::builder() ->setServerInfo('test', '1.0.0') ->setReferenceHandler($referenceHandler) ->addTool(static fn (): string => 'original', name: 'test_tool', description: 'A test tool') ->build(); $result = $this->callTool($server, 'test_tool'); $this->assertSame('intercepted', $result); } #[TestDox('enableExtension() registers an extension and announces its capability payload')] public function testEnableExtensionRegistersExtension(): void { $server = Server::builder() ->setServerInfo('test', '1.0.0') ->enableExtension(new McpApps()) ->build(); $capabilities = $this->extractServerCapabilities($server); $this->assertNotNull($capabilities->extensions); $this->assertArrayHasKey(McpApps::EXTENSION_ID, $capabilities->extensions); $this->assertSame(['mimeTypes' => [McpApps::MIME_TYPE]], $capabilities->extensions[McpApps::EXTENSION_ID]); } #[TestDox('enableExtension() throws when the same extension is enabled twice')] public function testEnableExtensionRejectsDuplicate(): void { $this->expectException(LogicException::class); $this->expectExceptionMessage(McpApps::EXTENSION_ID); Server::builder()->enableExtension(new McpApps(), new McpApps()); } #[TestDox('enableExtension() extensions are merged into capabilities set via setCapabilities()')] public function testEnableExtensionMergesIntoCustomCapabilities(): void { $server = Server::builder() ->setServerInfo('test', '1.0.0') ->setCapabilities(new ServerCapabilities(tools: true)) ->enableExtension(new McpApps()) ->build(); $capabilities = $this->extractServerCapabilities($server); $this->assertNotNull($capabilities->extensions); $this->assertArrayHasKey(McpApps::EXTENSION_ID, $capabilities->extensions); } private function extractServerCapabilities(Server $server): ServerCapabilities { $protocol = (new \ReflectionClass($server))->getProperty('protocol')->getValue($server); $requestHandlers = (new \ReflectionClass($protocol))->getProperty('requestHandlers')->getValue($protocol); foreach ($requestHandlers as $handler) { if ($handler instanceof InitializeHandler) { return $handler->configuration->capabilities; } } $this->fail('InitializeHandler not found in request handlers'); } 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 CallToolHandler) { $request = CallToolRequest::fromArray([ 'jsonrpc' => '2.0', 'method' => 'tools/call', 'id' => 'test-1', 'params' => ['name' => $toolName, 'arguments' => []], ]); $session = $this->createStub(SessionInterface::class); $response = $handler->handle($request, $session); if ($response instanceof Response) { $content = $response->result->content[0] ?? null; return $content instanceof TextContent ? $content->text : null; } $this->fail('Expected Response, got '.$response::class); } } $this->fail('CallToolHandler not found in request handlers'); } }