diff --git a/src/mcp-sdk/doc/index.rst b/src/mcp-sdk/doc/index.rst index 36606601..4f076a5c 100644 --- a/src/mcp-sdk/doc/index.rst +++ b/src/mcp-sdk/doc/index.rst @@ -56,8 +56,17 @@ Capabilities Any client would like to discover the capabilities of the server. Exactly what the server supports is defined in the ``Symfony\AI\McpSdk\Server\RequestHandler\InitializeHandler``. -When the client connects, it sees the capabilities and will ask the server to list -the tools/resource/prompts etc. When you want to add a new capability, example a +When the client connects, it sends an ``initialize`` request. The server responds +with its capabilities, which are defined by two main classes: + +* ``Symfony\AI\McpSdk\Capability\Server\Implementation``: Describes the server software itself (e.g., its name and version). +* ``Symfony\AI\McpSdk\Capability\Server\ServerCapabilities``: Details the features the server supports, such as tools, prompts, or resources. + +After that client see available capabilities and will ask the server to list the tools/resource/prompts etc. depending on ``ServerCapabilities`` you provided. +Also ``initialize`` request provides information about protocol version, instructions and ``_meta`` field. +You can find more information about ``Initialize`` response `InitializeResult `_ + +When you want to add a new tool for example: **Tool** that can tell the current time, you need to provide some metadata to the ``Symfony\AI\McpSdk\Server\RequestHandler\ToolListHandler``:: diff --git a/src/mcp-sdk/examples/cli/src/Builder.php b/src/mcp-sdk/examples/cli/src/Builder.php index c2fc9cee..39c1e0db 100644 --- a/src/mcp-sdk/examples/cli/src/Builder.php +++ b/src/mcp-sdk/examples/cli/src/Builder.php @@ -11,8 +11,14 @@ namespace App; +use Symfony\AI\McpSdk\Capability\Prompt\PromptCapability; use Symfony\AI\McpSdk\Capability\PromptChain; +use Symfony\AI\McpSdk\Capability\Resource\ResourceCapability; use Symfony\AI\McpSdk\Capability\ResourceChain; +use Symfony\AI\McpSdk\Capability\Server\Implementation; +use Symfony\AI\McpSdk\Capability\Server\ProtocolVersion; +use Symfony\AI\McpSdk\Capability\Server\ServerCapabilities; +use Symfony\AI\McpSdk\Capability\Tool\ToolCapability; use Symfony\AI\McpSdk\Capability\ToolChain; use Symfony\AI\McpSdk\Server\NotificationHandler\InitializedHandler; use Symfony\AI\McpSdk\Server\NotificationHandlerInterface; @@ -45,8 +51,23 @@ public static function buildRequestHandlers(): array new ExampleTool(), ]); + $implementation = new Implementation( + name: 'MCP-SDK-CLI-Example', + version: '0.1.0' + ); + $serverCapabilities = new ServerCapabilities( + prompts: new PromptCapability(listChanged: false), + resources: new ResourceCapability(subscribe: false, listChanged: false), + tools: new ToolCapability(listChanged: false), + ); + return [ - new InitializeHandler(), + new InitializeHandler( + implementation: $implementation, + serverCapabilities: $serverCapabilities, + protocolVersion: ProtocolVersion::V2025_03_26, + instructions: 'Optional LLM instructions/hints', + ), new PingHandler(), new PromptListHandler($promptManager), new PromptGetHandler($promptManager), diff --git a/src/mcp-sdk/src/Capability/Completion/CompletionCapability.php b/src/mcp-sdk/src/Capability/Completion/CompletionCapability.php new file mode 100644 index 00000000..c290d23b --- /dev/null +++ b/src/mcp-sdk/src/Capability/Completion/CompletionCapability.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\McpSdk\Capability\Completion; + +/** + * Present if the server supports argument autocompletion suggestions. + * + * @see https://modelcontextprotocol.io/specification/2025-06-18/schema#servercapabilities-completions + */ +final class CompletionCapability +{ + public function __construct( + ) { + } +} diff --git a/src/mcp-sdk/src/Capability/Logging/LoggingCapability.php b/src/mcp-sdk/src/Capability/Logging/LoggingCapability.php new file mode 100644 index 00000000..a671a08e --- /dev/null +++ b/src/mcp-sdk/src/Capability/Logging/LoggingCapability.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\McpSdk\Capability\Logging; + +/** + * Present if the server supports sending log messages to the client. + * + * @see https://modelcontextprotocol.io/specification/2025-06-18/schema#servercapabilities-logging + */ +final class LoggingCapability +{ + public function __construct( + ) { + } +} diff --git a/src/mcp-sdk/src/Capability/Prompt/PromptCapability.php b/src/mcp-sdk/src/Capability/Prompt/PromptCapability.php new file mode 100644 index 00000000..86a405d7 --- /dev/null +++ b/src/mcp-sdk/src/Capability/Prompt/PromptCapability.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\McpSdk\Capability\Prompt; + +/** + * Present if the server offers any prompt templates. + * + * @see https://modelcontextprotocol.io/specification/2025-06-18/schema#servercapabilities-logging + */ +final readonly class PromptCapability +{ + public function __construct( + /** Whether this server supports notifications for changes to the prompt list. */ + public ?bool $listChanged = null, + ) { + } +} diff --git a/src/mcp-sdk/src/Capability/Resource/ResourceCapability.php b/src/mcp-sdk/src/Capability/Resource/ResourceCapability.php new file mode 100644 index 00000000..6d1a95f2 --- /dev/null +++ b/src/mcp-sdk/src/Capability/Resource/ResourceCapability.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\McpSdk\Capability\Resource; + +/** + * Present if the server offers any resources to read. + * + * @see https://modelcontextprotocol.io/specification/2025-06-18/schema#servercapabilities-resources + */ +final readonly class ResourceCapability +{ + public function __construct( + /** Whether this server supports notifications for changes to the resource list. */ + public ?bool $subscribe = null, + /** Whether this server supports subscribing to resource updates. */ + public ?bool $listChanged = null, + ) { + } +} diff --git a/src/mcp-sdk/src/Capability/Server/Implementation.php b/src/mcp-sdk/src/Capability/Server/Implementation.php new file mode 100644 index 00000000..5dfaa795 --- /dev/null +++ b/src/mcp-sdk/src/Capability/Server/Implementation.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\McpSdk\Capability\Server; + +/** + * Describes the name and version of an MCP implementation, with an optional title for UI representation. + * + * @see https://modelcontextprotocol.io/specification/2025-06-18/schema#implementation + */ +final readonly class Implementation +{ + public function __construct( + public string $name = 'app', + public string $version = 'dev', + ) { + } +} diff --git a/src/mcp-sdk/src/Capability/Server/ProtocolVersion.php b/src/mcp-sdk/src/Capability/Server/ProtocolVersion.php new file mode 100644 index 00000000..16556c47 --- /dev/null +++ b/src/mcp-sdk/src/Capability/Server/ProtocolVersion.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\McpSdk\Capability\Server; + +enum ProtocolVersion: string +{ + case V2024_11_05 = '2024-11-05'; + case V2025_03_26 = '2025-03-26'; + case V2025_06_18 = '2025-06-18'; +} diff --git a/src/mcp-sdk/src/Capability/Server/ServerCapabilities.php b/src/mcp-sdk/src/Capability/Server/ServerCapabilities.php new file mode 100644 index 00000000..d8de6955 --- /dev/null +++ b/src/mcp-sdk/src/Capability/Server/ServerCapabilities.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\McpSdk\Capability\Server; + +use Symfony\AI\McpSdk\Capability\Completion\CompletionCapability; +use Symfony\AI\McpSdk\Capability\Logging\LoggingCapability; +use Symfony\AI\McpSdk\Capability\Prompt\PromptCapability; +use Symfony\AI\McpSdk\Capability\Resource\ResourceCapability; +use Symfony\AI\McpSdk\Capability\Tool\ToolCapability; + +/** + * Capabilities that a server may support. Known capabilities are defined here, in this schema, + * but this is not a closed set: any server can define its own, additional capabilities. + * + * @see https://modelcontextprotocol.io/specification/2025-06-18/schema#servercapabilities + */ +final readonly class ServerCapabilities implements \JsonSerializable +{ + /** + * @param array>|null $experimental + */ + public function __construct( + /** + * Present if the server supports sending log messages to the client. + * + * @see https://modelcontextprotocol.io/specification/2025-06-18/schema#servercapabilities-logging + */ + public ?LoggingCapability $logging = null, + /** + * Present if the server offers any prompt templates. + * + * @see https://modelcontextprotocol.io/specification/2025-06-18/schema#servercapabilities-prompts + */ + public ?PromptCapability $prompts = null, + /** + * Present if the server offers any resources to read. + * + * @see https://modelcontextprotocol.io/specification/2025-06-18/schema#servercapabilities-resources + */ + public ?ResourceCapability $resources = null, + /** + * Present if the server offers any tools to call. + * + * @see https://modelcontextprotocol.io/specification/2025-06-18/schema#servercapabilities-tools + */ + public ?ToolCapability $tools = null, + /** + * Present if the server supports argument autocompletion suggestions. + * + * @see https://modelcontextprotocol.io/specification/2025-06-18/schema#servercapabilities-completions + */ + public ?CompletionCapability $completions = null, + /** + * @var array>|null + */ + public ?array $experimental = null, + ) { + } + + /** + * @return array{ + * logging?: LoggingCapability, + * prompts?: PromptCapability, + * resources?: ResourceCapability, + * tools?: ToolCapability, + * completions?: CompletionCapability, + * experimental?: array> + * } + */ + public function jsonSerialize(): array + { + return array_filter((array) $this, fn ($value) => null !== $value); + } +} diff --git a/src/mcp-sdk/src/Capability/Tool/ToolCapability.php b/src/mcp-sdk/src/Capability/Tool/ToolCapability.php new file mode 100644 index 00000000..0c2c3170 --- /dev/null +++ b/src/mcp-sdk/src/Capability/Tool/ToolCapability.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\McpSdk\Capability\Tool; + +/** + * Present if the server offers any tools to call. + * + * @see https://modelcontextprotocol.io/specification/2025-06-18/schema#servercapabilities-tools + */ +final readonly class ToolCapability +{ + public function __construct( + /** Whether this server supports notifications for changes to the tool list. */ + public ?bool $listChanged = null, + ) { + } +} diff --git a/src/mcp-sdk/src/Field/MetaField.php b/src/mcp-sdk/src/Field/MetaField.php new file mode 100644 index 00000000..587f1854 --- /dev/null +++ b/src/mcp-sdk/src/Field/MetaField.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\McpSdk\Field; + +/** + * The _meta property/parameter is reserved by MCP to allow clients and servers to attach additional metadata to their interactions. Certain key names are reserved by MCP for protocol-level metadata, as specified below; implementations MUST NOT make assumptions about values at these keys. Additionally, definitions in the schema may reserve particular names for purpose-specific metadata, as declared in those definitions. Key name format: valid _meta key names have two segments: an optional prefix, and a name. Prefix: + * + * If specified, MUST be a series of labels separated by dots (.), followed by a slash (/). + * Labels MUST start with a letter and end with a letter or digit; interior characters can be letters, digits, or hyphens (-). + * Any prefix beginning with zero or more valid labels, followed by modelcontextprotocol or mcp, followed by any valid label, is reserved for MCP use. + * For example: modelcontextprotocol.io/, mcp.dev/, api.modelcontextprotocol.org/, and tools.mcp.com/ are all reserved. + * + * Name: + * + * Unless empty, MUST begin and end with an alphanumeric character ([a-z0-9A-Z]). + * MAY contain hyphens (-), underscores (_), dots (.), and alphanumerics in between. + * + * @see https://modelcontextprotocol.io/specification/2025-06-18/basic/index#general-fields + */ +final readonly class MetaField +{ + /** + * @param array $meta + */ + public function __construct( + public array $meta, + ) { + } +} diff --git a/src/mcp-sdk/src/Message/Notification.php b/src/mcp-sdk/src/Message/Notification.php index b094be89..5da77b6a 100644 --- a/src/mcp-sdk/src/Message/Notification.php +++ b/src/mcp-sdk/src/Message/Notification.php @@ -22,6 +22,11 @@ public function __construct( ) { } + public function __toString(): string + { + return \sprintf('%s', $this->method); + } + /** * @param array{method: string, params?: array} $data */ @@ -44,9 +49,4 @@ public function jsonSerialize(): array 'params' => $this->params, ]; } - - public function __toString(): string - { - return \sprintf('%s', $this->method); - } } diff --git a/src/mcp-sdk/src/Message/Request.php b/src/mcp-sdk/src/Message/Request.php index afa98cd3..8ec35cb9 100644 --- a/src/mcp-sdk/src/Message/Request.php +++ b/src/mcp-sdk/src/Message/Request.php @@ -23,6 +23,11 @@ public function __construct( ) { } + public function __toString(): string + { + return \sprintf('%s: %s', $this->id, $this->method); + } + /** * @param array{id: string|int, method: string, params?: array} $data */ @@ -47,9 +52,4 @@ public function jsonSerialize(): array 'params' => $this->params, ]; } - - public function __toString(): string - { - return \sprintf('%s: %s', $this->id, $this->method); - } } diff --git a/src/mcp-sdk/src/Message/Response.php b/src/mcp-sdk/src/Message/Response.php index 2b26d9d2..a4142fe5 100644 --- a/src/mcp-sdk/src/Message/Response.php +++ b/src/mcp-sdk/src/Message/Response.php @@ -30,7 +30,7 @@ public function jsonSerialize(): array return [ 'jsonrpc' => '2.0', 'id' => $this->id, - 'result' => $this->result, + 'result' => array_filter($this->result, fn ($value) => null !== $value), ]; } } diff --git a/src/mcp-sdk/src/Server/NotificationHandler/InitializedHandler.php b/src/mcp-sdk/src/Server/NotificationHandler/InitializedHandler.php index cff8a3f0..fcc680c4 100644 --- a/src/mcp-sdk/src/Server/NotificationHandler/InitializedHandler.php +++ b/src/mcp-sdk/src/Server/NotificationHandler/InitializedHandler.php @@ -15,12 +15,12 @@ final class InitializedHandler extends BaseNotificationHandler { - protected function supportedNotification(): string + public function handle(Notification $notification): void { - return 'initialized'; } - public function handle(Notification $notification): void + protected function supportedNotification(): string { + return 'initialized'; } } diff --git a/src/mcp-sdk/src/Server/RequestHandler/InitializeHandler.php b/src/mcp-sdk/src/Server/RequestHandler/InitializeHandler.php index f04cdb91..f25e5506 100644 --- a/src/mcp-sdk/src/Server/RequestHandler/InitializeHandler.php +++ b/src/mcp-sdk/src/Server/RequestHandler/InitializeHandler.php @@ -11,27 +11,69 @@ namespace Symfony\AI\McpSdk\Server\RequestHandler; +use Symfony\AI\McpSdk\Capability\Server\Implementation; +use Symfony\AI\McpSdk\Capability\Server\ProtocolVersion; +use Symfony\AI\McpSdk\Capability\Server\ServerCapabilities; +use Symfony\AI\McpSdk\Field\MetaField; use Symfony\AI\McpSdk\Message\Request; use Symfony\AI\McpSdk\Message\Response; final class InitializeHandler extends BaseRequestHandler { public function __construct( - private readonly string $name = 'app', - private readonly string $version = 'dev', + /** + * Describes the name and version of an MCP implementation, with an optional title for UI representation. + * + * @see https://modelcontextprotocol.io/specification/2025-06-18/schema#implementation + */ + private readonly Implementation $implementation, + /** + * Capabilities that a server may support. Known capabilities are defined here, in this schema, + * but this is not a closed set: any server can define its own, additional capabilities. + * + * @see https://modelcontextprotocol.io/specification/2025-06-18/schema#servercapabilities + */ + private readonly ServerCapabilities $serverCapabilities, + /** + * The version of the Model Context Protocol that the server wants to use. + * This may not match the version that the client requested. + * If the client cannot support this version, it MUST disconnect. + * + * @see https://modelcontextprotocol.io/specification/2025-06-18/schema#initializeresult-protocolversion + */ + private readonly ProtocolVersion $protocolVersion = ProtocolVersion::V2025_03_26, + /** + * Instructions describing how to use the server and its features. + * + * This can be used by clients to improve the LLM’s understanding of available tools, resources, etc. + * It can be thought of like a “hint” to the model. + * For example, this information MAY be added to the system prompt. + * + * @see https://modelcontextprotocol.io/specification/2025-06-18/schema#initializeresult-instructions + */ + private readonly ?string $instructions = null, + /** + * The _meta property/parameter is reserved by MCP to allow clients and servers to attach additional metadata to their interactions. + * + * @see https://modelcontextprotocol.io/specification/2025-06-18/basic/index#general-fields + */ + private readonly ?MetaField $metaField = null, ) { } + /** + * After receiving an initialize request from the client, the server sends this response. + * + * @see https://modelcontextprotocol.io/specification/2025-06-18/schema#initializeresult. + */ public function createResponse(Request $message): Response { return new Response($message->id, [ - 'protocolVersion' => '2025-03-26', - 'capabilities' => [ - 'prompts' => ['listChanged' => false], - 'tools' => ['listChanged' => false], - 'resources' => ['listChanged' => false, 'subscribe' => false], - ], - 'serverInfo' => ['name' => $this->name, 'version' => $this->version], + 'protocolVersion' => $this->protocolVersion, + 'capabilities' => $this->serverCapabilities->jsonSerialize(), + 'serverInfo' => $this->implementation, + 'instructions' => $this->instructions, + '_meta' => $this->metaField, ]); } diff --git a/src/mcp-sdk/tests/Server/RequestHandler/InitializeHandlerTest.php b/src/mcp-sdk/tests/Server/RequestHandler/InitializeHandlerTest.php new file mode 100644 index 00000000..95fa7c0b --- /dev/null +++ b/src/mcp-sdk/tests/Server/RequestHandler/InitializeHandlerTest.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\McpSdk\Tests\Server\RequestHandler; + +use PHPUnit\Framework\TestCase; +use Symfony\AI\McpSdk\Capability\Server\Implementation; +use Symfony\AI\McpSdk\Capability\Server\ProtocolVersion; +use Symfony\AI\McpSdk\Capability\Server\ServerCapabilities; +use Symfony\AI\McpSdk\Capability\Tool\ToolCapability; +use Symfony\AI\McpSdk\Message\Request; +use Symfony\AI\McpSdk\Server\RequestHandler\InitializeHandler; + +class InitializeHandlerTest extends TestCase +{ + public function testCreateResponse() + { + $implementation = new Implementation('TestServer', '1.0.0'); + $serverCapabilities = new ServerCapabilities( + logging: null, + tools: new ToolCapability(listChanged: true), + ); + + $handler = new InitializeHandler( + $implementation, + $serverCapabilities, + ProtocolVersion::V2024_11_05, + 'Test instructions', + null, + ); + + $request = new Request(1, 'initialize', []); + $response = $handler->createResponse($request); + $serializedResponse = json_decode(json_encode($response), true); + + $this->assertIsArray($serializedResponse['result']); + + $this->assertArrayHasKey('protocolVersion', $serializedResponse['result']); + $this->assertEquals('2024-11-05', $serializedResponse['result']['protocolVersion']); + + $this->assertArrayHasKey('serverInfo', $serializedResponse['result']); + $this->assertEquals(['name' => 'TestServer', 'version' => '1.0.0'], $serializedResponse['result']['serverInfo']); + + $this->assertArrayNotHasKey('_meta', $serializedResponse['result']); + + $this->assertArrayHasKey('instructions', $serializedResponse['result']); + $this->assertEquals('Test instructions', $serializedResponse['result']['instructions']); + + $this->assertArrayHasKey('capabilities', $serializedResponse['result']); + $capabilities = $serializedResponse['result']['capabilities']; + $this->assertArrayHasKey('tools', $capabilities); + $this->assertTrue($capabilities['tools']['listChanged']); + $this->assertArrayNotHasKey('logging', $capabilities); + $this->assertArrayNotHasKey('prompts', $capabilities); + $this->assertArrayNotHasKey('resources', $capabilities); + $this->assertArrayNotHasKey('completions', $capabilities); + $this->assertArrayNotHasKey('experimental', $capabilities); + } +}