connect('roots', $this->clientExposing( new Root('file:///workspace/app', 'App'), new Root('file:///workspace/docs', 'Docs'), )); $result = $client->callTool('inspect_roots'); $this->assertInstanceOf(TextContent::class, $result->content[0]); $this->assertSame('file:///workspace/app (App), file:///workspace/docs (Docs)', $result->content[0]->text); } #[TestDox('an empty root list is a valid answer, not a failure')] public function testEmptyRootList(): void { $client = $this->connect('roots', $this->clientExposing()); $result = $client->callTool('inspect_roots'); $this->assertInstanceOf(TextContent::class, $result->content[0]); $this->assertSame('', $result->content[0]->text); } #[TestDox('a client that does not advertise roots is not asked')] public function testCapabilityIsVisibleToTheServer(): void { $client = $this->connect('roots'); $result = $client->callTool('inspect_roots'); $this->assertInstanceOf(TextContent::class, $result->content[0]); $this->assertSame('unsupported', $result->content[0]->text); } #[TestDox('the client can announce that its roots changed')] public function testRootsListChangedNotification(): void { $client = $this->connect('roots', $this->clientExposing(new Root('file:///workspace'))); // A notification has no reply, so what this pins down is that sending // one mid-session leaves the connection usable. $client->sendRootsListChanged(); $this->assertTrue($client->isConnected()); $this->assertInstanceOf(TextContent::class, $client->callTool('inspect_roots')->content[0]); } private function clientExposing(Root ...$roots): ClientBuilder { $callback = new class(array_values($roots)) implements RootsCallbackInterface { /** @param list $roots */ public function __construct(private readonly array $roots) { } public function __invoke(ListRootsRequest $request): ListRootsResult { return new ListRootsResult($this->roots); } }; return $this->clientBuilder() ->setCapabilities(new ClientCapabilities(roots: true, rootsListChanged: true)) ->addRequestHandler(new ListRootsRequestHandler($callback)); } }