diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d9a859a6..1bf3d2253 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ All notable changes to `mcp/sdk` will be documented in this file. * [BC Break] `AbstractSchemaDefinition` declares an abstract `getDefault()`, which a custom schema definition has to implement. * Fix `Client::getPrompt()` without arguments sending `"arguments": []`, which servers validating the spec's object type (e.g. the TypeScript SDK) reject: empty arguments are now omitted. * Add `Tool::$execution` (`ToolExecution` with a `TaskSupport` enum) and `ServerCapabilities::$tasks`, which were dropped when parsing a 2025-11-25 server's `tools/list` and `initialize` results. +* Add `Client::getServerCapabilities()`, returning what the server declared in `initialize` or, from `2026-07-28` on, in `server/discover`. +* [BC Break] `ClientStateInterface` declares `setServerCapabilities()` and `getServerCapabilities()`, which a custom implementation has to add. 0.8.0 ----- diff --git a/src/Client.php b/src/Client.php index 143ab2062..5e40cad27 100644 --- a/src/Client.php +++ b/src/Client.php @@ -48,6 +48,7 @@ use Mcp\Schema\Result\ListResourceTemplatesResult; use Mcp\Schema\Result\ListToolsResult; use Mcp\Schema\Result\ReadResourceResult; +use Mcp\Schema\ServerCapabilities; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; @@ -147,6 +148,18 @@ public function getInstructions(): ?string return $this->protocol->getState()->getInstructions(); } + /** + * Capabilities the server declared: during the handshake, or in its + * `server/discover` result from 2026-07-28 on. + * + * Null until a server declared them, e.g. before connecting or when a + * 2026-07-28 server does not answer `server/discover`. + */ + public function getServerCapabilities(): ?ServerCapabilities + { + return $this->protocol->getState()->getServerCapabilities(); + } + /** * Protocol revision negotiated during the handshake. * diff --git a/src/Client/Protocol.php b/src/Client/Protocol.php index 2d71c00ae..251826c75 100644 --- a/src/Client/Protocol.php +++ b/src/Client/Protocol.php @@ -38,6 +38,7 @@ use Mcp\Schema\Request\DiscoverRequest; use Mcp\Schema\Request\InitializeRequest; use Mcp\Schema\Result\InitializeResult; +use Mcp\Schema\ServerCapabilities; use Mcp\Server\Stateless\RequestMeta; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; @@ -228,6 +229,7 @@ public function initialize(Configuration $config): Response|Error $this->state->setProtocolVersion($negotiated); $this->state->setServerInfo($initResult->serverInfo); $this->state->setInstructions($initResult->instructions); + $this->state->setServerCapabilities($initResult->capabilities); $this->state->setInitialized(true); $this->sendNotification(new InitializedNotification()); @@ -246,9 +248,10 @@ public function initialize(Configuration $config): Response|Error * * There is nothing to negotiate: the revision travels on every request, so * the connection is usable the moment the transport is. `server/discover` - * is only asked because the facade exposes `getServerInfo()`, and a server - * that will not answer it still serves every other method — so a failure - * here is logged and the connection proceeds. + * is only asked because the facade exposes `getServerInfo()` and + * `getServerCapabilities()`, and a server that will not answer it still + * serves every other method — so a failure here is logged and the + * connection proceeds. * * @return Response> */ @@ -300,6 +303,10 @@ private function readDiscovery(array $result): void $this->state->setInstructions($result['instructions']); } + if (\is_array($result['capabilities'] ?? null)) { + $this->state->setServerCapabilities(ServerCapabilities::fromArray($result['capabilities'])); + } + $this->reconcileVersion($result['supportedVersions'] ?? null); $this->logger->info('Discovery complete', [ diff --git a/src/Client/State/ClientState.php b/src/Client/State/ClientState.php index a1e1f8960..8fb865930 100644 --- a/src/Client/State/ClientState.php +++ b/src/Client/State/ClientState.php @@ -15,6 +15,7 @@ use Mcp\Schema\Implementation; use Mcp\Schema\JsonRpc\Error; use Mcp\Schema\JsonRpc\Response; +use Mcp\Schema\ServerCapabilities; /** * In-memory client state implementation. @@ -32,6 +33,7 @@ class ClientState implements ClientStateInterface private ?ProtocolVersion $protocolVersion = null; private ?Implementation $serverInfo = null; private ?string $instructions = null; + private ?ServerCapabilities $serverCapabilities = null; /** @var array */ private array $pendingRequests = []; @@ -128,6 +130,16 @@ public function getInstructions(): ?string return $this->instructions; } + public function setServerCapabilities(ServerCapabilities $capabilities): void + { + $this->serverCapabilities = $capabilities; + } + + public function getServerCapabilities(): ?ServerCapabilities + { + return $this->serverCapabilities; + } + public function storeProgress(string $token, float $progress, ?float $total, ?string $message): void { $this->progressUpdates[] = [ diff --git a/src/Client/State/ClientStateInterface.php b/src/Client/State/ClientStateInterface.php index a5de42d9a..64276279c 100644 --- a/src/Client/State/ClientStateInterface.php +++ b/src/Client/State/ClientStateInterface.php @@ -15,6 +15,7 @@ use Mcp\Schema\Implementation; use Mcp\Schema\JsonRpc\Error; use Mcp\Schema\JsonRpc\Response; +use Mcp\Schema\ServerCapabilities; /** * Interface for client state management. @@ -109,6 +110,16 @@ public function setInstructions(?string $instructions): void; */ public function getInstructions(): ?string; + /** + * Set the capabilities the server declared. + */ + public function setServerCapabilities(ServerCapabilities $capabilities): void; + + /** + * Get the capabilities the server declared, null until it declared them. + */ + public function getServerCapabilities(): ?ServerCapabilities; + /** * Store progress data received from a notification. * diff --git a/tests/Integration/HandshakeTest.php b/tests/Integration/HandshakeTest.php index 5d2132095..99ff46478 100644 --- a/tests/Integration/HandshakeTest.php +++ b/tests/Integration/HandshakeTest.php @@ -82,6 +82,26 @@ public function testServerInfoIsExchanged(): void $this->assertTrue($client->isConnected()); } + #[TestDox('the handshake carries the server capabilities to the client')] + public function testServerCapabilitiesAreExchanged(): void + { + $capabilities = $this->connect('handshake')->getServerCapabilities(); + + // The fixture registers no tools, resources or prompts. + $this->assertNotNull($capabilities); + $this->assertTrue($capabilities->logging); + $this->assertTrue($capabilities->completions); + $this->assertFalse($capabilities->tools); + $this->assertFalse($capabilities->prompts); + $this->assertFalse($capabilities->resources); + } + + #[TestDox('the server capabilities are unset before the handshake')] + public function testServerCapabilitiesAreNullBeforeConnecting(): void + { + $this->assertNull($this->clientBuilder()->build()->getServerCapabilities()); + } + #[TestDox('the negotiated revision is unset before the handshake')] public function testProtocolVersionIsNullBeforeConnecting(): void { diff --git a/tests/Integration/StatelessClientTest.php b/tests/Integration/StatelessClientTest.php index fbd3d59e8..7bf946999 100644 --- a/tests/Integration/StatelessClientTest.php +++ b/tests/Integration/StatelessClientTest.php @@ -75,6 +75,7 @@ public function testConnectWithoutHandshake(): void $this->assertTrue($client->isConnected()); $this->assertSame(ProtocolVersion::V2026_07_28, $client->getProtocolVersion()); $this->assertSame('Stateless Lifecycle Demo', $client->getServerInfo()?->name); + $this->assertTrue($client->getServerCapabilities()?->tools); $client->disconnect(); } diff --git a/tests/Unit/Client/ProtocolTest.php b/tests/Unit/Client/ProtocolTest.php index d2611c023..ad2503425 100644 --- a/tests/Unit/Client/ProtocolTest.php +++ b/tests/Unit/Client/ProtocolTest.php @@ -93,6 +93,7 @@ public function testDiscoveryFailureIsNotFatal(): void $protocol->initialize($config); $this->assertTrue($protocol->getState()->isInitialized()); + $this->assertNull($protocol->getState()->getServerCapabilities()); } #[TestDox('refuses to continue when discovery shows the server has no modern revision')]