Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----
Expand Down
13 changes: 13 additions & 0 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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.
*
Expand Down
13 changes: 10 additions & 3 deletions src/Client/Protocol.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
Expand All @@ -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<array<string, mixed>>
*/
Expand Down Expand Up @@ -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', [
Expand Down
12 changes: 12 additions & 0 deletions src/Client/State/ClientState.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<int|string, array{request_id: int|string, timestamp: int, timeout: int}> */
private array $pendingRequests = [];
Expand Down Expand Up @@ -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[] = [
Expand Down
11 changes: 11 additions & 0 deletions src/Client/State/ClientStateInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
*
Expand Down
20 changes: 20 additions & 0 deletions tests/Integration/HandshakeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
1 change: 1 addition & 0 deletions tests/Integration/StatelessClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
1 change: 1 addition & 0 deletions tests/Unit/Client/ProtocolTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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')]
Expand Down
Loading