From 4842b24461ac5678208b3471e9f37bd8356f616b Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Fri, 5 Sep 2025 10:07:08 +0200 Subject: [PATCH 01/11] Remove internal MCP SDK and migrate to external mcp/sdk package - Remove src/mcp-sdk/ directory entirely - Update MCP Bundle to use external mcp/sdk package instead of internal SDK - Update all namespace imports from Symfony\AI\McpSdk to Mcp - Update composer.json dependencies in MCP Bundle - Update documentation references - Update demo application tool to use new SDK - Add migration notes documenting API changes and current status The external mcp/sdk is still in development with significant API changes. Further work is needed to fully adapt the bundle architecture to the new SDK structure. --- .phpstan/ForbidNativeExceptionRule.php | 1 - CLAUDE.md | 5 +- README.md | 3 +- demo/src/MCP/Tools/CurrentTimeTool.php | 10 +- src/mcp-bundle/MIGRATION_NOTES.md | 29 ++++ src/mcp-bundle/README.md | 4 +- src/mcp-bundle/composer.json | 4 +- src/mcp-bundle/config/services.php | 15 +- src/mcp-bundle/doc/index.rst | 4 +- src/mcp-bundle/src/Command/McpCommand.php | 6 +- .../src/Controller/McpController.php | 6 +- src/mcp-bundle/src/McpBundle.php | 6 +- .../DependencyInjection/McpBundleTest.php | 10 +- src/mcp-sdk/.gitattributes | 6 - src/mcp-sdk/.github/PULL_REQUEST_TEMPLATE.md | 8 - .../.github/workflows/close-pull-request.yml | 20 --- src/mcp-sdk/.gitignore | 4 - src/mcp-sdk/CHANGELOG.md | 24 --- src/mcp-sdk/LICENSE | 19 --- src/mcp-sdk/README.md | 28 --- src/mcp-sdk/composer.json | 42 ----- src/mcp-sdk/doc/index.rst | 158 ----------------- src/mcp-sdk/examples/cli/README.md | 29 ---- src/mcp-sdk/examples/cli/composer.json | 24 --- .../examples/cli/example-requests.json | 12 -- src/mcp-sdk/examples/cli/index.php | 39 ----- src/mcp-sdk/examples/cli/src/Builder.php | 69 -------- .../examples/cli/src/ExamplePrompt.php | 55 ------ .../examples/cli/src/ExampleResource.php | 53 ------ src/mcp-sdk/examples/cli/src/ExampleTool.php | 70 -------- src/mcp-sdk/phpstan.dist.neon | 14 -- src/mcp-sdk/phpunit.xml.dist | 22 --- .../Capability/Prompt/CollectionInterface.php | 26 --- .../Capability/Prompt/IdentifierInterface.php | 20 --- .../Capability/Prompt/MetadataInterface.php | 26 --- .../src/Capability/Prompt/PromptGet.php | 25 --- .../src/Capability/Prompt/PromptGetResult.php | 24 --- .../Prompt/PromptGetResultMessages.php | 27 --- .../Prompt/PromptGetterInterface.php | 24 --- src/mcp-sdk/src/Capability/PromptChain.php | 75 -------- .../Resource/CollectionInterface.php | 29 ---- .../Resource/IdentifierInterface.php | 20 --- .../Capability/Resource/MetadataInterface.php | 29 ---- .../src/Capability/Resource/ResourceRead.php | 21 --- .../Resource/ResourceReadResult.php | 27 --- .../Resource/ResourceReaderInterface.php | 27 --- src/mcp-sdk/src/Capability/ResourceChain.php | 75 -------- .../Capability/Tool/CollectionInterface.php | 26 --- .../Capability/Tool/IdentifierInterface.php | 20 --- .../src/Capability/Tool/MetadataInterface.php | 74 -------- .../Tool/ToolAnnotationsInterface.php | 62 ------- src/mcp-sdk/src/Capability/Tool/ToolCall.php | 25 --- .../src/Capability/Tool/ToolCallResult.php | 27 --- .../Tool/ToolCollectionInterface.php | 20 --- .../Capability/Tool/ToolExecutorInterface.php | 24 --- src/mcp-sdk/src/Capability/ToolChain.php | 77 --------- .../src/Exception/ExceptionInterface.php | 16 -- .../Exception/HandlerNotFoundException.php | 16 -- .../Exception/InvalidArgumentException.php | 19 --- .../src/Exception/InvalidCursorException.php | 21 --- .../InvalidInputMessageException.php | 16 -- .../Exception/NotFoundExceptionInterface.php | 16 -- .../src/Exception/PromptGetException.php | 24 --- .../src/Exception/PromptNotFoundException.php | 23 --- .../Exception/ResourceNotFoundException.php | 23 --- .../src/Exception/ResourceReadException.php | 24 --- .../src/Exception/ToolExecutionException.php | 24 --- .../src/Exception/ToolNotFoundException.php | 23 --- src/mcp-sdk/src/Message/Error.php | 73 -------- src/mcp-sdk/src/Message/Factory.php | 44 ----- src/mcp-sdk/src/Message/Notification.php | 52 ------ src/mcp-sdk/src/Message/Request.php | 55 ------ src/mcp-sdk/src/Message/Response.php | 36 ---- src/mcp-sdk/src/Server.php | 61 ------- src/mcp-sdk/src/Server/JsonRpcHandler.php | 160 ------------------ .../BaseNotificationHandler.php | 28 --- .../InitializedHandler.php | 29 ---- .../Server/NotificationHandlerInterface.php | 28 --- .../RequestHandler/BaseRequestHandler.php | 28 --- .../RequestHandler/InitializeHandler.php | 45 ----- .../src/Server/RequestHandler/PingHandler.php | 31 ---- .../RequestHandler/PromptGetHandler.php | 83 --------- .../RequestHandler/PromptListHandler.php | 85 ---------- .../RequestHandler/ResourceListHandler.php | 79 --------- .../RequestHandler/ResourceReadHandler.php | 59 ------- .../Server/RequestHandler/ToolCallHandler.php | 75 -------- .../Server/RequestHandler/ToolListHandler.php | 78 --------- .../src/Server/RequestHandlerInterface.php | 30 ---- .../Transport/Sse/Store/CachePoolStore.php | 62 ------- .../Server/Transport/Sse/StoreInterface.php | 26 --- .../Server/Transport/Sse/StreamTransport.php | 62 ------- .../Stdio/SymfonyConsoleTransport.php | 67 -------- src/mcp-sdk/src/Server/TransportInterface.php | 28 --- .../tests/Fixtures/InMemoryTransport.php | 50 ------ src/mcp-sdk/tests/Message/ErrorTest.php | 52 ------ src/mcp-sdk/tests/Message/FactoryTest.php | 94 ---------- src/mcp-sdk/tests/Message/ResponseTest.php | 46 ----- .../tests/Server/JsonRpcHandlerTest.php | 90 ---------- .../RequestHandler/PromptListHandlerTest.php | 80 --------- .../ResourceListHandlerTest.php | 117 ------------- .../RequestHandler/ToolListHandlerTest.php | 123 -------------- src/mcp-sdk/tests/ServerTest.php | 50 ------ 102 files changed, 62 insertions(+), 3918 deletions(-) create mode 100644 src/mcp-bundle/MIGRATION_NOTES.md delete mode 100644 src/mcp-sdk/.gitattributes delete mode 100644 src/mcp-sdk/.github/PULL_REQUEST_TEMPLATE.md delete mode 100644 src/mcp-sdk/.github/workflows/close-pull-request.yml delete mode 100644 src/mcp-sdk/.gitignore delete mode 100644 src/mcp-sdk/CHANGELOG.md delete mode 100644 src/mcp-sdk/LICENSE delete mode 100644 src/mcp-sdk/README.md delete mode 100644 src/mcp-sdk/composer.json delete mode 100644 src/mcp-sdk/doc/index.rst delete mode 100644 src/mcp-sdk/examples/cli/README.md delete mode 100644 src/mcp-sdk/examples/cli/composer.json delete mode 100644 src/mcp-sdk/examples/cli/example-requests.json delete mode 100644 src/mcp-sdk/examples/cli/index.php delete mode 100644 src/mcp-sdk/examples/cli/src/Builder.php delete mode 100644 src/mcp-sdk/examples/cli/src/ExamplePrompt.php delete mode 100644 src/mcp-sdk/examples/cli/src/ExampleResource.php delete mode 100644 src/mcp-sdk/examples/cli/src/ExampleTool.php delete mode 100644 src/mcp-sdk/phpstan.dist.neon delete mode 100644 src/mcp-sdk/phpunit.xml.dist delete mode 100644 src/mcp-sdk/src/Capability/Prompt/CollectionInterface.php delete mode 100644 src/mcp-sdk/src/Capability/Prompt/IdentifierInterface.php delete mode 100644 src/mcp-sdk/src/Capability/Prompt/MetadataInterface.php delete mode 100644 src/mcp-sdk/src/Capability/Prompt/PromptGet.php delete mode 100644 src/mcp-sdk/src/Capability/Prompt/PromptGetResult.php delete mode 100644 src/mcp-sdk/src/Capability/Prompt/PromptGetResultMessages.php delete mode 100644 src/mcp-sdk/src/Capability/Prompt/PromptGetterInterface.php delete mode 100644 src/mcp-sdk/src/Capability/PromptChain.php delete mode 100644 src/mcp-sdk/src/Capability/Resource/CollectionInterface.php delete mode 100644 src/mcp-sdk/src/Capability/Resource/IdentifierInterface.php delete mode 100644 src/mcp-sdk/src/Capability/Resource/MetadataInterface.php delete mode 100644 src/mcp-sdk/src/Capability/Resource/ResourceRead.php delete mode 100644 src/mcp-sdk/src/Capability/Resource/ResourceReadResult.php delete mode 100644 src/mcp-sdk/src/Capability/Resource/ResourceReaderInterface.php delete mode 100644 src/mcp-sdk/src/Capability/ResourceChain.php delete mode 100644 src/mcp-sdk/src/Capability/Tool/CollectionInterface.php delete mode 100644 src/mcp-sdk/src/Capability/Tool/IdentifierInterface.php delete mode 100644 src/mcp-sdk/src/Capability/Tool/MetadataInterface.php delete mode 100644 src/mcp-sdk/src/Capability/Tool/ToolAnnotationsInterface.php delete mode 100644 src/mcp-sdk/src/Capability/Tool/ToolCall.php delete mode 100644 src/mcp-sdk/src/Capability/Tool/ToolCallResult.php delete mode 100644 src/mcp-sdk/src/Capability/Tool/ToolCollectionInterface.php delete mode 100644 src/mcp-sdk/src/Capability/Tool/ToolExecutorInterface.php delete mode 100644 src/mcp-sdk/src/Capability/ToolChain.php delete mode 100644 src/mcp-sdk/src/Exception/ExceptionInterface.php delete mode 100644 src/mcp-sdk/src/Exception/HandlerNotFoundException.php delete mode 100644 src/mcp-sdk/src/Exception/InvalidArgumentException.php delete mode 100644 src/mcp-sdk/src/Exception/InvalidCursorException.php delete mode 100644 src/mcp-sdk/src/Exception/InvalidInputMessageException.php delete mode 100644 src/mcp-sdk/src/Exception/NotFoundExceptionInterface.php delete mode 100644 src/mcp-sdk/src/Exception/PromptGetException.php delete mode 100644 src/mcp-sdk/src/Exception/PromptNotFoundException.php delete mode 100644 src/mcp-sdk/src/Exception/ResourceNotFoundException.php delete mode 100644 src/mcp-sdk/src/Exception/ResourceReadException.php delete mode 100644 src/mcp-sdk/src/Exception/ToolExecutionException.php delete mode 100644 src/mcp-sdk/src/Exception/ToolNotFoundException.php delete mode 100644 src/mcp-sdk/src/Message/Error.php delete mode 100644 src/mcp-sdk/src/Message/Factory.php delete mode 100644 src/mcp-sdk/src/Message/Notification.php delete mode 100644 src/mcp-sdk/src/Message/Request.php delete mode 100644 src/mcp-sdk/src/Message/Response.php delete mode 100644 src/mcp-sdk/src/Server.php delete mode 100644 src/mcp-sdk/src/Server/JsonRpcHandler.php delete mode 100644 src/mcp-sdk/src/Server/NotificationHandler/BaseNotificationHandler.php delete mode 100644 src/mcp-sdk/src/Server/NotificationHandler/InitializedHandler.php delete mode 100644 src/mcp-sdk/src/Server/NotificationHandlerInterface.php delete mode 100644 src/mcp-sdk/src/Server/RequestHandler/BaseRequestHandler.php delete mode 100644 src/mcp-sdk/src/Server/RequestHandler/InitializeHandler.php delete mode 100644 src/mcp-sdk/src/Server/RequestHandler/PingHandler.php delete mode 100644 src/mcp-sdk/src/Server/RequestHandler/PromptGetHandler.php delete mode 100644 src/mcp-sdk/src/Server/RequestHandler/PromptListHandler.php delete mode 100644 src/mcp-sdk/src/Server/RequestHandler/ResourceListHandler.php delete mode 100644 src/mcp-sdk/src/Server/RequestHandler/ResourceReadHandler.php delete mode 100644 src/mcp-sdk/src/Server/RequestHandler/ToolCallHandler.php delete mode 100644 src/mcp-sdk/src/Server/RequestHandler/ToolListHandler.php delete mode 100644 src/mcp-sdk/src/Server/RequestHandlerInterface.php delete mode 100644 src/mcp-sdk/src/Server/Transport/Sse/Store/CachePoolStore.php delete mode 100644 src/mcp-sdk/src/Server/Transport/Sse/StoreInterface.php delete mode 100644 src/mcp-sdk/src/Server/Transport/Sse/StreamTransport.php delete mode 100644 src/mcp-sdk/src/Server/Transport/Stdio/SymfonyConsoleTransport.php delete mode 100644 src/mcp-sdk/src/Server/TransportInterface.php delete mode 100644 src/mcp-sdk/tests/Fixtures/InMemoryTransport.php delete mode 100644 src/mcp-sdk/tests/Message/ErrorTest.php delete mode 100644 src/mcp-sdk/tests/Message/FactoryTest.php delete mode 100644 src/mcp-sdk/tests/Message/ResponseTest.php delete mode 100644 src/mcp-sdk/tests/Server/JsonRpcHandlerTest.php delete mode 100644 src/mcp-sdk/tests/Server/RequestHandler/PromptListHandlerTest.php delete mode 100644 src/mcp-sdk/tests/Server/RequestHandler/ResourceListHandlerTest.php delete mode 100644 src/mcp-sdk/tests/Server/RequestHandler/ToolListHandlerTest.php delete mode 100644 src/mcp-sdk/tests/ServerTest.php diff --git a/.phpstan/ForbidNativeExceptionRule.php b/.phpstan/ForbidNativeExceptionRule.php index 9cd6c2e1a..278a611c8 100644 --- a/.phpstan/ForbidNativeExceptionRule.php +++ b/.phpstan/ForbidNativeExceptionRule.php @@ -51,7 +51,6 @@ final class ForbidNativeExceptionRule implements Rule 'Symfony\\AI\\Agent' => 'Symfony\\AI\\Agent\\Exception\\', 'Symfony\\AI\\Platform' => 'Symfony\\AI\\Platform\\Exception\\', 'Symfony\\AI\\Store' => 'Symfony\\AI\\Store\\Exception\\', - 'Symfony\\AI\\McpSdk' => 'Symfony\\AI\\McpSdk\\Exception\\', 'Symfony\\AI\\AiBundle' => 'Symfony\\AI\\AiBundle\\Exception\\', 'Symfony\\AI\\McpBundle' => 'Symfony\\AI\\McpBundle\\Exception\\', ]; diff --git a/CLAUDE.md b/CLAUDE.md index 8689349db..75f2cc81f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,11 +12,10 @@ This is the Symfony AI monorepo containing multiple components and bundles that - **Platform** (`src/platform/`): Unified interface to AI platforms (OpenAI, Anthropic, Azure, Gemini, VertexAI, etc.) - **Agent** (`src/agent/`): Framework for building AI agents that interact with users and perform tasks - **Store** (`src/store/`): Data storage abstraction with indexing and retrieval for vector databases -- **MCP SDK** (`src/mcp-sdk/`): SDK for Model Context Protocol enabling agent-tool communication ### Integration Bundles - **AI Bundle** (`src/ai-bundle/`): Symfony integration for Platform, Store, and Agent components -- **MCP Bundle** (`src/mcp-bundle/`): Symfony integration for MCP SDK +- **MCP Bundle** (`src/mcp-bundle/`): Symfony integration for external MCP SDK ### Supporting Directories - **Examples** (`examples/`): Standalone examples demonstrating component usage across different AI platforms @@ -92,7 +91,7 @@ symfony server:start Components are designed to work independently but have these relationships: - Agent depends on Platform for AI communication - AI Bundle integrates Platform, Agent, and Store -- MCP Bundle provides MCP SDK integration +- MCP Bundle provides external MCP SDK integration - Store is standalone but often used with Agent for RAG applications ## Testing Architecture diff --git a/README.md b/README.md index ac65db86d..affc46511 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,9 @@ Symfony AI consists of several lower and higher level **components** and the res * **[Platform](src/platform/README.md)**: A unified interface to various AI platforms like OpenAI, Anthropic, Azure, Gemini, VertexAI, and more. * **[Agent](src/agent/README.md)**: Framework for building AI agents that can interact with users and perform tasks. * **[Store](src/store/README.md)**: Data storage abstraction with indexing and retrieval for AI applications. - * **[MCP SDK](src/mcp-sdk/README.md)**: SDK for [Model Context Protocol](https://modelcontextprotocol.io) enabling communication between AI agents and tools. * **Bundles** * **[AI Bundle](src/ai-bundle/README.md)**: Symfony integration for AI Platform, Store and Agent components. - * **[MCP Bundle](src/mcp-bundle/README.md)**: Symfony integration for MCP SDK, allowing them to act as MCP servers or clients. + * **[MCP Bundle](src/mcp-bundle/README.md)**: Symfony integration for external [mcp/sdk](https://github.com/modelcontextprotocol/php-sdk), allowing them to act as MCP servers or clients. ## Examples & Demo diff --git a/demo/src/MCP/Tools/CurrentTimeTool.php b/demo/src/MCP/Tools/CurrentTimeTool.php index 9f4a49b18..c43c4de5f 100644 --- a/demo/src/MCP/Tools/CurrentTimeTool.php +++ b/demo/src/MCP/Tools/CurrentTimeTool.php @@ -11,11 +11,11 @@ namespace App\MCP\Tools; -use Symfony\AI\McpSdk\Capability\Tool\MetadataInterface; -use Symfony\AI\McpSdk\Capability\Tool\ToolAnnotationsInterface; -use Symfony\AI\McpSdk\Capability\Tool\ToolCall; -use Symfony\AI\McpSdk\Capability\Tool\ToolCallResult; -use Symfony\AI\McpSdk\Capability\Tool\ToolExecutorInterface; +use Mcp\Capability\Tool\MetadataInterface; +use Mcp\Capability\Tool\ToolAnnotationsInterface; +use Mcp\Capability\Tool\ToolCall; +use Mcp\Capability\Tool\ToolCallResult; +use Mcp\Capability\Tool\ToolExecutorInterface; /** * @author Tom Hart diff --git a/src/mcp-bundle/MIGRATION_NOTES.md b/src/mcp-bundle/MIGRATION_NOTES.md new file mode 100644 index 000000000..8b572c514 --- /dev/null +++ b/src/mcp-bundle/MIGRATION_NOTES.md @@ -0,0 +1,29 @@ +# Migration to External MCP SDK + +This bundle has been updated to use the external `mcp/sdk` package instead of the internal `symfony/mcp-sdk`. + +## Current Status + +⚠️ **Work in Progress**: The external MCP SDK is still in active development and the API has changed significantly from the original internal SDK. + +## Major API Changes + +1. **Namespace changes**: `Symfony\AI\McpSdk` → `Mcp` +2. **Interface changes**: Many interfaces have been renamed or restructured +3. **Architecture changes**: The new SDK uses a unified `Registry` instead of separate tool executor/collection interfaces +4. **Handler changes**: Both notification and request handlers now implement `MethodHandlerInterface` +5. **Transport changes**: `StdioTransport` constructor expects raw resources instead of Symfony console I/O + +## Required Work + +The bundle currently has the namespace imports updated but the service definitions and architecture need to be fully adapted to work with the new SDK structure. This includes: + +1. Updating service definitions in `config/services.php` +2. Adapting the bundle to use the new `Registry` system +3. Updating transport integrations +4. Fixing test assertions +5. Updating the demo application tool example + +## Current Test Status + +Several tests are currently failing due to the namespace and class name changes. These need to be updated once the service architecture is adapted. \ No newline at end of file diff --git a/src/mcp-bundle/README.md b/src/mcp-bundle/README.md index 8c23a9db8..65bce34cd 100644 --- a/src/mcp-bundle/README.md +++ b/src/mcp-bundle/README.md @@ -1,7 +1,7 @@ # MCP Bundle -Symfony integration bundle for [Model Context Protocol](https://modelcontextprotocol.io/) using the Symfony AI -MCP SDK [symfony/mcp-sdk](https://github.com/symfony/mcp-sdk). +Symfony integration bundle for [Model Context Protocol](https://modelcontextprotocol.io/) using the external +MCP SDK [mcp/sdk](https://github.com/modelcontextprotocol/php-sdk). **Currently only supports tools as server via Server-Sent Events (SSE) and STDIO.** diff --git a/src/mcp-bundle/composer.json b/src/mcp-bundle/composer.json index 59bd75573..291aac34b 100644 --- a/src/mcp-bundle/composer.json +++ b/src/mcp-bundle/composer.json @@ -1,7 +1,7 @@ { "name": "symfony/mcp-bundle", "type": "symfony-bundle", - "description": "Symfony integration bundle for Model Context Protocol (via symfony/mcp-sdk)", + "description": "Symfony integration bundle for Model Context Protocol (via mcp/sdk)", "license": "MIT", "authors": [ { @@ -16,7 +16,7 @@ "symfony/framework-bundle": "^6.4 || ^7.0", "symfony/http-foundation": "^6.4 || ^7.0", "symfony/http-kernel": "^6.4 || ^7.0", - "symfony/mcp-sdk": "@dev", + "mcp/sdk": "dev-main", "symfony/routing": "^6.4 || ^7.0" }, "require-dev": { diff --git a/src/mcp-bundle/config/services.php b/src/mcp-bundle/config/services.php index 56d2c11e2..ef2ca9fe8 100644 --- a/src/mcp-bundle/config/services.php +++ b/src/mcp-bundle/config/services.php @@ -11,16 +11,11 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; -use Symfony\AI\McpSdk\Capability\ToolChain; -use Symfony\AI\McpSdk\Message\Factory; -use Symfony\AI\McpSdk\Server; -use Symfony\AI\McpSdk\Server\JsonRpcHandler; -use Symfony\AI\McpSdk\Server\NotificationHandler\InitializedHandler; -use Symfony\AI\McpSdk\Server\RequestHandler\InitializeHandler; -use Symfony\AI\McpSdk\Server\RequestHandler\PingHandler; -use Symfony\AI\McpSdk\Server\RequestHandler\ToolCallHandler; -use Symfony\AI\McpSdk\Server\RequestHandler\ToolListHandler; -use Symfony\AI\McpSdk\Server\Transport\Sse\Store\CachePoolStore; +use Mcp\Server; +use Mcp\Server\NotificationHandler\InitializedHandler; +use Mcp\Server\RequestHandler\InitializeHandler; +use Mcp\Server\RequestHandler\PingHandler; +use Mcp\Server\Transport\Sse\Store\CachePoolStore; return static function (ContainerConfigurator $container): void { $container->services() diff --git a/src/mcp-bundle/doc/index.rst b/src/mcp-bundle/doc/index.rst index 2e8ecb085..62848dc7b 100644 --- a/src/mcp-bundle/doc/index.rst +++ b/src/mcp-bundle/doc/index.rst @@ -1,7 +1,7 @@ MCP Bundle ========== -Symfony integration bundle for `Model Context Protocol`_ using the Symfony AI MCP SDK `symfony/mcp-sdk`_. +Symfony integration bundle for `Model Context Protocol`_ using the external MCP SDK `mcp/sdk`_. **Currently only supports tools as server via Server-Sent Events (SSE) and STDIO.** @@ -64,7 +64,7 @@ Configuration url: 'http://localhost:8000/sse' # URL to SSE endpoint of MCP server .. _`Model Context Protocol`: https://modelcontextprotocol.io/ -.. _`symfony/mcp-sdk`: https://github.com/symfony/mcp-sdk +.. _`mcp/sdk`: https://github.com/modelcontextprotocol/php-sdk .. _`Claude Desktop`: https://claude.ai/download .. _`MCP Server List`: https://modelcontextprotocol.io/examples .. _`AI Bundle`: https://github.com/symfony/ai-bundle diff --git a/src/mcp-bundle/src/Command/McpCommand.php b/src/mcp-bundle/src/Command/McpCommand.php index d3232fa5f..50dd604cd 100644 --- a/src/mcp-bundle/src/Command/McpCommand.php +++ b/src/mcp-bundle/src/Command/McpCommand.php @@ -11,8 +11,8 @@ namespace Symfony\AI\McpBundle\Command; -use Symfony\AI\McpSdk\Server; -use Symfony\AI\McpSdk\Server\Transport\Stdio\SymfonyConsoleTransport; +use Mcp\Server; +use Mcp\Server\Transport\StdioTransport; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -30,7 +30,7 @@ public function __construct( protected function execute(InputInterface $input, OutputInterface $output): int { $this->server->connect( - new SymfonyConsoleTransport($input, $output) + new StdioTransport($input, $output) ); return Command::SUCCESS; diff --git a/src/mcp-bundle/src/Controller/McpController.php b/src/mcp-bundle/src/Controller/McpController.php index 3b036aa47..5cad68e1d 100644 --- a/src/mcp-bundle/src/Controller/McpController.php +++ b/src/mcp-bundle/src/Controller/McpController.php @@ -11,9 +11,9 @@ namespace Symfony\AI\McpBundle\Controller; -use Symfony\AI\McpSdk\Server; -use Symfony\AI\McpSdk\Server\Transport\Sse\Store\CachePoolStore; -use Symfony\AI\McpSdk\Server\Transport\Sse\StreamTransport; +use Mcp\Server; +use Mcp\Server\Transport\Sse\Store\CachePoolStore; +use Mcp\Server\Transport\Sse\StreamTransport; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\StreamedResponse; diff --git a/src/mcp-bundle/src/McpBundle.php b/src/mcp-bundle/src/McpBundle.php index acd2374de..323411a69 100644 --- a/src/mcp-bundle/src/McpBundle.php +++ b/src/mcp-bundle/src/McpBundle.php @@ -11,12 +11,12 @@ namespace Symfony\AI\McpBundle; +use Mcp\Capability\Tool\IdentifierInterface; +use Mcp\Server\NotificationHandlerInterface; +use Mcp\Server\RequestHandlerInterface; use Symfony\AI\McpBundle\Command\McpCommand; use Symfony\AI\McpBundle\Controller\McpController; use Symfony\AI\McpBundle\Routing\RouteLoader; -use Symfony\AI\McpSdk\Capability\Tool\IdentifierInterface; -use Symfony\AI\McpSdk\Server\NotificationHandlerInterface; -use Symfony\AI\McpSdk\Server\RequestHandlerInterface; use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; diff --git a/src/mcp-bundle/tests/DependencyInjection/McpBundleTest.php b/src/mcp-bundle/tests/DependencyInjection/McpBundleTest.php index c2af6a1d4..e0e16e829 100644 --- a/src/mcp-bundle/tests/DependencyInjection/McpBundleTest.php +++ b/src/mcp-bundle/tests/DependencyInjection/McpBundleTest.php @@ -11,14 +11,14 @@ namespace Symfony\AI\McpBundle\Tests\DependencyInjection; +use Mcp\Capability\Tool\IdentifierInterface; +use Mcp\Server\MethodHandlerInterface as RequestHandlerInterface; +use Mcp\Server\NotificationHandlerInterface; +use Mcp\Server\RequestHandler\ListToolsHandler; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\AI\McpBundle\McpBundle; -use Symfony\AI\McpSdk\Capability\Tool\IdentifierInterface; -use Symfony\AI\McpSdk\Server\NotificationHandlerInterface; -use Symfony\AI\McpSdk\Server\RequestHandler\ToolListHandler; -use Symfony\AI\McpSdk\Server\RequestHandlerInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; #[CoversClass(McpBundle::class)] @@ -155,7 +155,7 @@ public function testDefaultPageSizeConfiguration() $this->assertTrue($container->hasDefinition('mcp.server.request_handler.tool_list')); $definition = $container->getDefinition('mcp.server.request_handler.tool_list'); - $this->assertSame(ToolListHandler::class, $definition->getClass()); + $this->assertSame(ListToolsHandler::class, $definition->getClass()); } public function testCustomPageSizeConfiguration() diff --git a/src/mcp-sdk/.gitattributes b/src/mcp-sdk/.gitattributes deleted file mode 100644 index 749150c5d..000000000 --- a/src/mcp-sdk/.gitattributes +++ /dev/null @@ -1,6 +0,0 @@ -/.git* export-ignore -/examples export-ignore -/tests export-ignore -/.php-cs-fixer.dist.php export-ignore -/phpstan.dist.neon export-ignore -/phpunit.xml.dist export-ignore diff --git a/src/mcp-sdk/.github/PULL_REQUEST_TEMPLATE.md b/src/mcp-sdk/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index fcb87228a..000000000 --- a/src/mcp-sdk/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,8 +0,0 @@ -Please do not submit any Pull Requests here. They will be closed. ---- - -Please submit your PR here instead: -https://github.com/symfony/ai - -This repository is what we call a "subtree split": a read-only subset of that main repository. -We're looking forward to your PR there! diff --git a/src/mcp-sdk/.github/workflows/close-pull-request.yml b/src/mcp-sdk/.github/workflows/close-pull-request.yml deleted file mode 100644 index 207153fd5..000000000 --- a/src/mcp-sdk/.github/workflows/close-pull-request.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Close Pull Request - -on: - pull_request_target: - types: [opened] - -jobs: - run: - runs-on: ubuntu-latest - steps: - - uses: superbrothers/close-pull-request@v3 - with: - comment: | - Thanks for your Pull Request! We love contributions. - - However, you should instead open your PR on the main repository: - https://github.com/symfony/ai - - This repository is what we call a "subtree split": a read-only subset of that main repository. - We're looking forward to your PR there! diff --git a/src/mcp-sdk/.gitignore b/src/mcp-sdk/.gitignore deleted file mode 100644 index 22dd1a417..000000000 --- a/src/mcp-sdk/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -.phpunit.cache -.php-cs-fixer.cache -composer.lock -vendor diff --git a/src/mcp-sdk/CHANGELOG.md b/src/mcp-sdk/CHANGELOG.md deleted file mode 100644 index c484c44de..000000000 --- a/src/mcp-sdk/CHANGELOG.md +++ /dev/null @@ -1,24 +0,0 @@ -CHANGELOG -========= - -0.1 ---- - - * Add Model Context Protocol (MCP) implementation for LLM-application communication - * Add JSON-RPC based protocol handling with `JsonRpcHandler` - * Add three core MCP capabilities: - - Resources: File-like data readable by clients (API responses, file contents) - - Tools: Functions callable by LLMs (with user approval) - - Prompts: Pre-written templates for specific tasks - * Add multiple transport implementations: - - Symfony Console Transport for testing and CLI applications - - Stream Transport supporting Server-Sent Events (SSE) and HTTP streaming - - STDIO transport for command-line interfaces - * Add capability chains for organizing features: - - `ToolChain` for tool management - - `ResourceChain` for resource management - - `PromptChain` for prompt template management - * Add Server component managing transport connections - * Add request/notification handlers for MCP operations - * Add standardized interface enabling LLMs to interact with external systems - * Add support for building LLM "plugins" with extra context capabilities \ No newline at end of file diff --git a/src/mcp-sdk/LICENSE b/src/mcp-sdk/LICENSE deleted file mode 100644 index bc38d714e..000000000 --- a/src/mcp-sdk/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2025-present Fabien Potencier - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished -to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/src/mcp-sdk/README.md b/src/mcp-sdk/README.md deleted file mode 100644 index b646e208e..000000000 --- a/src/mcp-sdk/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# Model Context Protocol PHP SDK - -Model Context Protocol SDK for Client and Server applications in PHP. - -**This Component is experimental**. -[Experimental features](https://symfony.com/doc/current/contributing/code/experimental.html) -are not covered by Symfony's -[Backward Compatibility Promise](https://symfony.com/doc/current/contributing/code/bc.html). - -## Installation - -```bash -composer require symfony/mcp-sdk -``` - -This is a low level SDK that implements the [Model Context Protocol](https://modelcontextprotocol.io/) -(MCP). The protocol is used by LLM model to build "plugins" and give them extra -context. Example the logged in users' latest order. - -**This repository is a READ-ONLY sub-tree split**. See -https://github.com/symfony/ai to create issues or submit pull requests. - -## Resources - -- [Documentation](doc/index.rst) -- [Report issues](https://github.com/symfony/ai/issues) and - [send Pull Requests](https://github.com/symfony/ai/pulls) - in the [main Symfony AI repository](https://github.com/symfony/ai) diff --git a/src/mcp-sdk/composer.json b/src/mcp-sdk/composer.json deleted file mode 100644 index e515ae898..000000000 --- a/src/mcp-sdk/composer.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "name": "symfony/mcp-sdk", - "type": "library", - "description": "Model Context Protocol SDK for Client and Server applications in PHP", - "license": "MIT", - "authors": [ - { - "name": "Christopher Hertel", - "email": "mail@christopher-hertel.de" - }, - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com" - } - ], - "require": { - "php": "^8.2", - "psr/log": "^1.0 || ^2.0 || ^3.0", - "symfony/uid": "^6.4 || ^7.0" - }, - "require-dev": { - "phpstan/phpstan": "^2.1", - "phpunit/phpunit": "^11.5", - "symfony/console": "^6.4 || ^7.0", - "psr/cache": "^3.0" - }, - "suggest": { - "symfony/console": "To use SymfonyConsoleTransport for STDIO", - "psr/cache": "To use CachePoolStore with SSE Transport" - }, - "autoload": { - "psr-4": { - "Symfony\\AI\\McpSdk\\": "src/" - } - }, - "autoload-dev": { - "psr-4": { - "Symfony\\AI\\McpSdk\\Tests\\": "tests/", - "Symfony\\AI\\PHPStan\\": "../../.phpstan/" - } - } -} diff --git a/src/mcp-sdk/doc/index.rst b/src/mcp-sdk/doc/index.rst deleted file mode 100644 index 366066013..000000000 --- a/src/mcp-sdk/doc/index.rst +++ /dev/null @@ -1,158 +0,0 @@ -Model Context Protocol SDK -========================== - -Symfony AI MCP SDK is the low level library that enables communication between -a PHP application and an LLM model. - -Installation ------------- - -Install the SDK using Composer: - -.. code-block:: terminal - - $ composer require symfony/mcp-sdk - -Usage ------ - -The `Model Context Protocol`_ is built on top of JSON-RPC. There two types of -messages. A Notification and Request. The Notification is just a status update -that something has happened. There is never a response to a Notification. A Request -is a message that expects a response. There are 3 concepts/capabilities that you -may use. These are:: - -1. **Resources**: File-like data that can be read by clients (like API responses or file contents) -1. **Tools**: Functions that can be called by the LLM (with user approval) -1. **Prompts**: Pre-written templates that help users accomplish specific tasks - -The SDK comes with NotificationHandlers and RequestHandlers which are expected -to be wired up in your application. - -JsonRpcHandler -.............. - -The ``Symfony\AI\McpSdk\Server\JsonRpcHandler`` is the heart of the SDK. It is here -you inject the NotificationHandlers and RequestHandlers. It is recommended to use -the built-in handlers in ``Symfony\AI\McpSdk\Server\NotificationHandlers\*`` and -``Symfony\AI\McpSdk\Server\RequestHandlers\*``. - -The ``Symfony\AI\McpSdk\Server\JsonRpcHandler`` is started and kept running by -the ``Symfony\AI\McpSdk\Server`` - -Transports -.......... - -The SDK supports multiple transports for sending and receiving messages. The -``Symfony\AI\McpSdk\Server`` is using the transport to fetch a message, then -give it to the ``Symfony\AI\McpSdk\Server\JsonRpcHandler`` and finally send the -response/error back to the transport. The SDK comes with a few transports:: - -1. **Symfony Console Transport**: Good for testing and for CLI applications -1. **Stream Transport**: It uses Server Side Events (SSE) and HTTP streaming - -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 -**Tool** that can tell the current time, you need to provide some metadata to the -``Symfony\AI\McpSdk\Server\RequestHandler\ToolListHandler``:: - - namespace App; - - use Symfony\AI\McpSdk\Capability\Tool\MetadataInterface; - - class CurrentTimeToolMetadata implements MetadataInterface - { - public function getName(): string - { - return 'Current time'; - } - - public function getDescription(): string - { - return 'Returns the current time in UTC'; - } - - public function getInputSchema(): array - { - return [ - 'type' => 'object', - 'properties' => [ - 'format' => [ - 'type' => 'string', - 'description' => 'The format of the time, e.g. "Y-m-d H:i:s"', - 'default' => 'Y-m-d H:i:s', - ], - ], - 'required' => [], - ]; - } - } - -We would also need a class to actually execute the tool:: - - namespace App; - - use Symfony\AI\McpSdk\Capability\Tool\IdentifierInterface; - use Symfony\AI\McpSdk\Capability\Tool\ToolCall; - use Symfony\AI\McpSdk\Capability\Tool\ToolCallResult; - use Symfony\AI\McpSdk\Capability\Tool\ToolExecutorInterface; - - class CurrentTimeToolExecutor implements ToolExecutorInterface, IdentifierInterface - { - public function getName(): string - { - return 'Current time'; - } - - public function call(ToolCall $input): ToolCallResult - { - $format = $input->arguments['format'] ?? 'Y-m-d H:i:s'; - - return new ToolCallResult( - (new \DateTime('now', new \DateTimeZone('UTC')))->format($format) - ); - } - } - -If you have multiple tools, you can put them in a ToolChain:: - - $tools = new ToolChain([ - new CurrentTimeToolMetadata(), - new CurrentTimeToolExecutor(), - ]); - - $jsonRpcHandler = new Symfony\AI\McpSdk\Server\JsonRpcHandler( - new Symfony\AI\McpSdk\Message\Factory(), - [ - new ToolCallHandler($tools), - new ToolListHandler($tools), - // Other RequestHandlers ... - ], - [ - // Other NotificationHandlers ... - ], - new NullLogger() - ); - -With this metadata and executor, the client can now call the tool. - -Extending the SDK ------------------ - -If you want to extend the SDK, you can create your own RequestHandlers and NotificationHandlers. -The provided one are very good defaults for most applications but they are not -a requirement. - -If you do decide to use them, you get the benefit of having a well-defined interfaces -and value objects to work with. They will assure that you follow the `Model Context Protocol`_. -specification. - -You also have the Transport abstraction that allows you to create your own transport -if non of the standard ones fit your needs. - -.. _`Model Context Protocol`: https://modelcontextprotocol.io/ diff --git a/src/mcp-sdk/examples/cli/README.md b/src/mcp-sdk/examples/cli/README.md deleted file mode 100644 index 0512b6bbb..000000000 --- a/src/mcp-sdk/examples/cli/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# Example app with CLI - -This is just for testing and debugging purposes. - - -Install and create symlink with: - -```bash -cd /path/to/your/project/examples/cli -composer update -rm -rf vendor/php-llm/mcp-sdk/src -ln -s /path/to/your/project/src /path/to/your/project/examples/cli/vendor/php-llm/mcp-sdk/src -``` - -Run the CLI with: - -```bash -DEBUG=1 php index.php -``` - -You will see debug outputs to help you understand what is happening. - -In this terminal you can now test add some json strings. See `example-requests.json`. - -Run with Inspector: - -```bash -npx @modelcontextprotocol/inspector php index.php -``` diff --git a/src/mcp-sdk/examples/cli/composer.json b/src/mcp-sdk/examples/cli/composer.json deleted file mode 100644 index 5975efcb1..000000000 --- a/src/mcp-sdk/examples/cli/composer.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "php-llm/mcp-cli-example", - "description": "An example application for CLI", - "license": "MIT", - "type": "project", - "authors": [ - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com" - } - ], - "require": { - "php": ">=8.2", - "symfony/mcp-sdk": "@dev", - "symfony/console": "^7.2" - }, - "minimum-stability": "stable", - "autoload": { - "psr-4": { - "App\\": "src/" - } - } -} - diff --git a/src/mcp-sdk/examples/cli/example-requests.json b/src/mcp-sdk/examples/cli/example-requests.json deleted file mode 100644 index b2b72f880..000000000 --- a/src/mcp-sdk/examples/cli/example-requests.json +++ /dev/null @@ -1,12 +0,0 @@ -[ - {"jsonrpc": "2.0", "id": 1, "method": "resources/list", "params": []}, - {"jsonrpc": "2.0", "id": 2, "method": "resources/read", "params": {"uri": "file:///project/src/main.rs"}}, - - {"jsonrpc": "2.0", "id": 1, "method": "tools/list"}, - {"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "Current time"}}, - {"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "Current time","arguments": {"format": "Y-m-d"}}}, - - {"jsonrpc": "2.0", "id": 1, "method": "prompts/list"}, - {"jsonrpc": "2.0", "id": 2 ,"method": "prompts/get", "params": {"name": "Greet"}}, - {"jsonrpc": "2.0", "id": 2 ,"method": "prompts/get", "params": {"name": "Greet", "arguments": { "firstName": "Tobias" }}} -] \ No newline at end of file diff --git a/src/mcp-sdk/examples/cli/index.php b/src/mcp-sdk/examples/cli/index.php deleted file mode 100644 index 740b953e9..000000000 --- a/src/mcp-sdk/examples/cli/index.php +++ /dev/null @@ -1,39 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -require __DIR__.'/vendor/autoload.php'; - -use Symfony\Component\Console as SymfonyConsole; -use Symfony\Component\Console\Output\OutputInterface; - -$debug = (bool) ($_SERVER['DEBUG'] ?? false); - -// Setup input, output and logger -$input = new SymfonyConsole\Input\ArgvInput($argv); -$output = new SymfonyConsole\Output\ConsoleOutput($debug ? OutputInterface::VERBOSITY_VERY_VERBOSE : OutputInterface::VERBOSITY_NORMAL); -$logger = new SymfonyConsole\Logger\ConsoleLogger($output); - -// Configure the JsonRpcHandler and build the functionality -$jsonRpcHandler = new Symfony\AI\McpSdk\Server\JsonRpcHandler( - new Symfony\AI\McpSdk\Message\Factory(), - App\Builder::buildRequestHandlers(), - App\Builder::buildNotificationHandlers(), - $logger -); - -// Set up the server -$sever = new Symfony\AI\McpSdk\Server($jsonRpcHandler, $logger); - -// Create the transport layer using Symfony Console -$transport = new Symfony\AI\McpSdk\Server\Transport\Stdio\SymfonyConsoleTransport($input, $output); - -// Start our application -$sever->connect($transport); diff --git a/src/mcp-sdk/examples/cli/src/Builder.php b/src/mcp-sdk/examples/cli/src/Builder.php deleted file mode 100644 index c2fc9cee3..000000000 --- a/src/mcp-sdk/examples/cli/src/Builder.php +++ /dev/null @@ -1,69 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace App; - -use Symfony\AI\McpSdk\Capability\PromptChain; -use Symfony\AI\McpSdk\Capability\ResourceChain; -use Symfony\AI\McpSdk\Capability\ToolChain; -use Symfony\AI\McpSdk\Server\NotificationHandler\InitializedHandler; -use Symfony\AI\McpSdk\Server\NotificationHandlerInterface; -use Symfony\AI\McpSdk\Server\RequestHandler\InitializeHandler; -use Symfony\AI\McpSdk\Server\RequestHandler\PingHandler; -use Symfony\AI\McpSdk\Server\RequestHandler\PromptGetHandler; -use Symfony\AI\McpSdk\Server\RequestHandler\PromptListHandler; -use Symfony\AI\McpSdk\Server\RequestHandler\ResourceListHandler; -use Symfony\AI\McpSdk\Server\RequestHandler\ResourceReadHandler; -use Symfony\AI\McpSdk\Server\RequestHandler\ToolCallHandler; -use Symfony\AI\McpSdk\Server\RequestHandler\ToolListHandler; -use Symfony\AI\McpSdk\Server\RequestHandlerInterface; - -class Builder -{ - /** - * @return list - */ - public static function buildRequestHandlers(): array - { - $promptManager = new PromptChain([ - new ExamplePrompt(), - ]); - - $resourceManager = new ResourceChain([ - new ExampleResource(), - ]); - - $toolManager = new ToolChain([ - new ExampleTool(), - ]); - - return [ - new InitializeHandler(), - new PingHandler(), - new PromptListHandler($promptManager), - new PromptGetHandler($promptManager), - new ResourceListHandler($resourceManager), - new ResourceReadHandler($resourceManager), - new ToolCallHandler($toolManager), - new ToolListHandler($toolManager), - ]; - } - - /** - * @return list - */ - public static function buildNotificationHandlers(): array - { - return [ - new InitializedHandler(), - ]; - } -} diff --git a/src/mcp-sdk/examples/cli/src/ExamplePrompt.php b/src/mcp-sdk/examples/cli/src/ExamplePrompt.php deleted file mode 100644 index 919ff3359..000000000 --- a/src/mcp-sdk/examples/cli/src/ExamplePrompt.php +++ /dev/null @@ -1,55 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace App; - -use Symfony\AI\McpSdk\Capability\Prompt\MetadataInterface; -use Symfony\AI\McpSdk\Capability\Prompt\PromptGet; -use Symfony\AI\McpSdk\Capability\Prompt\PromptGetResult; -use Symfony\AI\McpSdk\Capability\Prompt\PromptGetResultMessages; -use Symfony\AI\McpSdk\Capability\Prompt\PromptGetterInterface; - -class ExamplePrompt implements MetadataInterface, PromptGetterInterface -{ - public function get(PromptGet $input): PromptGetResult - { - $firstName = $input->arguments['first name'] ?? null; - - return new PromptGetResult( - $this->getDescription(), - [new PromptGetResultMessages( - 'user', - \sprintf('Hello %s', $firstName ?? 'World') - )] - ); - } - - public function getName(): string - { - return 'Greet'; - } - - public function getDescription(): ?string - { - return 'Greet a person with a nice message'; - } - - public function getArguments(): array - { - return [ - [ - 'name' => 'first name', - 'description' => 'The name of the person to greet', - 'required' => false, - ], - ]; - } -} diff --git a/src/mcp-sdk/examples/cli/src/ExampleResource.php b/src/mcp-sdk/examples/cli/src/ExampleResource.php deleted file mode 100644 index 724cb9456..000000000 --- a/src/mcp-sdk/examples/cli/src/ExampleResource.php +++ /dev/null @@ -1,53 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace App; - -use Symfony\AI\McpSdk\Capability\Resource\MetadataInterface; -use Symfony\AI\McpSdk\Capability\Resource\ResourceRead; -use Symfony\AI\McpSdk\Capability\Resource\ResourceReaderInterface; -use Symfony\AI\McpSdk\Capability\Resource\ResourceReadResult; - -class ExampleResource implements MetadataInterface, ResourceReaderInterface -{ - public function read(ResourceRead $input): ResourceReadResult - { - return new ResourceReadResult( - 'Content of '.$this->getName(), - $this->getUri(), - ); - } - - public function getUri(): string - { - return 'file:///project/src/main.rs'; - } - - public function getName(): string - { - return 'My resource'; - } - - public function getDescription(): ?string - { - return 'This is just an example'; - } - - public function getMimeType(): ?string - { - return null; - } - - public function getSize(): ?int - { - return null; - } -} diff --git a/src/mcp-sdk/examples/cli/src/ExampleTool.php b/src/mcp-sdk/examples/cli/src/ExampleTool.php deleted file mode 100644 index 70097be1b..000000000 --- a/src/mcp-sdk/examples/cli/src/ExampleTool.php +++ /dev/null @@ -1,70 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace App; - -use Symfony\AI\McpSdk\Capability\Tool\MetadataInterface; -use Symfony\AI\McpSdk\Capability\Tool\ToolAnnotationsInterface; -use Symfony\AI\McpSdk\Capability\Tool\ToolCall; -use Symfony\AI\McpSdk\Capability\Tool\ToolCallResult; -use Symfony\AI\McpSdk\Capability\Tool\ToolExecutorInterface; - -class ExampleTool implements MetadataInterface, ToolExecutorInterface -{ - public function call(ToolCall $input): ToolCallResult - { - $format = $input->arguments['format'] ?? 'Y-m-d H:i:s'; - - return new ToolCallResult( - (new \DateTime('now', new \DateTimeZone('UTC')))->format($format) - ); - } - - public function getName(): string - { - return 'Current time'; - } - - public function getDescription(): string - { - return 'Returns the current time in UTC'; - } - - public function getInputSchema(): array - { - return [ - 'type' => 'object', - 'properties' => [ - 'format' => [ - 'type' => 'string', - 'description' => 'The format of the time, e.g. "Y-m-d H:i:s"', - 'default' => 'Y-m-d H:i:s', - ], - ], - 'required' => [], - ]; - } - - public function getOutputSchema(): ?array - { - return null; - } - - public function getTitle(): ?string - { - return null; - } - - public function getAnnotations(): ?ToolAnnotationsInterface - { - return null; - } -} diff --git a/src/mcp-sdk/phpstan.dist.neon b/src/mcp-sdk/phpstan.dist.neon deleted file mode 100644 index 4ac488675..000000000 --- a/src/mcp-sdk/phpstan.dist.neon +++ /dev/null @@ -1,14 +0,0 @@ -includes: - - ../../.phpstan/extension.neon - -parameters: - level: 6 - paths: - - examples/ - - src/ - - tests/ - excludePaths: - - examples/cli/vendor (?) - ignoreErrors: - - - message: "#^Method .*::test.*\\(\\) has no return type specified\\.$#" diff --git a/src/mcp-sdk/phpunit.xml.dist b/src/mcp-sdk/phpunit.xml.dist deleted file mode 100644 index 5354508bf..000000000 --- a/src/mcp-sdk/phpunit.xml.dist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - tests - - - - - - src - - - diff --git a/src/mcp-sdk/src/Capability/Prompt/CollectionInterface.php b/src/mcp-sdk/src/Capability/Prompt/CollectionInterface.php deleted file mode 100644 index 371bba882..000000000 --- a/src/mcp-sdk/src/Capability/Prompt/CollectionInterface.php +++ /dev/null @@ -1,26 +0,0 @@ - - * - * 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; - -use Symfony\AI\McpSdk\Exception\InvalidCursorException; - -interface CollectionInterface -{ - /** - * @param int $count the number of metadata items to return - * - * @return iterable - * - * @throws InvalidCursorException if no item with $lastIdentifier was found - */ - public function getMetadata(int $count, ?string $lastIdentifier = null): iterable; -} diff --git a/src/mcp-sdk/src/Capability/Prompt/IdentifierInterface.php b/src/mcp-sdk/src/Capability/Prompt/IdentifierInterface.php deleted file mode 100644 index 56a834195..000000000 --- a/src/mcp-sdk/src/Capability/Prompt/IdentifierInterface.php +++ /dev/null @@ -1,20 +0,0 @@ - - * - * 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; - -/** - * @author Tobias Nyholm - */ -interface IdentifierInterface -{ - public function getName(): string; -} diff --git a/src/mcp-sdk/src/Capability/Prompt/MetadataInterface.php b/src/mcp-sdk/src/Capability/Prompt/MetadataInterface.php deleted file mode 100644 index 1972bf7d8..000000000 --- a/src/mcp-sdk/src/Capability/Prompt/MetadataInterface.php +++ /dev/null @@ -1,26 +0,0 @@ - - * - * 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; - -interface MetadataInterface extends IdentifierInterface -{ - public function getDescription(): ?string; - - /** - * @return list - */ - public function getArguments(): array; -} diff --git a/src/mcp-sdk/src/Capability/Prompt/PromptGet.php b/src/mcp-sdk/src/Capability/Prompt/PromptGet.php deleted file mode 100644 index ba5bd8c1c..000000000 --- a/src/mcp-sdk/src/Capability/Prompt/PromptGet.php +++ /dev/null @@ -1,25 +0,0 @@ - - * - * 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; - -final readonly class PromptGet -{ - /** - * @param array $arguments - */ - public function __construct( - public string $id, - public string $name, - public array $arguments = [], - ) { - } -} diff --git a/src/mcp-sdk/src/Capability/Prompt/PromptGetResult.php b/src/mcp-sdk/src/Capability/Prompt/PromptGetResult.php deleted file mode 100644 index 1c46dd4eb..000000000 --- a/src/mcp-sdk/src/Capability/Prompt/PromptGetResult.php +++ /dev/null @@ -1,24 +0,0 @@ - - * - * 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; - -final readonly class PromptGetResult -{ - /** - * @param list $messages - */ - public function __construct( - public string $description, - public array $messages = [], - ) { - } -} diff --git a/src/mcp-sdk/src/Capability/Prompt/PromptGetResultMessages.php b/src/mcp-sdk/src/Capability/Prompt/PromptGetResultMessages.php deleted file mode 100644 index b763109b5..000000000 --- a/src/mcp-sdk/src/Capability/Prompt/PromptGetResultMessages.php +++ /dev/null @@ -1,27 +0,0 @@ - - * - * 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; - -final readonly class PromptGetResultMessages -{ - public function __construct( - public string $role, - public string $result, - /** - * @var "text"|"image"|"audio"|"resource"|non-empty-string - */ - public string $type = 'text', - public string $mimeType = 'text/plan', - public ?string $uri = null, - ) { - } -} diff --git a/src/mcp-sdk/src/Capability/Prompt/PromptGetterInterface.php b/src/mcp-sdk/src/Capability/Prompt/PromptGetterInterface.php deleted file mode 100644 index 34718e423..000000000 --- a/src/mcp-sdk/src/Capability/Prompt/PromptGetterInterface.php +++ /dev/null @@ -1,24 +0,0 @@ - - * - * 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; - -use Symfony\AI\McpSdk\Exception\PromptGetException; -use Symfony\AI\McpSdk\Exception\PromptNotFoundException; - -interface PromptGetterInterface -{ - /** - * @throws PromptGetException if the prompt execution fails - * @throws PromptNotFoundException if the prompt is not found - */ - public function get(PromptGet $input): PromptGetResult; -} diff --git a/src/mcp-sdk/src/Capability/PromptChain.php b/src/mcp-sdk/src/Capability/PromptChain.php deleted file mode 100644 index 095448cdc..000000000 --- a/src/mcp-sdk/src/Capability/PromptChain.php +++ /dev/null @@ -1,75 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Capability; - -use Symfony\AI\McpSdk\Capability\Prompt\CollectionInterface; -use Symfony\AI\McpSdk\Capability\Prompt\IdentifierInterface; -use Symfony\AI\McpSdk\Capability\Prompt\MetadataInterface; -use Symfony\AI\McpSdk\Capability\Prompt\PromptGet; -use Symfony\AI\McpSdk\Capability\Prompt\PromptGetResult; -use Symfony\AI\McpSdk\Capability\Prompt\PromptGetterInterface; -use Symfony\AI\McpSdk\Exception\InvalidCursorException; -use Symfony\AI\McpSdk\Exception\PromptGetException; -use Symfony\AI\McpSdk\Exception\PromptNotFoundException; - -/** - * A collection of prompts. All prompts need to implement IdentifierInterface. - */ -class PromptChain implements PromptGetterInterface, CollectionInterface -{ - public function __construct( - /** - * @var IdentifierInterface[] - */ - private readonly array $items, - ) { - } - - public function getMetadata(int $count, ?string $lastIdentifier = null): iterable - { - $found = null === $lastIdentifier; - foreach ($this->items as $item) { - if (!$item instanceof MetadataInterface) { - continue; - } - - if (false === $found) { - $found = $item->getName() === $lastIdentifier; - continue; - } - - yield $item; - if (--$count <= 0) { - break; - } - } - - if (!$found) { - throw new InvalidCursorException($lastIdentifier); - } - } - - public function get(PromptGet $input): PromptGetResult - { - foreach ($this->items as $item) { - if ($item instanceof PromptGetterInterface && $input->name === $item->getName()) { - try { - return $item->get($input); - } catch (\Throwable $e) { - throw new PromptGetException($input, $e); - } - } - } - - throw new PromptNotFoundException($input); - } -} diff --git a/src/mcp-sdk/src/Capability/Resource/CollectionInterface.php b/src/mcp-sdk/src/Capability/Resource/CollectionInterface.php deleted file mode 100644 index f566c8ed4..000000000 --- a/src/mcp-sdk/src/Capability/Resource/CollectionInterface.php +++ /dev/null @@ -1,29 +0,0 @@ - - * - * 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; - -use Symfony\AI\McpSdk\Exception\InvalidCursorException; - -/** - * @author Tobias Nyholm - */ -interface CollectionInterface -{ - /** - * @param int $count the number of metadata items to return - * - * @return iterable - * - * @throws InvalidCursorException if no item with $lastIdentifier was found - */ - public function getMetadata(int $count, ?string $lastIdentifier = null): iterable; -} diff --git a/src/mcp-sdk/src/Capability/Resource/IdentifierInterface.php b/src/mcp-sdk/src/Capability/Resource/IdentifierInterface.php deleted file mode 100644 index b0bc3851e..000000000 --- a/src/mcp-sdk/src/Capability/Resource/IdentifierInterface.php +++ /dev/null @@ -1,20 +0,0 @@ - - * - * 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; - -/** - * @author Tobias Nyholm - */ -interface IdentifierInterface -{ - public function getUri(): string; -} diff --git a/src/mcp-sdk/src/Capability/Resource/MetadataInterface.php b/src/mcp-sdk/src/Capability/Resource/MetadataInterface.php deleted file mode 100644 index b6e9a38f2..000000000 --- a/src/mcp-sdk/src/Capability/Resource/MetadataInterface.php +++ /dev/null @@ -1,29 +0,0 @@ - - * - * 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; - -/** - * @author Tobias Nyholm - */ -interface MetadataInterface extends IdentifierInterface -{ - public function getName(): string; - - public function getDescription(): ?string; - - public function getMimeType(): ?string; - - /** - * Size in bytes. - */ - public function getSize(): ?int; -} diff --git a/src/mcp-sdk/src/Capability/Resource/ResourceRead.php b/src/mcp-sdk/src/Capability/Resource/ResourceRead.php deleted file mode 100644 index 0e0e0d4ad..000000000 --- a/src/mcp-sdk/src/Capability/Resource/ResourceRead.php +++ /dev/null @@ -1,21 +0,0 @@ - - * - * 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; - -final readonly class ResourceRead -{ - public function __construct( - public string $id, - public string $uri, - ) { - } -} diff --git a/src/mcp-sdk/src/Capability/Resource/ResourceReadResult.php b/src/mcp-sdk/src/Capability/Resource/ResourceReadResult.php deleted file mode 100644 index 2d3aa4535..000000000 --- a/src/mcp-sdk/src/Capability/Resource/ResourceReadResult.php +++ /dev/null @@ -1,27 +0,0 @@ - - * - * 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; - -final readonly class ResourceReadResult -{ - public function __construct( - public string $result, - public string $uri, - - /** - * @var "text"|"blob" - */ - public string $type = 'text', - public string $mimeType = 'text/plain', - ) { - } -} diff --git a/src/mcp-sdk/src/Capability/Resource/ResourceReaderInterface.php b/src/mcp-sdk/src/Capability/Resource/ResourceReaderInterface.php deleted file mode 100644 index 4693ad4db..000000000 --- a/src/mcp-sdk/src/Capability/Resource/ResourceReaderInterface.php +++ /dev/null @@ -1,27 +0,0 @@ - - * - * 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; - -use Symfony\AI\McpSdk\Exception\ResourceNotFoundException; -use Symfony\AI\McpSdk\Exception\ResourceReadException; - -/** - * @author Tobias Nyholm - */ -interface ResourceReaderInterface -{ - /** - * @throws ResourceReadException if the resource execution fails - * @throws ResourceNotFoundException if the resource is not found - */ - public function read(ResourceRead $input): ResourceReadResult; -} diff --git a/src/mcp-sdk/src/Capability/ResourceChain.php b/src/mcp-sdk/src/Capability/ResourceChain.php deleted file mode 100644 index 209f869a6..000000000 --- a/src/mcp-sdk/src/Capability/ResourceChain.php +++ /dev/null @@ -1,75 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Capability; - -use Symfony\AI\McpSdk\Capability\Resource\CollectionInterface; -use Symfony\AI\McpSdk\Capability\Resource\IdentifierInterface; -use Symfony\AI\McpSdk\Capability\Resource\MetadataInterface; -use Symfony\AI\McpSdk\Capability\Resource\ResourceRead; -use Symfony\AI\McpSdk\Capability\Resource\ResourceReaderInterface; -use Symfony\AI\McpSdk\Capability\Resource\ResourceReadResult; -use Symfony\AI\McpSdk\Exception\InvalidCursorException; -use Symfony\AI\McpSdk\Exception\ResourceNotFoundException; -use Symfony\AI\McpSdk\Exception\ResourceReadException; - -/** - * A collection of resources. All resources need to implement IdentifierInterface. - */ -class ResourceChain implements CollectionInterface, ResourceReaderInterface -{ - public function __construct( - /** - * @var IdentifierInterface[] - */ - private readonly array $items, - ) { - } - - public function getMetadata(int $count, ?string $lastIdentifier = null): iterable - { - $found = null === $lastIdentifier; - foreach ($this->items as $item) { - if (!$item instanceof MetadataInterface) { - continue; - } - - if (false === $found) { - $found = $item->getUri() === $lastIdentifier; - continue; - } - - yield $item; - if (--$count <= 0) { - break; - } - } - - if (!$found) { - throw new InvalidCursorException($lastIdentifier); - } - } - - public function read(ResourceRead $input): ResourceReadResult - { - foreach ($this->items as $item) { - if ($item instanceof ResourceReaderInterface && $input->uri === $item->getUri()) { - try { - return $item->read($input); - } catch (\Throwable $e) { - throw new ResourceReadException($input, $e); - } - } - } - - throw new ResourceNotFoundException($input); - } -} diff --git a/src/mcp-sdk/src/Capability/Tool/CollectionInterface.php b/src/mcp-sdk/src/Capability/Tool/CollectionInterface.php deleted file mode 100644 index 588e77679..000000000 --- a/src/mcp-sdk/src/Capability/Tool/CollectionInterface.php +++ /dev/null @@ -1,26 +0,0 @@ - - * - * 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; - -use Symfony\AI\McpSdk\Exception\InvalidCursorException; - -interface CollectionInterface -{ - /** - * @param int|null $count the number of metadata items to return, null returns all items - * - * @return iterable - * - * @throws InvalidCursorException if no item with $lastIdentifier was found - */ - public function getMetadata(?int $count, ?string $lastIdentifier = null): iterable; -} diff --git a/src/mcp-sdk/src/Capability/Tool/IdentifierInterface.php b/src/mcp-sdk/src/Capability/Tool/IdentifierInterface.php deleted file mode 100644 index 4a45edcd1..000000000 --- a/src/mcp-sdk/src/Capability/Tool/IdentifierInterface.php +++ /dev/null @@ -1,20 +0,0 @@ - - * - * 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; - -interface IdentifierInterface -{ - /** - * @return string intended for programmatic or logical use, but used as a display name in past specs or fallback (if title isn’t present) - */ - public function getName(): string; -} diff --git a/src/mcp-sdk/src/Capability/Tool/MetadataInterface.php b/src/mcp-sdk/src/Capability/Tool/MetadataInterface.php deleted file mode 100644 index b09dc5948..000000000 --- a/src/mcp-sdk/src/Capability/Tool/MetadataInterface.php +++ /dev/null @@ -1,74 +0,0 @@ - - * - * 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; - -/** - * @see {https://modelcontextprotocol.io/specification/2025-06-18/schema#tool} - */ -interface MetadataInterface extends IdentifierInterface -{ - /** - * @return string|null A human-readable description of the tool. - * This can be used by clients to improve the LLM’s understanding of available tools. It can be thought of like a “hint” to the model - * - * @see https://modelcontextprotocol.io/specification/2025-06-18/schema#tool-description - */ - public function getDescription(): ?string; - - /** - * @return array{ - * type?: 'object', - * required?: list, - * properties?: array, - * } - * - * @see https://modelcontextprotocol.io/specification/2025-06-18/schema#tool-inputschema - */ - public function getInputSchema(): array; - - /** - * @return array{ - * type?: 'object', - * required?: list, - * properties?: array, - * }|null - * - * @see https://modelcontextprotocol.io/specification/2025-06-18/schema#tool-outputschema - */ - public function getOutputSchema(): ?array; - - /** - * @return string|null Intended for UI and end-user contexts — optimized to be human-readable and easily understood, even by those unfamiliar with domain-specific terminology. - * - * If not provided, the name should be used for display (except for Tool, where annotations.title should be given precedence over using name, if present). - * - * @see https://modelcontextprotocol.io/specification/2025-06-18/schema#tool-title - */ - public function getTitle(): ?string; - - /** - * @return ToolAnnotationsInterface|null Additional properties describing a Tool to clients. - * - * NOTE: all properties in ToolAnnotations are hints. They are not guaranteed to provide a faithful description of tool behavior (including descriptive properties like title). - * - * Clients should never make tool use decisions based on ToolAnnotations received from untrusted servers. - * - * @see https://modelcontextprotocol.io/specification/2025-06-18/schema#tool-annotations - */ - public function getAnnotations(): ?ToolAnnotationsInterface; -} diff --git a/src/mcp-sdk/src/Capability/Tool/ToolAnnotationsInterface.php b/src/mcp-sdk/src/Capability/Tool/ToolAnnotationsInterface.php deleted file mode 100644 index b1868d28a..000000000 --- a/src/mcp-sdk/src/Capability/Tool/ToolAnnotationsInterface.php +++ /dev/null @@ -1,62 +0,0 @@ - - * - * 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; - -interface ToolAnnotationsInterface -{ - /** - * @return bool|null If true, the tool may perform destructive updates to its environment. If false, the tool performs only additive updates. - * - * (This property is meaningful only when readOnlyHint == false) - * - * Default: true - * - * @see https://modelcontextprotocol.io/specification/2025-06-18/schema#toolannotations-destructivehint - */ - public function getDestructiveHint(): ?bool; - - /** - * @return bool|null If true, calling the tool repeatedly with the same arguments will have no additional effect on the its environment. - * - * (This property is meaningful only when readOnlyHint == false) - * - * Default: false - * - * @see https://modelcontextprotocol.io/specification/2025-06-18/schema#toolannotations-idempotenthint - */ - public function getIdempotentHint(): ?bool; - - /** - * @return bool|null If true, this tool may interact with an “open world” of external entities. If false, the tool’s domain of interaction is closed. For example, the world of a web search tool is open, whereas that of a memory tool is not. - * - * Default: true - * - * @see https://modelcontextprotocol.io/specification/2025-06-18/schema#toolannotations-openworldhint - */ - public function getOpenWorldHint(): ?bool; - - /** - * @return bool|null If true, the tool does not modify its environment. - * - * Default: false - * - * @see https://modelcontextprotocol.io/specification/2025-06-18/schema#toolannotations-readonlyhint - */ - public function getReadOnlyHint(): ?bool; - - /** - * @return string|null A human-readable title for the tool - * - * @see https://modelcontextprotocol.io/specification/2025-06-18/schema#toolannotations-title - */ - public function getTitle(): ?string; -} diff --git a/src/mcp-sdk/src/Capability/Tool/ToolCall.php b/src/mcp-sdk/src/Capability/Tool/ToolCall.php deleted file mode 100644 index 9208fc171..000000000 --- a/src/mcp-sdk/src/Capability/Tool/ToolCall.php +++ /dev/null @@ -1,25 +0,0 @@ - - * - * 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; - -final readonly class ToolCall -{ - /** - * @param array $arguments - */ - public function __construct( - public string $id, - public string $name, - public array $arguments = [], - ) { - } -} diff --git a/src/mcp-sdk/src/Capability/Tool/ToolCallResult.php b/src/mcp-sdk/src/Capability/Tool/ToolCallResult.php deleted file mode 100644 index 2bdd2d0df..000000000 --- a/src/mcp-sdk/src/Capability/Tool/ToolCallResult.php +++ /dev/null @@ -1,27 +0,0 @@ - - * - * 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; - -final readonly class ToolCallResult -{ - public function __construct( - public string $result, - /** - * @var "text"|"image"|"audio"|"resource"|non-empty-string - */ - public string $type = 'text', - public string $mimeType = 'text/plan', - public bool $isError = false, - public ?string $uri = null, - ) { - } -} diff --git a/src/mcp-sdk/src/Capability/Tool/ToolCollectionInterface.php b/src/mcp-sdk/src/Capability/Tool/ToolCollectionInterface.php deleted file mode 100644 index 19b44928d..000000000 --- a/src/mcp-sdk/src/Capability/Tool/ToolCollectionInterface.php +++ /dev/null @@ -1,20 +0,0 @@ - - * - * 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; - -interface ToolCollectionInterface -{ - /** - * @return MetadataInterface[] - */ - public function getMetadata(): array; -} diff --git a/src/mcp-sdk/src/Capability/Tool/ToolExecutorInterface.php b/src/mcp-sdk/src/Capability/Tool/ToolExecutorInterface.php deleted file mode 100644 index 5e0306620..000000000 --- a/src/mcp-sdk/src/Capability/Tool/ToolExecutorInterface.php +++ /dev/null @@ -1,24 +0,0 @@ - - * - * 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; - -use Symfony\AI\McpSdk\Exception\ToolExecutionException; -use Symfony\AI\McpSdk\Exception\ToolNotFoundException; - -interface ToolExecutorInterface -{ - /** - * @throws ToolExecutionException if the tool execution fails - * @throws ToolNotFoundException if the tool is not found - */ - public function call(ToolCall $input): ToolCallResult; -} diff --git a/src/mcp-sdk/src/Capability/ToolChain.php b/src/mcp-sdk/src/Capability/ToolChain.php deleted file mode 100644 index 0462b6d8d..000000000 --- a/src/mcp-sdk/src/Capability/ToolChain.php +++ /dev/null @@ -1,77 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Capability; - -use Symfony\AI\McpSdk\Capability\Tool\CollectionInterface; -use Symfony\AI\McpSdk\Capability\Tool\IdentifierInterface; -use Symfony\AI\McpSdk\Capability\Tool\MetadataInterface; -use Symfony\AI\McpSdk\Capability\Tool\ToolCall; -use Symfony\AI\McpSdk\Capability\Tool\ToolCallResult; -use Symfony\AI\McpSdk\Capability\Tool\ToolExecutorInterface; -use Symfony\AI\McpSdk\Exception\InvalidCursorException; -use Symfony\AI\McpSdk\Exception\ToolExecutionException; -use Symfony\AI\McpSdk\Exception\ToolNotFoundException; - -/** - * A collection of tools. All tools need to implement IdentifierInterface. - * - * @author Tobias Nyholm - */ -class ToolChain implements ToolExecutorInterface, CollectionInterface -{ - public function __construct( - /** - * @var IdentifierInterface[] $items - */ - private readonly iterable $items, - ) { - } - - public function getMetadata(?int $count, ?string $lastIdentifier = null): iterable - { - $found = null === $lastIdentifier; - foreach ($this->items as $item) { - if (!$item instanceof MetadataInterface) { - continue; - } - - if (false === $found) { - $found = $item->getName() === $lastIdentifier; - continue; - } - - yield $item; - if (null !== $count && 0 >= --$count) { - break; - } - } - - if (!$found) { - throw new InvalidCursorException($lastIdentifier); - } - } - - public function call(ToolCall $input): ToolCallResult - { - foreach ($this->items as $item) { - if ($item instanceof ToolExecutorInterface && $input->name === $item->getName()) { - try { - return $item->call($input); - } catch (\Throwable $e) { - throw new ToolExecutionException($input, $e); - } - } - } - - throw new ToolNotFoundException($input); - } -} diff --git a/src/mcp-sdk/src/Exception/ExceptionInterface.php b/src/mcp-sdk/src/Exception/ExceptionInterface.php deleted file mode 100644 index 191a94e1e..000000000 --- a/src/mcp-sdk/src/Exception/ExceptionInterface.php +++ /dev/null @@ -1,16 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Exception; - -interface ExceptionInterface extends \Throwable -{ -} diff --git a/src/mcp-sdk/src/Exception/HandlerNotFoundException.php b/src/mcp-sdk/src/Exception/HandlerNotFoundException.php deleted file mode 100644 index baa389190..000000000 --- a/src/mcp-sdk/src/Exception/HandlerNotFoundException.php +++ /dev/null @@ -1,16 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Exception; - -class HandlerNotFoundException extends \InvalidArgumentException implements NotFoundExceptionInterface -{ -} diff --git a/src/mcp-sdk/src/Exception/InvalidArgumentException.php b/src/mcp-sdk/src/Exception/InvalidArgumentException.php deleted file mode 100644 index b9e9ad4f0..000000000 --- a/src/mcp-sdk/src/Exception/InvalidArgumentException.php +++ /dev/null @@ -1,19 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Exception; - -/** - * @author Christopher Hertel - */ -class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface -{ -} diff --git a/src/mcp-sdk/src/Exception/InvalidCursorException.php b/src/mcp-sdk/src/Exception/InvalidCursorException.php deleted file mode 100644 index 7cea23d09..000000000 --- a/src/mcp-sdk/src/Exception/InvalidCursorException.php +++ /dev/null @@ -1,21 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Exception; - -final class InvalidCursorException extends \InvalidArgumentException implements ExceptionInterface -{ - public function __construct( - public readonly string $cursor, - ) { - parent::__construct(\sprintf('Invalid value for pagination parameter "cursor": "%s"', $cursor)); - } -} diff --git a/src/mcp-sdk/src/Exception/InvalidInputMessageException.php b/src/mcp-sdk/src/Exception/InvalidInputMessageException.php deleted file mode 100644 index 9a923b47e..000000000 --- a/src/mcp-sdk/src/Exception/InvalidInputMessageException.php +++ /dev/null @@ -1,16 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Exception; - -class InvalidInputMessageException extends \InvalidArgumentException implements ExceptionInterface -{ -} diff --git a/src/mcp-sdk/src/Exception/NotFoundExceptionInterface.php b/src/mcp-sdk/src/Exception/NotFoundExceptionInterface.php deleted file mode 100644 index c07c25ed2..000000000 --- a/src/mcp-sdk/src/Exception/NotFoundExceptionInterface.php +++ /dev/null @@ -1,16 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Exception; - -interface NotFoundExceptionInterface extends ExceptionInterface -{ -} diff --git a/src/mcp-sdk/src/Exception/PromptGetException.php b/src/mcp-sdk/src/Exception/PromptGetException.php deleted file mode 100644 index b9901f6c2..000000000 --- a/src/mcp-sdk/src/Exception/PromptGetException.php +++ /dev/null @@ -1,24 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Exception; - -use Symfony\AI\McpSdk\Capability\Prompt\PromptGet; - -final class PromptGetException extends \RuntimeException implements ExceptionInterface -{ - public function __construct( - public readonly PromptGet $promptGet, - ?\Throwable $previous = null, - ) { - parent::__construct(\sprintf('Handling prompt "%s" failed with error: %s', $promptGet->name, $previous->getMessage()), previous: $previous); - } -} diff --git a/src/mcp-sdk/src/Exception/PromptNotFoundException.php b/src/mcp-sdk/src/Exception/PromptNotFoundException.php deleted file mode 100644 index 6c485bf67..000000000 --- a/src/mcp-sdk/src/Exception/PromptNotFoundException.php +++ /dev/null @@ -1,23 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Exception; - -use Symfony\AI\McpSdk\Capability\Prompt\PromptGet; - -final class PromptNotFoundException extends \RuntimeException implements NotFoundExceptionInterface -{ - public function __construct( - public readonly PromptGet $promptGet, - ) { - parent::__construct(\sprintf('Prompt not found for name: "%s"', $promptGet->name)); - } -} diff --git a/src/mcp-sdk/src/Exception/ResourceNotFoundException.php b/src/mcp-sdk/src/Exception/ResourceNotFoundException.php deleted file mode 100644 index ca88ac72f..000000000 --- a/src/mcp-sdk/src/Exception/ResourceNotFoundException.php +++ /dev/null @@ -1,23 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Exception; - -use Symfony\AI\McpSdk\Capability\Resource\ResourceRead; - -final class ResourceNotFoundException extends \RuntimeException implements NotFoundExceptionInterface -{ - public function __construct( - public readonly ResourceRead $readRequest, - ) { - parent::__construct(\sprintf('Resource not found for uri: "%s"', $readRequest->uri)); - } -} diff --git a/src/mcp-sdk/src/Exception/ResourceReadException.php b/src/mcp-sdk/src/Exception/ResourceReadException.php deleted file mode 100644 index e063fc1bf..000000000 --- a/src/mcp-sdk/src/Exception/ResourceReadException.php +++ /dev/null @@ -1,24 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Exception; - -use Symfony\AI\McpSdk\Capability\Resource\ResourceRead; - -final class ResourceReadException extends \RuntimeException implements ExceptionInterface -{ - public function __construct( - public readonly ResourceRead $readRequest, - ?\Throwable $previous = null, - ) { - parent::__construct(\sprintf('Reading resource "%s" failed with error: %s', $readRequest->uri, $previous?->getMessage() ?? ''), previous: $previous); - } -} diff --git a/src/mcp-sdk/src/Exception/ToolExecutionException.php b/src/mcp-sdk/src/Exception/ToolExecutionException.php deleted file mode 100644 index 704c7cef8..000000000 --- a/src/mcp-sdk/src/Exception/ToolExecutionException.php +++ /dev/null @@ -1,24 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Exception; - -use Symfony\AI\McpSdk\Capability\Tool\ToolCall; - -final class ToolExecutionException extends \RuntimeException implements ExceptionInterface -{ - public function __construct( - public readonly ToolCall $toolCall, - ?\Throwable $previous = null, - ) { - parent::__construct(\sprintf('Execution of tool "%s" failed with error: %s', $toolCall->name, $previous?->getMessage() ?? ''), previous: $previous); - } -} diff --git a/src/mcp-sdk/src/Exception/ToolNotFoundException.php b/src/mcp-sdk/src/Exception/ToolNotFoundException.php deleted file mode 100644 index 614015627..000000000 --- a/src/mcp-sdk/src/Exception/ToolNotFoundException.php +++ /dev/null @@ -1,23 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Exception; - -use Symfony\AI\McpSdk\Capability\Tool\ToolCall; - -final class ToolNotFoundException extends \RuntimeException implements NotFoundExceptionInterface -{ - public function __construct( - public readonly ToolCall $toolCall, - ) { - parent::__construct(\sprintf('Tool not found for call: "%s"', $toolCall->name)); - } -} diff --git a/src/mcp-sdk/src/Message/Error.php b/src/mcp-sdk/src/Message/Error.php deleted file mode 100644 index df5ad11d2..000000000 --- a/src/mcp-sdk/src/Message/Error.php +++ /dev/null @@ -1,73 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Message; - -final readonly class Error implements \JsonSerializable -{ - public const INVALID_REQUEST = -32600; - public const METHOD_NOT_FOUND = -32601; - public const INVALID_PARAMS = -32602; - public const INTERNAL_ERROR = -32603; - public const PARSE_ERROR = -32700; - public const RESOURCE_NOT_FOUND = -32002; - - public function __construct( - public string|int $id, - public int $code, - public string $message, - ) { - } - - public static function invalidRequest(string|int $id, string $message = 'Invalid Request'): self - { - return new self($id, self::INVALID_REQUEST, $message); - } - - public static function methodNotFound(string|int $id, string $message = 'Method not found'): self - { - return new self($id, self::METHOD_NOT_FOUND, $message); - } - - public static function invalidParams(string|int $id, string $message = 'Invalid params'): self - { - return new self($id, self::INVALID_PARAMS, $message); - } - - public static function internalError(string|int $id, string $message = 'Internal error'): self - { - return new self($id, self::INTERNAL_ERROR, $message); - } - - public static function parseError(string|int $id, string $message = 'Parse error'): self - { - return new self($id, self::PARSE_ERROR, $message); - } - - /** - * @return array{ - * jsonrpc: string, - * id: string|int, - * error: array{code: int, message: string} - * } - */ - public function jsonSerialize(): array - { - return [ - 'jsonrpc' => '2.0', - 'id' => $this->id, - 'error' => [ - 'code' => $this->code, - 'message' => $this->message, - ], - ]; - } -} diff --git a/src/mcp-sdk/src/Message/Factory.php b/src/mcp-sdk/src/Message/Factory.php deleted file mode 100644 index 6338d5d69..000000000 --- a/src/mcp-sdk/src/Message/Factory.php +++ /dev/null @@ -1,44 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Message; - -use Symfony\AI\McpSdk\Exception\InvalidInputMessageException; - -/** - * @author Christopher Hertel - */ -final class Factory -{ - /** - * @return iterable - * - * @throws \JsonException When the input string is not valid JSON - */ - public function create(string $input): iterable - { - $data = json_decode($input, true, flags: \JSON_THROW_ON_ERROR); - - if ('{' === $input[0]) { - $data = [$data]; - } - - foreach ($data as $message) { - if (!isset($message['method'])) { - yield new InvalidInputMessageException('Invalid JSON-RPC request, missing "method".'); - } elseif (str_starts_with((string) $message['method'], 'notifications/')) { - yield Notification::from($message); - } else { - yield Request::from($message); - } - } - } -} diff --git a/src/mcp-sdk/src/Message/Notification.php b/src/mcp-sdk/src/Message/Notification.php deleted file mode 100644 index 5da77b6a1..000000000 --- a/src/mcp-sdk/src/Message/Notification.php +++ /dev/null @@ -1,52 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Message; - -final readonly class Notification implements \JsonSerializable, \Stringable -{ - /** - * @param array|null $params - */ - public function __construct( - public string $method, - public ?array $params = null, - ) { - } - - public function __toString(): string - { - return \sprintf('%s', $this->method); - } - - /** - * @param array{method: string, params?: array} $data - */ - public static function from(array $data): self - { - return new self( - $data['method'], - $data['params'] ?? null, - ); - } - - /** - * @return array{jsonrpc: string, method: string, params: array|null} - */ - public function jsonSerialize(): array - { - return [ - 'jsonrpc' => '2.0', - 'method' => $this->method, - 'params' => $this->params, - ]; - } -} diff --git a/src/mcp-sdk/src/Message/Request.php b/src/mcp-sdk/src/Message/Request.php deleted file mode 100644 index 8ec35cb99..000000000 --- a/src/mcp-sdk/src/Message/Request.php +++ /dev/null @@ -1,55 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Message; - -final readonly class Request implements \JsonSerializable, \Stringable -{ - /** - * @param array|null $params - */ - public function __construct( - public int|string $id, - public string $method, - public ?array $params = null, - ) { - } - - public function __toString(): string - { - return \sprintf('%s: %s', $this->id, $this->method); - } - - /** - * @param array{id: string|int, method: string, params?: array} $data - */ - public static function from(array $data): self - { - return new self( - $data['id'], - $data['method'], - $data['params'] ?? null, - ); - } - - /** - * @return array{jsonrpc: string, id: string|int, method: string, params: array|null} - */ - public function jsonSerialize(): array - { - return [ - 'jsonrpc' => '2.0', - 'id' => $this->id, - 'method' => $this->method, - 'params' => $this->params, - ]; - } -} diff --git a/src/mcp-sdk/src/Message/Response.php b/src/mcp-sdk/src/Message/Response.php deleted file mode 100644 index 2b26d9d2c..000000000 --- a/src/mcp-sdk/src/Message/Response.php +++ /dev/null @@ -1,36 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Message; - -final readonly class Response implements \JsonSerializable -{ - /** - * @param array $result - */ - public function __construct( - public string|int $id, - public array $result = [], - ) { - } - - /** - * @return array{jsonrpc: string, id: string|int, result: array} - */ - public function jsonSerialize(): array - { - return [ - 'jsonrpc' => '2.0', - 'id' => $this->id, - 'result' => $this->result, - ]; - } -} diff --git a/src/mcp-sdk/src/Server.php b/src/mcp-sdk/src/Server.php deleted file mode 100644 index 1b5629b41..000000000 --- a/src/mcp-sdk/src/Server.php +++ /dev/null @@ -1,61 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk; - -use Psr\Log\LoggerInterface; -use Psr\Log\NullLogger; -use Symfony\AI\McpSdk\Server\JsonRpcHandler; -use Symfony\AI\McpSdk\Server\TransportInterface; - -final readonly class Server -{ - public function __construct( - private JsonRpcHandler $jsonRpcHandler, - private LoggerInterface $logger = new NullLogger(), - ) { - } - - public function connect(TransportInterface $transport): void - { - $transport->initialize(); - $this->logger->info('Transport initialized'); - - while ($transport->isConnected()) { - foreach ($transport->receive() as $message) { - if (null === $message) { - continue; - } - - try { - foreach ($this->jsonRpcHandler->process($message) as $response) { - if (null === $response) { - continue; - } - - $transport->send($response); - } - } catch (\JsonException $e) { - $this->logger->error('Failed to encode response to JSON', [ - 'message' => $message, - 'exception' => $e, - ]); - continue; - } - } - - usleep(1000); - } - - $transport->close(); - $this->logger->info('Transport closed'); - } -} diff --git a/src/mcp-sdk/src/Server/JsonRpcHandler.php b/src/mcp-sdk/src/Server/JsonRpcHandler.php deleted file mode 100644 index ed925fdf1..000000000 --- a/src/mcp-sdk/src/Server/JsonRpcHandler.php +++ /dev/null @@ -1,160 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Server; - -use Psr\Log\LoggerInterface; -use Psr\Log\NullLogger; -use Symfony\AI\McpSdk\Exception\ExceptionInterface; -use Symfony\AI\McpSdk\Exception\HandlerNotFoundException; -use Symfony\AI\McpSdk\Exception\InvalidInputMessageException; -use Symfony\AI\McpSdk\Exception\NotFoundExceptionInterface; -use Symfony\AI\McpSdk\Message\Error; -use Symfony\AI\McpSdk\Message\Factory; -use Symfony\AI\McpSdk\Message\Notification; -use Symfony\AI\McpSdk\Message\Request; -use Symfony\AI\McpSdk\Message\Response; - -/** - * @final - */ -readonly class JsonRpcHandler -{ - /** - * @var array - */ - private array $requestHandlers; - - /** - * @var array - */ - private array $notificationHandlers; - - /** - * @param iterable $requestHandlers - * @param iterable $notificationHandlers - */ - public function __construct( - private Factory $messageFactory, - iterable $requestHandlers, - iterable $notificationHandlers, - private LoggerInterface $logger = new NullLogger(), - ) { - $this->requestHandlers = $requestHandlers instanceof \Traversable ? iterator_to_array($requestHandlers) : $requestHandlers; - $this->notificationHandlers = $notificationHandlers instanceof \Traversable ? iterator_to_array($notificationHandlers) : $notificationHandlers; - } - - /** - * @return iterable - * - * @throws ExceptionInterface When a handler throws an exception during message processing - * @throws \JsonException When JSON encoding of the response fails - */ - public function process(string $input): iterable - { - $this->logger->info('Received message to process', ['message' => $input]); - - try { - $messages = $this->messageFactory->create($input); - } catch (\JsonException $e) { - $this->logger->warning('Failed to decode json message', ['exception' => $e]); - - yield $this->encodeResponse(Error::parseError($e->getMessage())); - - return; - } - - foreach ($messages as $message) { - if ($message instanceof InvalidInputMessageException) { - $this->logger->warning('Failed to create message', ['exception' => $message]); - yield $this->encodeResponse(Error::invalidRequest(0, $message->getMessage())); - continue; - } - - $this->logger->info('Decoded incoming message', ['message' => $message]); - - try { - yield $message instanceof Notification - ? $this->handleNotification($message) - : $this->encodeResponse($this->handleRequest($message)); - } catch (\DomainException) { - yield null; - } catch (NotFoundExceptionInterface $e) { - $this->logger->warning(\sprintf('Failed to create response: %s', $e->getMessage()), ['exception' => $e]); - - yield $this->encodeResponse(Error::methodNotFound($message->id, $e->getMessage())); - } catch (\InvalidArgumentException $e) { - $this->logger->warning(\sprintf('Invalid argument: %s', $e->getMessage()), ['exception' => $e]); - - yield $this->encodeResponse(Error::invalidParams($message->id, $e->getMessage())); - } catch (\Throwable $e) { - $this->logger->critical(\sprintf('Uncaught exception: %s', $e->getMessage()), ['exception' => $e]); - - yield $this->encodeResponse(Error::internalError($message->id, $e->getMessage())); - } - } - } - - /** - * @throws \JsonException When JSON encoding fails - */ - private function encodeResponse(Response|Error|null $response): ?string - { - if (null === $response) { - $this->logger->warning('Response is null'); - - return null; - } - - $this->logger->info('Encoding response', ['response' => $response]); - - if ($response instanceof Response && [] === $response->result) { - return json_encode($response, \JSON_THROW_ON_ERROR | \JSON_FORCE_OBJECT); - } - - return json_encode($response, \JSON_THROW_ON_ERROR); - } - - /** - * @throws ExceptionInterface When a notification handler throws an exception - */ - private function handleNotification(Notification $notification): null - { - $handled = false; - foreach ($this->notificationHandlers as $handler) { - if ($handler->supports($notification)) { - $handler->handle($notification); - $handled = true; - } - } - - if (!$handled) { - $this->logger->warning(\sprintf('No handler found for "%s".', $notification->method), ['notification' => $notification]); - } - - return null; - } - - /** - * @throws NotFoundExceptionInterface When no handler is found for the request method - * @throws ExceptionInterface When a request handler throws an exception - */ - private function handleRequest(Request $request): Response|Error - { - foreach ($this->requestHandlers as $handler) { - if ($handler->supports($request)) { - return $handler->createResponse($request); - } - } - - throw new HandlerNotFoundException(\sprintf('No handler found for method "%s".', $request->method)); - } -} diff --git a/src/mcp-sdk/src/Server/NotificationHandler/BaseNotificationHandler.php b/src/mcp-sdk/src/Server/NotificationHandler/BaseNotificationHandler.php deleted file mode 100644 index 4f3d94a78..000000000 --- a/src/mcp-sdk/src/Server/NotificationHandler/BaseNotificationHandler.php +++ /dev/null @@ -1,28 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Server\NotificationHandler; - -use Symfony\AI\McpSdk\Message\Notification; -use Symfony\AI\McpSdk\Server\NotificationHandlerInterface; - -/** - * @author Christopher Hertel - */ -abstract class BaseNotificationHandler implements NotificationHandlerInterface -{ - public function supports(Notification $message): bool - { - return $message->method === \sprintf('notifications/%s', $this->supportedNotification()); - } - - abstract protected function supportedNotification(): string; -} diff --git a/src/mcp-sdk/src/Server/NotificationHandler/InitializedHandler.php b/src/mcp-sdk/src/Server/NotificationHandler/InitializedHandler.php deleted file mode 100644 index 436bb9273..000000000 --- a/src/mcp-sdk/src/Server/NotificationHandler/InitializedHandler.php +++ /dev/null @@ -1,29 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Server\NotificationHandler; - -use Symfony\AI\McpSdk\Message\Notification; - -/** - * @author Christopher Hertel - */ -final class InitializedHandler extends BaseNotificationHandler -{ - public function handle(Notification $notification): void - { - } - - protected function supportedNotification(): string - { - return 'initialized'; - } -} diff --git a/src/mcp-sdk/src/Server/NotificationHandlerInterface.php b/src/mcp-sdk/src/Server/NotificationHandlerInterface.php deleted file mode 100644 index 1388edb6f..000000000 --- a/src/mcp-sdk/src/Server/NotificationHandlerInterface.php +++ /dev/null @@ -1,28 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Server; - -use Symfony\AI\McpSdk\Exception\ExceptionInterface; -use Symfony\AI\McpSdk\Message\Notification; - -/** - * @author Christopher Hertel - */ -interface NotificationHandlerInterface -{ - public function supports(Notification $message): bool; - - /** - * @throws ExceptionInterface When the handler encounters an error processing the notification - */ - public function handle(Notification $notification): void; -} diff --git a/src/mcp-sdk/src/Server/RequestHandler/BaseRequestHandler.php b/src/mcp-sdk/src/Server/RequestHandler/BaseRequestHandler.php deleted file mode 100644 index 307224c49..000000000 --- a/src/mcp-sdk/src/Server/RequestHandler/BaseRequestHandler.php +++ /dev/null @@ -1,28 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Server\RequestHandler; - -use Symfony\AI\McpSdk\Message\Request; -use Symfony\AI\McpSdk\Server\RequestHandlerInterface; - -/** - * @author Christopher Hertel - */ -abstract class BaseRequestHandler implements RequestHandlerInterface -{ - public function supports(Request $message): bool - { - return $message->method === $this->supportedMethod(); - } - - abstract protected function supportedMethod(): string; -} diff --git a/src/mcp-sdk/src/Server/RequestHandler/InitializeHandler.php b/src/mcp-sdk/src/Server/RequestHandler/InitializeHandler.php deleted file mode 100644 index e6f81fa69..000000000 --- a/src/mcp-sdk/src/Server/RequestHandler/InitializeHandler.php +++ /dev/null @@ -1,45 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Server\RequestHandler; - -use Symfony\AI\McpSdk\Message\Request; -use Symfony\AI\McpSdk\Message\Response; - -/** - * @author Christopher Hertel - */ -final class InitializeHandler extends BaseRequestHandler -{ - public function __construct( - private readonly string $name = 'app', - private readonly string $version = 'dev', - ) { - } - - 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], - ]); - } - - protected function supportedMethod(): string - { - return 'initialize'; - } -} diff --git a/src/mcp-sdk/src/Server/RequestHandler/PingHandler.php b/src/mcp-sdk/src/Server/RequestHandler/PingHandler.php deleted file mode 100644 index ab8bda66c..000000000 --- a/src/mcp-sdk/src/Server/RequestHandler/PingHandler.php +++ /dev/null @@ -1,31 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Server\RequestHandler; - -use Symfony\AI\McpSdk\Message\Request; -use Symfony\AI\McpSdk\Message\Response; - -/** - * @author Christopher Hertel - */ -final class PingHandler extends BaseRequestHandler -{ - public function createResponse(Request $message): Response - { - return new Response($message->id, []); - } - - protected function supportedMethod(): string - { - return 'ping'; - } -} diff --git a/src/mcp-sdk/src/Server/RequestHandler/PromptGetHandler.php b/src/mcp-sdk/src/Server/RequestHandler/PromptGetHandler.php deleted file mode 100644 index 17c1a4c3f..000000000 --- a/src/mcp-sdk/src/Server/RequestHandler/PromptGetHandler.php +++ /dev/null @@ -1,83 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Server\RequestHandler; - -use Symfony\AI\McpSdk\Capability\Prompt\PromptGet; -use Symfony\AI\McpSdk\Capability\Prompt\PromptGetterInterface; -use Symfony\AI\McpSdk\Exception\ExceptionInterface; -use Symfony\AI\McpSdk\Exception\InvalidArgumentException; -use Symfony\AI\McpSdk\Message\Error; -use Symfony\AI\McpSdk\Message\Request; -use Symfony\AI\McpSdk\Message\Response; - -/** - * @author Tobias Nyholm - */ -final class PromptGetHandler extends BaseRequestHandler -{ - public function __construct( - private readonly PromptGetterInterface $getter, - ) { - } - - public function createResponse(Request $message): Response|Error - { - $name = $message->params['name']; - $arguments = $message->params['arguments'] ?? []; - - try { - $result = $this->getter->get(new PromptGet(uniqid('', true), $name, $arguments)); - } catch (ExceptionInterface) { - return Error::internalError($message->id, 'Error while handling prompt'); - } - - $messages = []; - foreach ($result->messages as $resultMessage) { - $content = match ($resultMessage->type) { - 'text' => [ - 'type' => 'text', - 'text' => $resultMessage->result, - ], - 'image', 'audio' => [ - 'type' => $resultMessage->type, - 'data' => $resultMessage->result, - 'mimeType' => $resultMessage->mimeType, - ], - 'resource' => [ - 'type' => 'resource', - 'resource' => [ - 'uri' => $resultMessage->uri, - 'mimeType' => $resultMessage->mimeType, - 'text' => $resultMessage->result, - ], - ], - // TODO better exception - default => throw new InvalidArgumentException(\sprintf('Unsupported PromptGet result type: %s', $resultMessage->type)), - }; - - $messages[] = [ - 'role' => $resultMessage->role, - 'content' => $content, - ]; - } - - return new Response($message->id, [ - 'description' => $result->description, - 'messages' => $messages, - ]); - } - - protected function supportedMethod(): string - { - return 'prompts/get'; - } -} diff --git a/src/mcp-sdk/src/Server/RequestHandler/PromptListHandler.php b/src/mcp-sdk/src/Server/RequestHandler/PromptListHandler.php deleted file mode 100644 index 1ee7c44d2..000000000 --- a/src/mcp-sdk/src/Server/RequestHandler/PromptListHandler.php +++ /dev/null @@ -1,85 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Server\RequestHandler; - -use Symfony\AI\McpSdk\Capability\Prompt\CollectionInterface; -use Symfony\AI\McpSdk\Message\Request; -use Symfony\AI\McpSdk\Message\Response; - -/** - * @author Tobias Nyholm - */ -final class PromptListHandler extends BaseRequestHandler -{ - public function __construct( - private readonly CollectionInterface $collection, - private readonly int $pageSize = 20, - ) { - } - - public function createResponse(Request $message): Response - { - $nextCursor = null; - $prompts = []; - - $metadataList = $this->collection->getMetadata( - $this->pageSize, - $message->params['cursor'] ?? null - ); - - foreach ($metadataList as $metadata) { - $nextCursor = $metadata->getName(); - $result = [ - 'name' => $metadata->getName(), - ]; - - $description = $metadata->getDescription(); - if (null !== $description) { - $result['description'] = $description; - } - - $arguments = []; - foreach ($metadata->getArguments() as $data) { - $argument = [ - 'name' => $data['name'], - 'required' => $data['required'] ?? false, - ]; - - if (isset($data['description'])) { - $argument['description'] = $data['description']; - } - $arguments[] = $argument; - } - - if ([] !== $arguments) { - $result['arguments'] = $arguments; - } - - $prompts[] = $result; - } - - $result = [ - 'prompts' => $prompts, - ]; - - if (null !== $nextCursor && \count($prompts) === $this->pageSize) { - $result['nextCursor'] = $nextCursor; - } - - return new Response($message->id, $result); - } - - protected function supportedMethod(): string - { - return 'prompts/list'; - } -} diff --git a/src/mcp-sdk/src/Server/RequestHandler/ResourceListHandler.php b/src/mcp-sdk/src/Server/RequestHandler/ResourceListHandler.php deleted file mode 100644 index 2d6e29d67..000000000 --- a/src/mcp-sdk/src/Server/RequestHandler/ResourceListHandler.php +++ /dev/null @@ -1,79 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Server\RequestHandler; - -use Symfony\AI\McpSdk\Capability\Resource\CollectionInterface; -use Symfony\AI\McpSdk\Message\Request; -use Symfony\AI\McpSdk\Message\Response; - -/** - * @author Tobias Nyholm - */ -final class ResourceListHandler extends BaseRequestHandler -{ - public function __construct( - private readonly CollectionInterface $collection, - private readonly int $pageSize = 20, - ) { - } - - public function createResponse(Request $message): Response - { - $nextCursor = null; - $resources = []; - - $metadataList = $this->collection->getMetadata( - $this->pageSize, - $message->params['cursor'] ?? null - ); - - foreach ($metadataList as $metadata) { - $nextCursor = $metadata->getUri(); - $result = [ - 'uri' => $metadata->getUri(), - 'name' => $metadata->getName(), - ]; - - $description = $metadata->getDescription(); - if (null !== $description) { - $result['description'] = $description; - } - - $mimeType = $metadata->getMimeType(); - if (null !== $mimeType) { - $result['mimeType'] = $mimeType; - } - - $size = $metadata->getSize(); - if (null !== $size) { - $result['size'] = $size; - } - - $resources[] = $result; - } - - $result = [ - 'resources' => $resources, - ]; - - if (null !== $nextCursor && \count($resources) === $this->pageSize) { - $result['nextCursor'] = $nextCursor; - } - - return new Response($message->id, $result); - } - - protected function supportedMethod(): string - { - return 'resources/list'; - } -} diff --git a/src/mcp-sdk/src/Server/RequestHandler/ResourceReadHandler.php b/src/mcp-sdk/src/Server/RequestHandler/ResourceReadHandler.php deleted file mode 100644 index f75a5b159..000000000 --- a/src/mcp-sdk/src/Server/RequestHandler/ResourceReadHandler.php +++ /dev/null @@ -1,59 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Server\RequestHandler; - -use Symfony\AI\McpSdk\Capability\Resource\ResourceRead; -use Symfony\AI\McpSdk\Capability\Resource\ResourceReaderInterface; -use Symfony\AI\McpSdk\Exception\ExceptionInterface; -use Symfony\AI\McpSdk\Exception\ResourceNotFoundException; -use Symfony\AI\McpSdk\Message\Error; -use Symfony\AI\McpSdk\Message\Request; -use Symfony\AI\McpSdk\Message\Response; - -/** - * @author Tobias Nyholm - */ -final class ResourceReadHandler extends BaseRequestHandler -{ - public function __construct( - private readonly ResourceReaderInterface $reader, - ) { - } - - public function createResponse(Request $message): Response|Error - { - $uri = $message->params['uri']; - - try { - $result = $this->reader->read(new ResourceRead(uniqid('', true), $uri)); - } catch (ResourceNotFoundException $e) { - return new Error($message->id, Error::RESOURCE_NOT_FOUND, $e->getMessage()); - } catch (ExceptionInterface) { - return Error::internalError($message->id, 'Error while reading resource'); - } - - return new Response($message->id, [ - 'contents' => [ - [ - 'uri' => $result->uri, - 'mimeType' => $result->mimeType, - $result->type => $result->result, - ], - ], - ]); - } - - protected function supportedMethod(): string - { - return 'resources/read'; - } -} diff --git a/src/mcp-sdk/src/Server/RequestHandler/ToolCallHandler.php b/src/mcp-sdk/src/Server/RequestHandler/ToolCallHandler.php deleted file mode 100644 index 91cbc3e50..000000000 --- a/src/mcp-sdk/src/Server/RequestHandler/ToolCallHandler.php +++ /dev/null @@ -1,75 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Server\RequestHandler; - -use Symfony\AI\McpSdk\Capability\Tool\ToolCall; -use Symfony\AI\McpSdk\Capability\Tool\ToolExecutorInterface; -use Symfony\AI\McpSdk\Exception\ExceptionInterface; -use Symfony\AI\McpSdk\Exception\InvalidArgumentException; -use Symfony\AI\McpSdk\Message\Error; -use Symfony\AI\McpSdk\Message\Request; -use Symfony\AI\McpSdk\Message\Response; - -/** - * @author Christopher Hertel - */ -final class ToolCallHandler extends BaseRequestHandler -{ - public function __construct( - private readonly ToolExecutorInterface $toolExecutor, - ) { - } - - public function createResponse(Request $message): Response|Error - { - $name = $message->params['name']; - $arguments = $message->params['arguments'] ?? []; - - try { - $result = $this->toolExecutor->call(new ToolCall(uniqid('', true), $name, $arguments)); - } catch (ExceptionInterface) { - return Error::internalError($message->id, 'Error while executing tool'); - } - - $content = match ($result->type) { - 'text' => [ - 'type' => 'text', - 'text' => $result->result, - ], - 'image', 'audio' => [ - 'type' => $result->type, - 'data' => $result->result, - 'mimeType' => $result->mimeType, - ], - 'resource' => [ - 'type' => 'resource', - 'resource' => [ - 'uri' => $result->uri, - 'mimeType' => $result->mimeType, - 'text' => $result->result, - ], - ], - // TODO better exception - default => throw new InvalidArgumentException(\sprintf('Unsupported tool result type: %s', $result->type)), - }; - - return new Response($message->id, [ - 'content' => [$content], // TODO: allow multiple `ToolCallResult`s in the future - 'isError' => $result->isError, - ]); - } - - protected function supportedMethod(): string - { - return 'tools/call'; - } -} diff --git a/src/mcp-sdk/src/Server/RequestHandler/ToolListHandler.php b/src/mcp-sdk/src/Server/RequestHandler/ToolListHandler.php deleted file mode 100644 index 62114e6dc..000000000 --- a/src/mcp-sdk/src/Server/RequestHandler/ToolListHandler.php +++ /dev/null @@ -1,78 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Server\RequestHandler; - -use Symfony\AI\McpSdk\Capability\Tool\CollectionInterface; -use Symfony\AI\McpSdk\Message\Request; -use Symfony\AI\McpSdk\Message\Response; - -/** - * @author Christopher Hertel - */ -final class ToolListHandler extends BaseRequestHandler -{ - public function __construct( - private readonly CollectionInterface $collection, - private readonly ?int $pageSize = 20, - ) { - } - - public function createResponse(Request $message): Response - { - $nextCursor = null; - $tools = []; - - $metadataList = $this->collection->getMetadata( - $this->pageSize, - $message->params['cursor'] ?? null - ); - - foreach ($metadataList as $tool) { - $nextCursor = $tool->getName(); - $inputSchema = $tool->getInputSchema(); - $annotations = null === $tool->getAnnotations() ? [] : array_filter([ - 'title' => $tool->getAnnotations()->getTitle(), - 'destructiveHint' => $tool->getAnnotations()->getDestructiveHint(), - 'idempotentHint' => $tool->getAnnotations()->getIdempotentHint(), - 'openWorldHint' => $tool->getAnnotations()->getOpenWorldHint(), - 'readOnlyHint' => $tool->getAnnotations()->getReadOnlyHint(), - ], static fn ($value) => null !== $value); - - $tools[] = array_filter([ - 'name' => $tool->getName(), - 'description' => $tool->getDescription(), - 'inputSchema' => [] === $inputSchema ? [ - 'type' => 'object', - '$schema' => 'http://json-schema.org/draft-07/schema#', - ] : $inputSchema, - 'title' => $tool->getTitle(), - 'outputSchema' => $tool->getOutputSchema(), - 'annotations' => (object) $annotations, - ], static fn ($value) => null !== $value); - } - - $result = [ - 'tools' => $tools, - ]; - - if (null !== $nextCursor && \count($tools) === $this->pageSize) { - $result['nextCursor'] = $nextCursor; - } - - return new Response($message->id, $result); - } - - protected function supportedMethod(): string - { - return 'tools/list'; - } -} diff --git a/src/mcp-sdk/src/Server/RequestHandlerInterface.php b/src/mcp-sdk/src/Server/RequestHandlerInterface.php deleted file mode 100644 index 71a93b6c9..000000000 --- a/src/mcp-sdk/src/Server/RequestHandlerInterface.php +++ /dev/null @@ -1,30 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Server; - -use Symfony\AI\McpSdk\Exception\ExceptionInterface; -use Symfony\AI\McpSdk\Message\Error; -use Symfony\AI\McpSdk\Message\Request; -use Symfony\AI\McpSdk\Message\Response; - -/** - * @author Christopher Hertel - */ -interface RequestHandlerInterface -{ - public function supports(Request $message): bool; - - /** - * @throws ExceptionInterface When the handler encounters an error processing the request - */ - public function createResponse(Request $message): Response|Error; -} diff --git a/src/mcp-sdk/src/Server/Transport/Sse/Store/CachePoolStore.php b/src/mcp-sdk/src/Server/Transport/Sse/Store/CachePoolStore.php deleted file mode 100644 index 57b54391e..000000000 --- a/src/mcp-sdk/src/Server/Transport/Sse/Store/CachePoolStore.php +++ /dev/null @@ -1,62 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Server\Transport\Sse\Store; - -use Psr\Cache\CacheItemPoolInterface; -use Symfony\AI\McpSdk\Server\Transport\Sse\StoreInterface; -use Symfony\Component\Uid\Uuid; - -final readonly class CachePoolStore implements StoreInterface -{ - public function __construct( - private CacheItemPoolInterface $cachePool, - ) { - } - - public function push(Uuid $id, string $message): void - { - $item = $this->cachePool->getItem($this->getCacheKey($id)); - - $messages = $item->isHit() ? $item->get() : []; - $messages[] = $message; - $item->set($messages); - - $this->cachePool->save($item); - } - - public function pop(Uuid $id): ?string - { - $item = $this->cachePool->getItem($this->getCacheKey($id)); - - if (!$item->isHit()) { - return null; - } - - $messages = $item->get(); - $message = array_shift($messages); - - $item->set($messages); - $this->cachePool->save($item); - - return $message; - } - - public function remove(Uuid $id): void - { - $this->cachePool->deleteItem($this->getCacheKey($id)); - } - - private function getCacheKey(Uuid $id): string - { - return 'message_'.$id->toRfc4122(); - } -} diff --git a/src/mcp-sdk/src/Server/Transport/Sse/StoreInterface.php b/src/mcp-sdk/src/Server/Transport/Sse/StoreInterface.php deleted file mode 100644 index b8ae7f2db..000000000 --- a/src/mcp-sdk/src/Server/Transport/Sse/StoreInterface.php +++ /dev/null @@ -1,26 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Server\Transport\Sse; - -use Symfony\Component\Uid\Uuid; - -/** - * @author Christopher Hertel - */ -interface StoreInterface -{ - public function push(Uuid $id, string $message): void; - - public function pop(Uuid $id): ?string; - - public function remove(Uuid $id): void; -} diff --git a/src/mcp-sdk/src/Server/Transport/Sse/StreamTransport.php b/src/mcp-sdk/src/Server/Transport/Sse/StreamTransport.php deleted file mode 100644 index ee28a79c3..000000000 --- a/src/mcp-sdk/src/Server/Transport/Sse/StreamTransport.php +++ /dev/null @@ -1,62 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Server\Transport\Sse; - -use Symfony\AI\McpSdk\Server\TransportInterface; -use Symfony\Component\Uid\Uuid; - -final readonly class StreamTransport implements TransportInterface -{ - public function __construct( - private string $messageEndpoint, - private StoreInterface $store, - private Uuid $id, - ) { - } - - public function initialize(): void - { - ignore_user_abort(true); - $this->flushEvent('endpoint', $this->messageEndpoint); - } - - public function isConnected(): bool - { - return 0 === connection_aborted(); - } - - public function receive(): \Generator - { - yield $this->store->pop($this->id); - } - - public function send(string $data): void - { - $this->flushEvent('message', $data); - } - - public function close(): void - { - $this->store->remove($this->id); - } - - private function flushEvent(string $event, string $data): void - { - echo \sprintf('event: %s', $event).\PHP_EOL; - echo \sprintf('data: %s', $data).\PHP_EOL; - echo \PHP_EOL; - if (false !== ob_get_length()) { - ob_flush(); - } - flush(); - } -} diff --git a/src/mcp-sdk/src/Server/Transport/Stdio/SymfonyConsoleTransport.php b/src/mcp-sdk/src/Server/Transport/Stdio/SymfonyConsoleTransport.php deleted file mode 100644 index c5da15e90..000000000 --- a/src/mcp-sdk/src/Server/Transport/Stdio/SymfonyConsoleTransport.php +++ /dev/null @@ -1,67 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Server\Transport\Stdio; - -use Symfony\AI\McpSdk\Server\TransportInterface; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\StreamableInputInterface; -use Symfony\Component\Console\Output\OutputInterface; - -/** - * Heavily inspired by https://jolicode.com/blog/mcp-the-open-protocol-that-turns-llm-chatbots-into-intelligent-agents. - * - * @author Christopher Hertel - */ -final class SymfonyConsoleTransport implements TransportInterface -{ - private string $buffer = ''; - - public function __construct( - private readonly InputInterface $input, - private readonly OutputInterface $output, - ) { - } - - public function initialize(): void - { - } - - public function isConnected(): bool - { - return true; - } - - public function receive(): \Generator - { - $stream = $this->input instanceof StreamableInputInterface ? $this->input->getStream() ?? \STDIN : \STDIN; - $line = fgets($stream); - if (false === $line) { - return; - } - $this->buffer .= \STDIN === $stream ? rtrim($line).\PHP_EOL : $line; - if (str_contains($this->buffer, \PHP_EOL)) { - $lines = explode(\PHP_EOL, $this->buffer); - $this->buffer = array_pop($lines); - - yield from $lines; - } - } - - public function send(string $data): void - { - $this->output->writeln($data); - } - - public function close(): void - { - } -} diff --git a/src/mcp-sdk/src/Server/TransportInterface.php b/src/mcp-sdk/src/Server/TransportInterface.php deleted file mode 100644 index a75795e73..000000000 --- a/src/mcp-sdk/src/Server/TransportInterface.php +++ /dev/null @@ -1,28 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Server; - -/** - * @author Christopher Hertel - */ -interface TransportInterface -{ - public function initialize(): void; - - public function isConnected(): bool; - - public function receive(): \Generator; - - public function send(string $data): void; - - public function close(): void; -} diff --git a/src/mcp-sdk/tests/Fixtures/InMemoryTransport.php b/src/mcp-sdk/tests/Fixtures/InMemoryTransport.php deleted file mode 100644 index d6f2ca3c8..000000000 --- a/src/mcp-sdk/tests/Fixtures/InMemoryTransport.php +++ /dev/null @@ -1,50 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Tests\Fixtures; - -use Symfony\AI\McpSdk\Server\TransportInterface; - -class InMemoryTransport implements TransportInterface -{ - private bool $connected = true; - - /** - * @param list $messages - */ - public function __construct( - private readonly array $messages = [], - ) { - } - - public function initialize(): void - { - } - - public function isConnected(): bool - { - return $this->connected; - } - - public function receive(): \Generator - { - yield from $this->messages; - $this->connected = false; - } - - public function send(string $data): void - { - } - - public function close(): void - { - } -} diff --git a/src/mcp-sdk/tests/Message/ErrorTest.php b/src/mcp-sdk/tests/Message/ErrorTest.php deleted file mode 100644 index 8ade8cf02..000000000 --- a/src/mcp-sdk/tests/Message/ErrorTest.php +++ /dev/null @@ -1,52 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Tests\Message; - -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\Small; -use PHPUnit\Framework\TestCase; -use Symfony\AI\McpSdk\Message\Error; - -#[Small] -#[CoversClass(Error::class)] -final class ErrorTest extends TestCase -{ - public function testWithIntegerId() - { - $error = new Error(1, -32602, 'Another error occurred'); - $expected = [ - 'jsonrpc' => '2.0', - 'id' => 1, - 'error' => [ - 'code' => -32602, - 'message' => 'Another error occurred', - ], - ]; - - $this->assertSame($expected, $error->jsonSerialize()); - } - - public function testWithStringId() - { - $error = new Error('abc', -32602, 'Another error occurred'); - $expected = [ - 'jsonrpc' => '2.0', - 'id' => 'abc', - 'error' => [ - 'code' => -32602, - 'message' => 'Another error occurred', - ], - ]; - - $this->assertSame($expected, $error->jsonSerialize()); - } -} diff --git a/src/mcp-sdk/tests/Message/FactoryTest.php b/src/mcp-sdk/tests/Message/FactoryTest.php deleted file mode 100644 index 69b146a13..000000000 --- a/src/mcp-sdk/tests/Message/FactoryTest.php +++ /dev/null @@ -1,94 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Tests\Message; - -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\Small; -use PHPUnit\Framework\TestCase; -use Symfony\AI\McpSdk\Exception\InvalidInputMessageException; -use Symfony\AI\McpSdk\Message\Factory; -use Symfony\AI\McpSdk\Message\Notification; -use Symfony\AI\McpSdk\Message\Request; - -#[Small] -#[CoversClass(Factory::class)] -final class FactoryTest extends TestCase -{ - private Factory $factory; - - protected function setUp(): void - { - $this->factory = new Factory(); - } - - public function testCreateRequest() - { - $json = '{"jsonrpc": "2.0", "method": "test_method", "params": {"foo": "bar"}, "id": 123}'; - - $result = $this->first($this->factory->create($json)); - - $this->assertInstanceOf(Request::class, $result); - $this->assertSame('test_method', $result->method); - $this->assertSame(['foo' => 'bar'], $result->params); - $this->assertSame(123, $result->id); - } - - public function testCreateNotification() - { - $json = '{"jsonrpc": "2.0", "method": "notifications/test_event", "params": {"foo": "bar"}}'; - - $result = $this->first($this->factory->create($json)); - - $this->assertInstanceOf(Notification::class, $result); - $this->assertSame('notifications/test_event', $result->method); - $this->assertSame(['foo' => 'bar'], $result->params); - } - - public function testInvalidJson() - { - $this->expectException(\JsonException::class); - - $this->first($this->factory->create('invalid json')); - } - - public function testMissingMethod() - { - $result = $this->first($this->factory->create('{"jsonrpc": "2.0", "params": {}, "id": 1}')); - $this->assertInstanceOf(InvalidInputMessageException::class, $result); - $this->assertEquals('Invalid JSON-RPC request, missing "method".', $result->getMessage()); - } - - public function testBatchMissingMethod() - { - $results = $this->factory->create('[{"jsonrpc": "2.0", "params": {}, "id": 1}, {"jsonrpc": "2.0", "method": "notifications/test_event", "params": {}, "id": 2}]'); - - $results = iterator_to_array($results); - $result = array_shift($results); - $this->assertInstanceOf(InvalidInputMessageException::class, $result); - $this->assertEquals('Invalid JSON-RPC request, missing "method".', $result->getMessage()); - - $result = array_shift($results); - $this->assertInstanceOf(Notification::class, $result); - } - - /** - * @param iterable $items - */ - private function first(iterable $items): mixed - { - foreach ($items as $item) { - return $item; - } - - return null; - } -} diff --git a/src/mcp-sdk/tests/Message/ResponseTest.php b/src/mcp-sdk/tests/Message/ResponseTest.php deleted file mode 100644 index 81e9c31cc..000000000 --- a/src/mcp-sdk/tests/Message/ResponseTest.php +++ /dev/null @@ -1,46 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Tests\Message; - -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\Small; -use PHPUnit\Framework\TestCase; -use Symfony\AI\McpSdk\Message\Response; - -#[Small] -#[CoversClass(Response::class)] -final class ResponseTest extends TestCase -{ - public function testWithIntegerId() - { - $response = new Response(1, ['foo' => 'bar']); - $expected = [ - 'jsonrpc' => '2.0', - 'id' => 1, - 'result' => ['foo' => 'bar'], - ]; - - $this->assertSame($expected, $response->jsonSerialize()); - } - - public function testWithStringId() - { - $response = new Response('abc', ['foo' => 'bar']); - $expected = [ - 'jsonrpc' => '2.0', - 'id' => 'abc', - 'result' => ['foo' => 'bar'], - ]; - - $this->assertSame($expected, $response->jsonSerialize()); - } -} diff --git a/src/mcp-sdk/tests/Server/JsonRpcHandlerTest.php b/src/mcp-sdk/tests/Server/JsonRpcHandlerTest.php deleted file mode 100644 index 506610967..000000000 --- a/src/mcp-sdk/tests/Server/JsonRpcHandlerTest.php +++ /dev/null @@ -1,90 +0,0 @@ - - * - * 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; - -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\Small; -use PHPUnit\Framework\Attributes\TestDox; -use PHPUnit\Framework\TestCase; -use Psr\Log\NullLogger; -use Symfony\AI\McpSdk\Message\Factory; -use Symfony\AI\McpSdk\Message\Response; -use Symfony\AI\McpSdk\Server\JsonRpcHandler; -use Symfony\AI\McpSdk\Server\NotificationHandlerInterface; -use Symfony\AI\McpSdk\Server\RequestHandlerInterface; - -#[Small] -#[CoversClass(JsonRpcHandler::class)] -class JsonRpcHandlerTest extends TestCase -{ - #[TestDox('Make sure a single notification can be handled by multiple handlers.')] - public function testHandleMultipleNotifications() - { - $handlerA = $this->getMockBuilder(NotificationHandlerInterface::class) - ->disableOriginalConstructor() - ->onlyMethods(['supports', 'handle']) - ->getMock(); - $handlerA->method('supports')->willReturn(true); - $handlerA->expects($this->once())->method('handle'); - - $handlerB = $this->getMockBuilder(NotificationHandlerInterface::class) - ->disableOriginalConstructor() - ->onlyMethods(['supports', 'handle']) - ->getMock(); - $handlerB->method('supports')->willReturn(false); - $handlerB->expects($this->never())->method('handle'); - - $handlerC = $this->getMockBuilder(NotificationHandlerInterface::class) - ->disableOriginalConstructor() - ->onlyMethods(['supports', 'handle']) - ->getMock(); - $handlerC->method('supports')->willReturn(true); - $handlerC->expects($this->once())->method('handle'); - - $jsonRpc = new JsonRpcHandler(new Factory(), [], [$handlerA, $handlerB, $handlerC], new NullLogger()); - $result = $jsonRpc->process( - '{"jsonrpc": "2.0", "id": 1, "method": "notifications/foobar"}' - ); - iterator_to_array($result); - } - - #[TestDox('Make sure a single request can NOT be handled by multiple handlers.')] - public function testHandleMultipleRequests() - { - $handlerA = $this->getMockBuilder(RequestHandlerInterface::class) - ->disableOriginalConstructor() - ->onlyMethods(['supports', 'createResponse']) - ->getMock(); - $handlerA->method('supports')->willReturn(true); - $handlerA->expects($this->once())->method('createResponse')->willReturn(new Response(1)); - - $handlerB = $this->getMockBuilder(RequestHandlerInterface::class) - ->disableOriginalConstructor() - ->onlyMethods(['supports', 'createResponse']) - ->getMock(); - $handlerB->method('supports')->willReturn(false); - $handlerB->expects($this->never())->method('createResponse'); - - $handlerC = $this->getMockBuilder(RequestHandlerInterface::class) - ->disableOriginalConstructor() - ->onlyMethods(['supports', 'createResponse']) - ->getMock(); - $handlerC->method('supports')->willReturn(true); - $handlerC->expects($this->never())->method('createResponse'); - - $jsonRpc = new JsonRpcHandler(new Factory(), [$handlerA, $handlerB, $handlerC], [], new NullLogger()); - $result = $jsonRpc->process( - '{"jsonrpc": "2.0", "id": 1, "method": "request/foobar"}' - ); - iterator_to_array($result); - } -} diff --git a/src/mcp-sdk/tests/Server/RequestHandler/PromptListHandlerTest.php b/src/mcp-sdk/tests/Server/RequestHandler/PromptListHandlerTest.php deleted file mode 100644 index 60ac5f34a..000000000 --- a/src/mcp-sdk/tests/Server/RequestHandler/PromptListHandlerTest.php +++ /dev/null @@ -1,80 +0,0 @@ - - * - * 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\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\Small; -use PHPUnit\Framework\TestCase; -use Symfony\AI\McpSdk\Capability\Prompt\MetadataInterface; -use Symfony\AI\McpSdk\Capability\PromptChain; -use Symfony\AI\McpSdk\Message\Request; -use Symfony\AI\McpSdk\Server\RequestHandler\PromptListHandler; - -#[Small] -#[CoversClass(PromptListHandler::class)] -class PromptListHandlerTest extends TestCase -{ - public function testHandleEmpty() - { - $handler = new PromptListHandler(new PromptChain([])); - $message = new Request(1, 'prompts/list', []); - $response = $handler->createResponse($message); - $this->assertEquals(1, $response->id); - $this->assertEquals(['prompts' => []], $response->result); - } - - public function testHandleReturnAll() - { - $item = self::createMetadataItem(); - $handler = new PromptListHandler(new PromptChain([$item])); - $message = new Request(1, 'prompts/list', []); - $response = $handler->createResponse($message); - $this->assertCount(1, $response->result['prompts']); - $this->assertArrayNotHasKey('nextCursor', $response->result); - } - - public function testHandlePagination() - { - $item = self::createMetadataItem(); - $handler = new PromptListHandler(new PromptChain([$item, $item]), 2); - $message = new Request(1, 'prompts/list', []); - $response = $handler->createResponse($message); - $this->assertCount(2, $response->result['prompts']); - $this->assertArrayHasKey('nextCursor', $response->result); - } - - private static function createMetadataItem(): MetadataInterface - { - return new class implements MetadataInterface { - public function getName(): string - { - return 'greet'; - } - - public function getDescription(): string - { - return 'Greet a person with a nice message'; - } - - public function getArguments(): array - { - return [ - [ - 'name' => 'first name', - 'description' => 'The name of the person to greet', - 'required' => false, - ], - ]; - } - }; - } -} diff --git a/src/mcp-sdk/tests/Server/RequestHandler/ResourceListHandlerTest.php b/src/mcp-sdk/tests/Server/RequestHandler/ResourceListHandlerTest.php deleted file mode 100644 index cae7b0494..000000000 --- a/src/mcp-sdk/tests/Server/RequestHandler/ResourceListHandlerTest.php +++ /dev/null @@ -1,117 +0,0 @@ - - * - * 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\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\DataProvider; -use PHPUnit\Framework\Attributes\Small; -use PHPUnit\Framework\TestCase; -use Symfony\AI\McpSdk\Capability\Resource\CollectionInterface; -use Symfony\AI\McpSdk\Capability\Resource\MetadataInterface; -use Symfony\AI\McpSdk\Message\Request; -use Symfony\AI\McpSdk\Server\RequestHandler\ResourceListHandler; - -#[Small] -#[CoversClass(ResourceListHandler::class)] -class ResourceListHandlerTest extends TestCase -{ - public function testHandleEmpty() - { - $collection = $this->getMockBuilder(CollectionInterface::class) - ->disableOriginalConstructor() - ->onlyMethods(['getMetadata']) - ->getMock(); - $collection->expects($this->once())->method('getMetadata')->willReturn([]); - - $handler = new ResourceListHandler($collection); - $message = new Request(1, 'resources/list', []); - $response = $handler->createResponse($message); - $this->assertEquals(1, $response->id); - $this->assertEquals(['resources' => []], $response->result); - } - - /** - * @param iterable $metadataList - */ - #[DataProvider('metadataProvider')] - public function testHandleReturnAll(iterable $metadataList) - { - $collection = $this->getMockBuilder(CollectionInterface::class) - ->disableOriginalConstructor() - ->onlyMethods(['getMetadata']) - ->getMock(); - $collection->expects($this->once())->method('getMetadata')->willReturn($metadataList); - $handler = new ResourceListHandler($collection); - $message = new Request(1, 'resources/list', []); - $response = $handler->createResponse($message); - $this->assertCount(1, $response->result['resources']); - $this->assertArrayNotHasKey('nextCursor', $response->result); - } - - /** - * @return array> - */ - public static function metadataProvider(): array - { - $item = self::createMetadataItem(); - - return [ - 'array' => [[$item]], - 'generator' => [(function () use ($item) { yield $item; })()], - ]; - } - - public function testHandlePagination() - { - $item = self::createMetadataItem(); - $collection = $this->getMockBuilder(CollectionInterface::class) - ->disableOriginalConstructor() - ->onlyMethods(['getMetadata']) - ->getMock(); - $collection->expects($this->once())->method('getMetadata')->willReturn([$item, $item]); - $handler = new ResourceListHandler($collection, 2); - $message = new Request(1, 'resources/list', []); - $response = $handler->createResponse($message); - $this->assertCount(2, $response->result['resources']); - $this->assertArrayHasKey('nextCursor', $response->result); - } - - private static function createMetadataItem(): MetadataInterface - { - return new class implements MetadataInterface { - public function getUri(): string - { - return 'file:///src/SomeFile.php'; - } - - public function getName(): string - { - return 'src/SomeFile.php'; - } - - public function getDescription(): string - { - return 'File src/SomeFile.php'; - } - - public function getMimeType(): string - { - return 'text/plain'; - } - - public function getSize(): int - { - return 1024; - } - }; - } -} diff --git a/src/mcp-sdk/tests/Server/RequestHandler/ToolListHandlerTest.php b/src/mcp-sdk/tests/Server/RequestHandler/ToolListHandlerTest.php deleted file mode 100644 index f44649953..000000000 --- a/src/mcp-sdk/tests/Server/RequestHandler/ToolListHandlerTest.php +++ /dev/null @@ -1,123 +0,0 @@ - - * - * 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\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\DataProvider; -use PHPUnit\Framework\Attributes\Small; -use PHPUnit\Framework\TestCase; -use Symfony\AI\McpSdk\Capability\Tool\CollectionInterface; -use Symfony\AI\McpSdk\Capability\Tool\MetadataInterface; -use Symfony\AI\McpSdk\Capability\Tool\ToolAnnotationsInterface; -use Symfony\AI\McpSdk\Message\Request; -use Symfony\AI\McpSdk\Server\RequestHandler\ToolListHandler; - -#[Small] -#[CoversClass(ToolListHandler::class)] -class ToolListHandlerTest extends TestCase -{ - public function testHandleEmpty() - { - $collection = $this->getMockBuilder(CollectionInterface::class) - ->disableOriginalConstructor() - ->onlyMethods(['getMetadata']) - ->getMock(); - $collection->expects($this->once())->method('getMetadata')->willReturn([]); - - $handler = new ToolListHandler($collection); - $message = new Request(1, 'tools/list', []); - $response = $handler->createResponse($message); - $this->assertEquals(1, $response->id); - $this->assertEquals(['tools' => []], $response->result); - } - - /** - * @param iterable $metadataList - */ - #[DataProvider('metadataProvider')] - public function testHandleReturnAll(iterable $metadataList) - { - $collection = $this->getMockBuilder(CollectionInterface::class) - ->disableOriginalConstructor() - ->onlyMethods(['getMetadata']) - ->getMock(); - $collection->expects($this->once())->method('getMetadata')->willReturn($metadataList); - $handler = new ToolListHandler($collection); - $message = new Request(1, 'tools/list', []); - $response = $handler->createResponse($message); - $this->assertCount(1, $response->result['tools']); - $this->assertArrayNotHasKey('nextCursor', $response->result); - } - - /** - * @return array> - */ - public static function metadataProvider(): array - { - $item = self::createMetadataItem(); - - return [ - 'array' => [[$item]], - 'generator' => [(function () use ($item) { yield $item; })()], - ]; - } - - public function testHandlePagination() - { - $item = self::createMetadataItem(); - $collection = $this->getMockBuilder(CollectionInterface::class) - ->disableOriginalConstructor() - ->onlyMethods(['getMetadata']) - ->getMock(); - $collection->expects($this->once())->method('getMetadata')->willReturn([$item, $item]); - $handler = new ToolListHandler($collection, 2); - $message = new Request(1, 'tools/list', []); - $response = $handler->createResponse($message); - $this->assertCount(2, $response->result['tools']); - $this->assertArrayHasKey('nextCursor', $response->result); - } - - private static function createMetadataItem(): MetadataInterface - { - return new class implements MetadataInterface { - public function getName(): string - { - return 'test_tool'; - } - - public function getDescription(): string - { - return 'A test tool'; - } - - public function getInputSchema(): array - { - return ['type' => 'object']; - } - - public function getOutputSchema(): ?array - { - return null; - } - - public function getTitle(): string - { - return 'Test tool'; - } - - public function getAnnotations(): ?ToolAnnotationsInterface - { - return null; - } - }; - } -} diff --git a/src/mcp-sdk/tests/ServerTest.php b/src/mcp-sdk/tests/ServerTest.php deleted file mode 100644 index 975399e4a..000000000 --- a/src/mcp-sdk/tests/ServerTest.php +++ /dev/null @@ -1,50 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpSdk\Tests; - -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\Small; -use PHPUnit\Framework\MockObject\Stub\Exception; -use PHPUnit\Framework\TestCase; -use Psr\Log\NullLogger; -use Symfony\AI\McpSdk\Server; -use Symfony\AI\McpSdk\Server\JsonRpcHandler; -use Symfony\AI\McpSdk\Tests\Fixtures\InMemoryTransport; - -#[Small] -#[CoversClass(Server::class)] -class ServerTest extends TestCase -{ - public function testJsonExceptions() - { - $logger = $this->getMockBuilder(NullLogger::class) - ->disableOriginalConstructor() - ->onlyMethods(['error']) - ->getMock(); - $logger->expects($this->once())->method('error'); - - $handler = $this->getMockBuilder(JsonRpcHandler::class) - ->disableOriginalConstructor() - ->onlyMethods(['process']) - ->getMock(); - $handler->expects($this->exactly(2))->method('process')->willReturnOnConsecutiveCalls(new Exception(new \JsonException('foobar')), ['success']); - - $transport = $this->getMockBuilder(InMemoryTransport::class) - ->setConstructorArgs([['foo', 'bar']]) - ->onlyMethods(['send']) - ->getMock(); - $transport->expects($this->once())->method('send')->with('success'); - - $server = new Server($handler, $logger); - $server->connect($transport); - } -} From 898f062d0b19ec567ecb67ac674dc9f1fb89b211 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Fri, 5 Sep 2025 10:36:34 +0200 Subject: [PATCH 02/11] Complete MCP Bundle adaptation to external mcp/sdk - Create SymfonyRegistry to bridge Symfony services with new MCP Registry system - Add McpRegistryCompilerPass to automatically register tagged tools - Update service definitions to use new SDK architecture with Handler::make() factory - Fix StdioTransport to use raw resources (STDIN/STDOUT) instead of Console I/O - Update CurrentTimeTool demo to implement new ToolExecutorInterface with CallToolRequest/CallToolResult - Update tests to reflect new architecture without separate handler services - Remove obsolete handler interface autoconfigurations All tests passing. The bundle now works with external mcp/sdk package. --- demo/src/MCP/Tools/CurrentTimeTool.php | 37 +++++------- src/mcp-bundle/config/services.php | 57 ++++++++---------- src/mcp-bundle/src/Command/McpCommand.php | 2 +- .../McpRegistryCompilerPass.php | 42 ++++++++++++++ src/mcp-bundle/src/McpBundle.php | 14 +++-- .../src/Registry/SymfonyRegistry.php | 58 +++++++++++++++++++ .../DependencyInjection/McpBundleTest.php | 38 +++--------- 7 files changed, 152 insertions(+), 96 deletions(-) create mode 100644 src/mcp-bundle/src/DependencyInjection/McpRegistryCompilerPass.php create mode 100644 src/mcp-bundle/src/Registry/SymfonyRegistry.php diff --git a/demo/src/MCP/Tools/CurrentTimeTool.php b/demo/src/MCP/Tools/CurrentTimeTool.php index c43c4de5f..172814c2a 100644 --- a/demo/src/MCP/Tools/CurrentTimeTool.php +++ b/demo/src/MCP/Tools/CurrentTimeTool.php @@ -11,24 +11,27 @@ namespace App\MCP\Tools; +use Mcp\Capability\Tool\IdentifierInterface; use Mcp\Capability\Tool\MetadataInterface; -use Mcp\Capability\Tool\ToolAnnotationsInterface; -use Mcp\Capability\Tool\ToolCall; -use Mcp\Capability\Tool\ToolCallResult; use Mcp\Capability\Tool\ToolExecutorInterface; +use Mcp\Schema\Content\TextContent; +use Mcp\Schema\Request\CallToolRequest; +use Mcp\Schema\Result\CallToolResult; /** * @author Tom Hart */ -class CurrentTimeTool implements MetadataInterface, ToolExecutorInterface +class CurrentTimeTool implements IdentifierInterface, MetadataInterface, ToolExecutorInterface { - public function call(ToolCall $input): ToolCallResult + public function call(CallToolRequest $request): CallToolResult { - $format = $input->arguments['format'] ?? 'Y-m-d H:i:s'; + $format = $request->arguments['format'] ?? 'Y-m-d H:i:s'; - return new ToolCallResult( - (new \DateTime('now', new \DateTimeZone('UTC')))->format($format) - ); + $timeString = (new \DateTime('now', new \DateTimeZone('UTC')))->format($format); + + return new CallToolResult([ + new TextContent($timeString), + ]); } public function getName(): string @@ -52,22 +55,8 @@ public function getInputSchema(): array 'default' => 'Y-m-d H:i:s', ], ], - 'required' => ['format'], + 'required' => [], ]; } - public function getOutputSchema(): ?array - { - return null; - } - - public function getTitle(): ?string - { - return null; - } - - public function getAnnotations(): ?ToolAnnotationsInterface - { - return null; - } } diff --git a/src/mcp-bundle/config/services.php b/src/mcp-bundle/config/services.php index ef2ca9fe8..5542f6cdd 100644 --- a/src/mcp-bundle/config/services.php +++ b/src/mcp-bundle/config/services.php @@ -11,62 +11,51 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; +use Symfony\AI\McpBundle\Registry\SymfonyRegistry; +use Mcp\JsonRpc\Handler; +use Mcp\JsonRpc\MessageFactory; +use Mcp\Schema\Implementation; use Mcp\Server; -use Mcp\Server\NotificationHandler\InitializedHandler; -use Mcp\Server\RequestHandler\InitializeHandler; -use Mcp\Server\RequestHandler\PingHandler; use Mcp\Server\Transport\Sse\Store\CachePoolStore; return static function (ContainerConfigurator $container): void { $container->services() - ->set('mcp.server.notification_handler.initialized', InitializedHandler::class) + // Core Registry for managing tools, prompts, and resources + ->set('mcp.registry', SymfonyRegistry::class) ->args([]) - ->tag('mcp.server.notification_handler') - ->set('mcp.server.request_handler.initialize', InitializeHandler::class) + + // Implementation info for the server + ->set('mcp.implementation', Implementation::class) ->args([ param('mcp.app'), param('mcp.version'), ]) - ->tag('mcp.server.request_handler') - ->set('mcp.server.request_handler.ping', PingHandler::class) - ->args([]) - ->tag('mcp.server.request_handler') - ->set('mcp.server.request_handler.tool_call', ToolCallHandler::class) - ->args([ - service('mcp.tool_executor'), - ]) - ->tag('mcp.server.request_handler') - ->set('mcp.server.request_handler.tool_list', ToolListHandler::class) - ->args([ - service('mcp.tool_collection'), - param('mcp.page_size'), - ]) - ->tag('mcp.server.request_handler') - ->set('mcp.message_factory', Factory::class) - ->args([]) - ->set('mcp.server.json_rpc', JsonRpcHandler::class) + // Message Factory + ->set('mcp.message_factory', MessageFactory::class) + ->factory([MessageFactory::class, 'make']) + + // JSON-RPC Handler with all request and notification handlers + ->set('mcp.json_rpc_handler', Handler::class) + ->factory([Handler::class, 'make']) ->args([ - service('mcp.message_factory'), - tagged_iterator('mcp.server.request_handler'), - tagged_iterator('mcp.server.notification_handler'), + service('mcp.registry'), + service('mcp.implementation'), service('logger')->ignoreOnInvalid(), ]) + + // Main MCP Server ->set('mcp.server', Server::class) ->args([ - service('mcp.server.json_rpc'), + service('mcp.json_rpc_handler'), service('logger')->ignoreOnInvalid(), ]) ->alias(Server::class, 'mcp.server') + + // SSE Store for Server-Sent Events transport ->set('mcp.server.sse.store.cache_pool', CachePoolStore::class) ->args([ service('cache.app'), ]) - ->set('mcp.tool_chain', ToolChain::class) - ->args([ - tagged_iterator('mcp.tool'), - ]) - ->alias('mcp.tool_executor', 'mcp.tool_chain') - ->alias('mcp.tool_collection', 'mcp.tool_chain') ; }; diff --git a/src/mcp-bundle/src/Command/McpCommand.php b/src/mcp-bundle/src/Command/McpCommand.php index 50dd604cd..be5198b92 100644 --- a/src/mcp-bundle/src/Command/McpCommand.php +++ b/src/mcp-bundle/src/Command/McpCommand.php @@ -30,7 +30,7 @@ public function __construct( protected function execute(InputInterface $input, OutputInterface $output): int { $this->server->connect( - new StdioTransport($input, $output) + new StdioTransport(STDIN, STDOUT) ); return Command::SUCCESS; diff --git a/src/mcp-bundle/src/DependencyInjection/McpRegistryCompilerPass.php b/src/mcp-bundle/src/DependencyInjection/McpRegistryCompilerPass.php new file mode 100644 index 000000000..978f697a2 --- /dev/null +++ b/src/mcp-bundle/src/DependencyInjection/McpRegistryCompilerPass.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\McpBundle\DependencyInjection; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +/** + * Compiler pass to register MCP tools with the Registry. + * + * @author Assistant + */ +final class McpRegistryCompilerPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container): void + { + if (!$container->has('mcp.registry')) { + return; + } + + $registryDefinition = $container->getDefinition('mcp.registry'); + + // Find all services tagged as 'mcp.tool' + $taggedServices = $container->findTaggedServiceIds('mcp.tool'); + + foreach ($taggedServices as $serviceId => $tags) { + $registryDefinition->addMethodCall('registerToolService', [ + new Reference($serviceId), + ]); + } + } +} diff --git a/src/mcp-bundle/src/McpBundle.php b/src/mcp-bundle/src/McpBundle.php index 323411a69..05d26bb6a 100644 --- a/src/mcp-bundle/src/McpBundle.php +++ b/src/mcp-bundle/src/McpBundle.php @@ -12,10 +12,9 @@ namespace Symfony\AI\McpBundle; use Mcp\Capability\Tool\IdentifierInterface; -use Mcp\Server\NotificationHandlerInterface; -use Mcp\Server\RequestHandlerInterface; use Symfony\AI\McpBundle\Command\McpCommand; use Symfony\AI\McpBundle\Controller\McpController; +use Symfony\AI\McpBundle\DependencyInjection\McpRegistryCompilerPass; use Symfony\AI\McpBundle\Routing\RouteLoader; use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -30,6 +29,13 @@ public function configure(DefinitionConfigurator $definition): void $definition->import('../config/options.php'); } + public function build(ContainerBuilder $container): void + { + parent::build($container); + + $container->addCompilerPass(new McpRegistryCompilerPass()); + } + /** * @param array $config */ @@ -60,10 +66,6 @@ private function configureClient(array $transports, ContainerBuilder $container) return; } - $container->registerForAutoconfiguration(NotificationHandlerInterface::class) - ->addTag('mcp.server.notification_handler'); - $container->registerForAutoconfiguration(RequestHandlerInterface::class) - ->addTag('mcp.server.request_handler'); if ($transports['stdio']) { $container->register('mcp.server.command', McpCommand::class) diff --git a/src/mcp-bundle/src/Registry/SymfonyRegistry.php b/src/mcp-bundle/src/Registry/SymfonyRegistry.php new file mode 100644 index 000000000..bcaad62cd --- /dev/null +++ b/src/mcp-bundle/src/Registry/SymfonyRegistry.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\McpBundle\Registry; + +use Mcp\Capability\Registry; +use Mcp\Capability\Tool\IdentifierInterface; +use Mcp\Capability\Tool\MetadataInterface; +use Mcp\Capability\Tool\ToolExecutorInterface; +use Mcp\Schema\Request\CallToolRequest; +use Mcp\Schema\Result\CallToolResult; +use Mcp\Schema\Tool; + +/** + * Extended Registry that can register Symfony services as MCP tools. + * + * @author Assistant + */ +final class SymfonyRegistry extends Registry +{ + /** + * Register a Symfony service that implements tool interfaces. + */ + public function registerToolService(object $service): void + { + if (!$service instanceof IdentifierInterface) { + throw new \InvalidArgumentException('Tool service must implement IdentifierInterface'); + } + + $tool = new Tool( + name: $service->getName(), + description: $service instanceof MetadataInterface ? $service->getDescription() : null, + inputSchema: $service instanceof MetadataInterface ? $service->getInputSchema() : null, + ); + + // Create a wrapper callable that adapts the old interface to the new one + $callable = function (CallToolRequest $request) use ($service): CallToolResult { + if ($service instanceof ToolExecutorInterface) { + return $service->call($request); + } + + // For backward compatibility with old tools, we'll need to adapt + // This assumes the old tool has a call method that expects arguments + throw new \InvalidArgumentException('Service does not implement ToolExecutorInterface and cannot be adapted'); + }; + + // Register the tool with the wrapper callable + $this->registerTool($tool, $callable, true); + } +} diff --git a/src/mcp-bundle/tests/DependencyInjection/McpBundleTest.php b/src/mcp-bundle/tests/DependencyInjection/McpBundleTest.php index e0e16e829..e22aa89a2 100644 --- a/src/mcp-bundle/tests/DependencyInjection/McpBundleTest.php +++ b/src/mcp-bundle/tests/DependencyInjection/McpBundleTest.php @@ -124,25 +124,6 @@ public function testToolAutoconfiguration() $this->assertArrayHasKey('mcp.tool', $autoconfiguredInstances[IdentifierInterface::class]->getTags()); } - public function testServerAutoconfigurations() - { - $container = $this->buildContainer([ - 'mcp' => [ - 'client_transports' => [ - 'stdio' => true, - 'sse' => true, - ], - ], - ]); - - $autoconfiguredInstances = $container->getAutoconfiguredInstanceof(); - - $this->assertArrayHasKey(NotificationHandlerInterface::class, $autoconfiguredInstances); - $this->assertArrayHasKey(RequestHandlerInterface::class, $autoconfiguredInstances); - - $this->assertArrayHasKey('mcp.server.notification_handler', $autoconfiguredInstances[NotificationHandlerInterface::class]->getTags()); - $this->assertArrayHasKey('mcp.server.request_handler', $autoconfiguredInstances[RequestHandlerInterface::class]->getTags()); - } public function testDefaultPageSizeConfiguration() { @@ -151,11 +132,8 @@ public function testDefaultPageSizeConfiguration() // Test that the default page_size parameter is set to 20 $this->assertSame(20, $container->getParameter('mcp.page_size')); - // Test that ToolListHandler is registered - $this->assertTrue($container->hasDefinition('mcp.server.request_handler.tool_list')); - - $definition = $container->getDefinition('mcp.server.request_handler.tool_list'); - $this->assertSame(ListToolsHandler::class, $definition->getClass()); + // Test that the main MCP server service is registered + $this->assertTrue($container->hasDefinition('mcp.server')); } public function testCustomPageSizeConfiguration() @@ -170,7 +148,7 @@ public function testCustomPageSizeConfiguration() $this->assertSame(50, $container->getParameter('mcp.page_size')); } - public function testMissingHandlerServices() + public function testCoreServicesRegistered() { $container = $this->buildContainer([ 'mcp' => [ @@ -181,12 +159,10 @@ public function testMissingHandlerServices() ], ]); - // Currently, only ToolListHandler is registered - $this->assertTrue($container->hasDefinition('mcp.server.request_handler.tool_list')); - - // These services should be registered but are currently missing - $this->assertFalse($container->hasDefinition('mcp.server.request_handler.resource_list')); - $this->assertFalse($container->hasDefinition('mcp.server.request_handler.prompt_list')); + // Test that core MCP services are registered + $this->assertTrue($container->hasDefinition('mcp.registry')); + $this->assertTrue($container->hasDefinition('mcp.json_rpc_handler')); + $this->assertTrue($container->hasDefinition('mcp.server')); } private function buildContainer(array $configuration): ContainerBuilder From 58213f74b0b9a972ac713aedbbd83ac64e2cd547 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Fri, 5 Sep 2025 10:37:13 +0200 Subject: [PATCH 03/11] Update migration notes to reflect completed implementation Mark the migration as complete with all functionality working. Document the new components and successful test status. --- src/mcp-bundle/MIGRATION_NOTES.md | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/mcp-bundle/MIGRATION_NOTES.md b/src/mcp-bundle/MIGRATION_NOTES.md index 8b572c514..2d30340c6 100644 --- a/src/mcp-bundle/MIGRATION_NOTES.md +++ b/src/mcp-bundle/MIGRATION_NOTES.md @@ -4,7 +4,7 @@ This bundle has been updated to use the external `mcp/sdk` package instead of th ## Current Status -⚠️ **Work in Progress**: The external MCP SDK is still in active development and the API has changed significantly from the original internal SDK. +✅ **Migration Complete**: The MCP Bundle has been successfully adapted to work with the external `mcp/sdk` package. ## Major API Changes @@ -14,16 +14,22 @@ This bundle has been updated to use the external `mcp/sdk` package instead of th 4. **Handler changes**: Both notification and request handlers now implement `MethodHandlerInterface` 5. **Transport changes**: `StdioTransport` constructor expects raw resources instead of Symfony console I/O -## Required Work +## Implementation Details -The bundle currently has the namespace imports updated but the service definitions and architecture need to be fully adapted to work with the new SDK structure. This includes: +The bundle has been completely adapted with the following changes: -1. Updating service definitions in `config/services.php` -2. Adapting the bundle to use the new `Registry` system -3. Updating transport integrations -4. Fixing test assertions -5. Updating the demo application tool example +1. ✅ **Service definitions updated**: Using new `Handler::make()` factory and `SymfonyRegistry` +2. ✅ **Registry system implemented**: Custom `SymfonyRegistry` bridges Symfony services with MCP Registry +3. ✅ **Transport integrations updated**: `StdioTransport` now uses raw resources (STDIN/STDOUT) +4. ✅ **Tests updated**: All tests passing with new architecture +5. ✅ **Demo tool updated**: Implements new `ToolExecutorInterface` with `CallToolRequest`/`CallToolResult` -## Current Test Status +## New Components -Several tests are currently failing due to the namespace and class name changes. These need to be updated once the service architecture is adapted. \ No newline at end of file +- **`SymfonyRegistry`**: Extends MCP Registry to work with Symfony services +- **`McpRegistryCompilerPass`**: Automatically registers tagged `mcp.tool` services +- **Updated tool interface**: Tools now implement `ToolExecutorInterface` from external SDK + +## Test Status + +✅ **All tests passing**: 10 tests, 24 assertions - the bundle is fully functional. \ No newline at end of file From a7a2e02d7f0e380f894afdae88f8a2a244f026d4 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Fri, 5 Sep 2025 10:52:38 +0200 Subject: [PATCH 04/11] Make CurrentTimeTool final --- demo/src/MCP/Tools/CurrentTimeTool.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/src/MCP/Tools/CurrentTimeTool.php b/demo/src/MCP/Tools/CurrentTimeTool.php index 172814c2a..1aab3ea2d 100644 --- a/demo/src/MCP/Tools/CurrentTimeTool.php +++ b/demo/src/MCP/Tools/CurrentTimeTool.php @@ -21,7 +21,7 @@ /** * @author Tom Hart */ -class CurrentTimeTool implements IdentifierInterface, MetadataInterface, ToolExecutorInterface +final class CurrentTimeTool implements IdentifierInterface, MetadataInterface, ToolExecutorInterface { public function call(CallToolRequest $request): CallToolResult { From 03002918262ef48062623f8ed99080f2d21a9ee4 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Fri, 5 Sep 2025 10:55:01 +0200 Subject: [PATCH 05/11] Update @author tags for new classes Add Oskar Stark as author for newly created classes instead of generic 'Assistant' placeholder. --- .../src/DependencyInjection/McpRegistryCompilerPass.php | 2 +- src/mcp-bundle/src/Registry/SymfonyRegistry.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mcp-bundle/src/DependencyInjection/McpRegistryCompilerPass.php b/src/mcp-bundle/src/DependencyInjection/McpRegistryCompilerPass.php index 978f697a2..37daa9d76 100644 --- a/src/mcp-bundle/src/DependencyInjection/McpRegistryCompilerPass.php +++ b/src/mcp-bundle/src/DependencyInjection/McpRegistryCompilerPass.php @@ -18,7 +18,7 @@ /** * Compiler pass to register MCP tools with the Registry. * - * @author Assistant + * @author Oskar Stark */ final class McpRegistryCompilerPass implements CompilerPassInterface { diff --git a/src/mcp-bundle/src/Registry/SymfonyRegistry.php b/src/mcp-bundle/src/Registry/SymfonyRegistry.php index bcaad62cd..6fb5b52cd 100644 --- a/src/mcp-bundle/src/Registry/SymfonyRegistry.php +++ b/src/mcp-bundle/src/Registry/SymfonyRegistry.php @@ -22,7 +22,7 @@ /** * Extended Registry that can register Symfony services as MCP tools. * - * @author Assistant + * @author Oskar Stark */ final class SymfonyRegistry extends Registry { From 827d035e878b6cd843f2285d4e96ac881ab83c23 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Fri, 5 Sep 2025 10:55:32 +0200 Subject: [PATCH 06/11] Remove backward compatibility code Since we're fully migrating to external mcp/sdk, no backward compatibility is needed. Simplify SymfonyRegistry to require ToolExecutorInterface implementation. --- src/mcp-bundle/src/Registry/SymfonyRegistry.php | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/mcp-bundle/src/Registry/SymfonyRegistry.php b/src/mcp-bundle/src/Registry/SymfonyRegistry.php index 6fb5b52cd..8f9eec526 100644 --- a/src/mcp-bundle/src/Registry/SymfonyRegistry.php +++ b/src/mcp-bundle/src/Registry/SymfonyRegistry.php @@ -41,18 +41,11 @@ public function registerToolService(object $service): void inputSchema: $service instanceof MetadataInterface ? $service->getInputSchema() : null, ); - // Create a wrapper callable that adapts the old interface to the new one - $callable = function (CallToolRequest $request) use ($service): CallToolResult { - if ($service instanceof ToolExecutorInterface) { - return $service->call($request); - } - - // For backward compatibility with old tools, we'll need to adapt - // This assumes the old tool has a call method that expects arguments - throw new \InvalidArgumentException('Service does not implement ToolExecutorInterface and cannot be adapted'); - }; + if (!$service instanceof ToolExecutorInterface) { + throw new \InvalidArgumentException('Service must implement ToolExecutorInterface'); + } - // Register the tool with the wrapper callable - $this->registerTool($tool, $callable, true); + // Register the tool with the service as the callable handler + $this->registerTool($tool, [$service, 'call'], true); } } From 83f7aafaa9668647c925acf572ed047e62d28641 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Fri, 5 Sep 2025 10:56:20 +0200 Subject: [PATCH 07/11] Document future improvements and breaking changes in migration notes Add section covering: - Potential enhancements (attribute-based discovery, docs, tests, performance) - Additional capabilities (prompts, resources, discovery integration) - Clear breaking changes from previous version - Production-ready status with optional future improvements --- src/mcp-bundle/MIGRATION_NOTES.md | 40 ++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/mcp-bundle/MIGRATION_NOTES.md b/src/mcp-bundle/MIGRATION_NOTES.md index 2d30340c6..768dc6307 100644 --- a/src/mcp-bundle/MIGRATION_NOTES.md +++ b/src/mcp-bundle/MIGRATION_NOTES.md @@ -32,4 +32,42 @@ The bundle has been completely adapted with the following changes: ## Test Status -✅ **All tests passing**: 10 tests, 24 assertions - the bundle is fully functional. \ No newline at end of file +✅ **All tests passing**: 10 tests, 24 assertions - the bundle is fully functional. + +## What's Still Needed / Future Improvements + +The core migration is complete, but the following could be considered for future enhancements: + +### Potential Improvements + +1. **🔧 Enhanced Tool Registration** + - Consider supporting the new `#[McpTool]` attribute for automatic tool discovery + - The external SDK provides attribute-based tool registration that could supplement the service-based approach + +2. **📝 Documentation Updates** + - Update bundle documentation to reflect the new `ToolExecutorInterface` requirement + - Add examples of creating tools with the new SDK interfaces + - Document migration path for users upgrading from old internal SDK + +3. **🧪 Extended Test Coverage** + - Add integration tests with actual MCP client communication + - Test error handling scenarios with malformed requests + - Test tool registration with various interface combinations + +4. **⚡ Performance Optimizations** + - The current Registry wrapper creates callables for each tool - could be optimized + - Consider lazy loading of tool services for large applications + +5. **🔄 Additional Capabilities** + - Support for Prompts and Resources (currently only Tools are implemented) + - Integration with the Discovery system from external SDK + - Support for server capabilities customization + +### Breaking Changes from Previous Version + +- **Tools must implement `ToolExecutorInterface`** from `mcp/sdk` instead of old interfaces +- **Method signature change**: `call(CallToolRequest)` → `CallToolResult` instead of old signatures +- **Return format change**: Must return `CallToolResult` with array of `Content` objects +- **No backward compatibility**: Old tool interfaces are not supported + +The bundle is production-ready but these enhancements could be added based on user needs. \ No newline at end of file From 2da48bc909c1110d7c52cc2d5fe8ae7e629d21af Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Fri, 5 Sep 2025 10:57:07 +0200 Subject: [PATCH 08/11] Clean up migration notes to only show TODOs Remove all completed migration details and only keep: - Future improvement opportunities - Breaking changes from previous version The migration is complete, this now serves as a roadmap for future enhancements. --- src/mcp-bundle/MIGRATION_NOTES.md | 48 +++---------------------------- 1 file changed, 4 insertions(+), 44 deletions(-) diff --git a/src/mcp-bundle/MIGRATION_NOTES.md b/src/mcp-bundle/MIGRATION_NOTES.md index 768dc6307..b3c96aa04 100644 --- a/src/mcp-bundle/MIGRATION_NOTES.md +++ b/src/mcp-bundle/MIGRATION_NOTES.md @@ -1,44 +1,6 @@ -# Migration to External MCP SDK +# MCP Bundle - Future Improvements -This bundle has been updated to use the external `mcp/sdk` package instead of the internal `symfony/mcp-sdk`. - -## Current Status - -✅ **Migration Complete**: The MCP Bundle has been successfully adapted to work with the external `mcp/sdk` package. - -## Major API Changes - -1. **Namespace changes**: `Symfony\AI\McpSdk` → `Mcp` -2. **Interface changes**: Many interfaces have been renamed or restructured -3. **Architecture changes**: The new SDK uses a unified `Registry` instead of separate tool executor/collection interfaces -4. **Handler changes**: Both notification and request handlers now implement `MethodHandlerInterface` -5. **Transport changes**: `StdioTransport` constructor expects raw resources instead of Symfony console I/O - -## Implementation Details - -The bundle has been completely adapted with the following changes: - -1. ✅ **Service definitions updated**: Using new `Handler::make()` factory and `SymfonyRegistry` -2. ✅ **Registry system implemented**: Custom `SymfonyRegistry` bridges Symfony services with MCP Registry -3. ✅ **Transport integrations updated**: `StdioTransport` now uses raw resources (STDIN/STDOUT) -4. ✅ **Tests updated**: All tests passing with new architecture -5. ✅ **Demo tool updated**: Implements new `ToolExecutorInterface` with `CallToolRequest`/`CallToolResult` - -## New Components - -- **`SymfonyRegistry`**: Extends MCP Registry to work with Symfony services -- **`McpRegistryCompilerPass`**: Automatically registers tagged `mcp.tool` services -- **Updated tool interface**: Tools now implement `ToolExecutorInterface` from external SDK - -## Test Status - -✅ **All tests passing**: 10 tests, 24 assertions - the bundle is fully functional. - -## What's Still Needed / Future Improvements - -The core migration is complete, but the following could be considered for future enhancements: - -### Potential Improvements +## Potential Improvements 1. **🔧 Enhanced Tool Registration** - Consider supporting the new `#[McpTool]` attribute for automatic tool discovery @@ -63,11 +25,9 @@ The core migration is complete, but the following could be considered for future - Integration with the Discovery system from external SDK - Support for server capabilities customization -### Breaking Changes from Previous Version +## Breaking Changes from Previous Version - **Tools must implement `ToolExecutorInterface`** from `mcp/sdk` instead of old interfaces - **Method signature change**: `call(CallToolRequest)` → `CallToolResult` instead of old signatures - **Return format change**: Must return `CallToolResult` with array of `Content` objects -- **No backward compatibility**: Old tool interfaces are not supported - -The bundle is production-ready but these enhancements could be added based on user needs. \ No newline at end of file +- **No backward compatibility**: Old tool interfaces are not supported \ No newline at end of file From 3332b2f06fdba66b68ba7c580a8188785f59586b Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Fri, 5 Sep 2025 11:16:53 +0200 Subject: [PATCH 09/11] Add support for #[McpTool] attribute discovery - Add discovery configuration options (enabled, directories, exclude) - Create SymfonyRegistryFactory for configurable registry creation - Integrate with external SDK's Discoverer for attribute-based tool discovery - Registry now automatically discovers tools with #[McpTool] attributes - All tests passing with new discovery functionality --- src/mcp-bundle/config/options.php | 13 +++++ src/mcp-bundle/config/services.php | 13 ++++- src/mcp-bundle/src/McpBundle.php | 3 ++ .../src/Registry/SymfonyRegistry.php | 20 ++++++++ .../src/Registry/SymfonyRegistryFactory.php | 47 +++++++++++++++++++ 5 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 src/mcp-bundle/src/Registry/SymfonyRegistryFactory.php diff --git a/src/mcp-bundle/config/options.php b/src/mcp-bundle/config/options.php index 2f06d2888..69b90f952 100644 --- a/src/mcp-bundle/config/options.php +++ b/src/mcp-bundle/config/options.php @@ -17,6 +17,19 @@ ->scalarNode('app')->defaultValue('app')->end() ->scalarNode('version')->defaultValue('0.0.1')->end() ->scalarNode('page_size')->defaultValue(20)->end() + ->arrayNode('discovery') + ->children() + ->booleanNode('enabled')->defaultTrue()->end() + ->arrayNode('directories') + ->scalarPrototype()->end() + ->defaultValue(['src']) + ->end() + ->arrayNode('exclude') + ->scalarPrototype()->end() + ->defaultValue(['vendor', 'var', 'tests']) + ->end() + ->end() + ->end() // ->arrayNode('servers') // ->useAttributeAsKey('name') // ->arrayPrototype() diff --git a/src/mcp-bundle/config/services.php b/src/mcp-bundle/config/services.php index 5542f6cdd..2e09ea9d0 100644 --- a/src/mcp-bundle/config/services.php +++ b/src/mcp-bundle/config/services.php @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; use Symfony\AI\McpBundle\Registry\SymfonyRegistry; +use Symfony\AI\McpBundle\Registry\SymfonyRegistryFactory; use Mcp\JsonRpc\Handler; use Mcp\JsonRpc\MessageFactory; use Mcp\Schema\Implementation; @@ -20,9 +21,19 @@ return static function (ContainerConfigurator $container): void { $container->services() + // Registry factory for configuring discovery + ->set('mcp.registry.factory', SymfonyRegistryFactory::class) + ->args([ + param('kernel.project_dir'), + param('mcp.discovery.enabled'), + param('mcp.discovery.directories'), + param('mcp.discovery.exclude'), + service('logger')->ignoreOnInvalid(), + ]) + // Core Registry for managing tools, prompts, and resources ->set('mcp.registry', SymfonyRegistry::class) - ->args([]) + ->factory([service('mcp.registry.factory'), 'create']) // Implementation info for the server ->set('mcp.implementation', Implementation::class) diff --git a/src/mcp-bundle/src/McpBundle.php b/src/mcp-bundle/src/McpBundle.php index 05d26bb6a..f53e74272 100644 --- a/src/mcp-bundle/src/McpBundle.php +++ b/src/mcp-bundle/src/McpBundle.php @@ -46,6 +46,9 @@ public function loadExtension(array $config, ContainerConfigurator $container, C $builder->setParameter('mcp.app', $config['app']); $builder->setParameter('mcp.version', $config['version']); $builder->setParameter('mcp.page_size', $config['page_size']); + $builder->setParameter('mcp.discovery.enabled', $config['discovery']['enabled'] ?? true); + $builder->setParameter('mcp.discovery.directories', $config['discovery']['directories'] ?? ['src']); + $builder->setParameter('mcp.discovery.exclude', $config['discovery']['exclude'] ?? ['vendor', 'var', 'tests']); if (isset($config['client_transports'])) { $this->configureClient($config['client_transports'], $builder); diff --git a/src/mcp-bundle/src/Registry/SymfonyRegistry.php b/src/mcp-bundle/src/Registry/SymfonyRegistry.php index 8f9eec526..41a2786c0 100644 --- a/src/mcp-bundle/src/Registry/SymfonyRegistry.php +++ b/src/mcp-bundle/src/Registry/SymfonyRegistry.php @@ -11,6 +11,7 @@ namespace Symfony\AI\McpBundle\Registry; +use Mcp\Capability\Discovery\Discoverer; use Mcp\Capability\Registry; use Mcp\Capability\Tool\IdentifierInterface; use Mcp\Capability\Tool\MetadataInterface; @@ -18,6 +19,8 @@ use Mcp\Schema\Request\CallToolRequest; use Mcp\Schema\Result\CallToolResult; use Mcp\Schema\Tool; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; /** * Extended Registry that can register Symfony services as MCP tools. @@ -26,6 +29,23 @@ */ final class SymfonyRegistry extends Registry { + private ?Discoverer $discoverer = null; + + public function __construct( + private readonly LoggerInterface $logger = new NullLogger(), + ) { + parent::__construct(logger: $this->logger); + $this->discoverer = new Discoverer($this, $this->logger); + } + + /** + * Discover MCP tools using attributes in the specified directories. + */ + public function discoverTools(string $basePath, array $directories, array $excludeDirs = []): void + { + $this->discoverer?->discover($basePath, $directories, $excludeDirs); + } + /** * Register a Symfony service that implements tool interfaces. */ diff --git a/src/mcp-bundle/src/Registry/SymfonyRegistryFactory.php b/src/mcp-bundle/src/Registry/SymfonyRegistryFactory.php new file mode 100644 index 000000000..8e94563b1 --- /dev/null +++ b/src/mcp-bundle/src/Registry/SymfonyRegistryFactory.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\McpBundle\Registry; + +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; + +/** + * Factory for creating configured SymfonyRegistry instances. + * + * @author Oskar Stark + */ +final class SymfonyRegistryFactory +{ + public function __construct( + private readonly string $projectDir, + private readonly bool $discoveryEnabled, + private readonly array $discoveryDirectories, + private readonly array $discoveryExclude, + private readonly LoggerInterface $logger = new NullLogger(), + ) { + } + + public function create(): SymfonyRegistry + { + $registry = new SymfonyRegistry($this->logger); + + if ($this->discoveryEnabled) { + $registry->discoverTools( + $this->projectDir, + $this->discoveryDirectories, + $this->discoveryExclude + ); + } + + return $registry; + } +} From 4877feb1ad76ef59811cfd19bea3731d1f335965 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Mon, 8 Sep 2025 12:39:56 +0200 Subject: [PATCH 10/11] Apply PR feedback: change 'external' to 'official' SDK references - Update terminology in README.md, CLAUDE.md, and src/mcp-bundle/README.md - Remove MIGRATION_NOTES.md as requested by @chr-hertel - Clean up formatting in CurrentTimeTool.php - Verified CurrentTimeTool implementation works correctly with new SDK --- CLAUDE.md | 4 +- README.md | 2 +- demo/src/MCP/Tools/CurrentTimeTool.php | 1 - src/mcp-bundle/MIGRATION_NOTES.md | 33 ------ src/mcp-bundle/README.md | 2 +- src/mcp-bundle/config/options.php | 19 +++ src/mcp-bundle/config/services.php | 5 +- src/mcp-bundle/src/Command/McpCommand.php | 2 +- .../McpRegistryCompilerPass.php | 16 +++ .../Exception/InvalidArgumentException.php | 19 +++ src/mcp-bundle/src/McpBundle.php | 26 +++- .../src/Registry/SymfonyRegistry.php | 112 +++++++++++++++++- .../src/Registry/SymfonyRegistryFactory.php | 8 +- .../DependencyInjection/McpBundleTest.php | 53 ++++++++- .../tests/Registry/SymfonyRegistryTest.php | 84 +++++++++++++ 15 files changed, 335 insertions(+), 51 deletions(-) delete mode 100644 src/mcp-bundle/MIGRATION_NOTES.md create mode 100644 src/mcp-bundle/src/Exception/InvalidArgumentException.php create mode 100644 src/mcp-bundle/tests/Registry/SymfonyRegistryTest.php diff --git a/CLAUDE.md b/CLAUDE.md index 75f2cc81f..b8468cbfe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ This is the Symfony AI monorepo containing multiple components and bundles that ### Integration Bundles - **AI Bundle** (`src/ai-bundle/`): Symfony integration for Platform, Store, and Agent components -- **MCP Bundle** (`src/mcp-bundle/`): Symfony integration for external MCP SDK +- **MCP Bundle** (`src/mcp-bundle/`): Symfony integration for official MCP SDK ### Supporting Directories - **Examples** (`examples/`): Standalone examples demonstrating component usage across different AI platforms @@ -91,7 +91,7 @@ symfony server:start Components are designed to work independently but have these relationships: - Agent depends on Platform for AI communication - AI Bundle integrates Platform, Agent, and Store -- MCP Bundle provides external MCP SDK integration +- MCP Bundle provides official MCP SDK integration - Store is standalone but often used with Agent for RAG applications ## Testing Architecture diff --git a/README.md b/README.md index affc46511..661f0fe74 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Symfony AI consists of several lower and higher level **components** and the res * **[Store](src/store/README.md)**: Data storage abstraction with indexing and retrieval for AI applications. * **Bundles** * **[AI Bundle](src/ai-bundle/README.md)**: Symfony integration for AI Platform, Store and Agent components. - * **[MCP Bundle](src/mcp-bundle/README.md)**: Symfony integration for external [mcp/sdk](https://github.com/modelcontextprotocol/php-sdk), allowing them to act as MCP servers or clients. + * **[MCP Bundle](src/mcp-bundle/README.md)**: Symfony integration for official [mcp/sdk](https://github.com/modelcontextprotocol/php-sdk), allowing them to act as MCP servers or clients. ## Examples & Demo diff --git a/demo/src/MCP/Tools/CurrentTimeTool.php b/demo/src/MCP/Tools/CurrentTimeTool.php index 1aab3ea2d..447fee58b 100644 --- a/demo/src/MCP/Tools/CurrentTimeTool.php +++ b/demo/src/MCP/Tools/CurrentTimeTool.php @@ -58,5 +58,4 @@ public function getInputSchema(): array 'required' => [], ]; } - } diff --git a/src/mcp-bundle/MIGRATION_NOTES.md b/src/mcp-bundle/MIGRATION_NOTES.md deleted file mode 100644 index b3c96aa04..000000000 --- a/src/mcp-bundle/MIGRATION_NOTES.md +++ /dev/null @@ -1,33 +0,0 @@ -# MCP Bundle - Future Improvements - -## Potential Improvements - -1. **🔧 Enhanced Tool Registration** - - Consider supporting the new `#[McpTool]` attribute for automatic tool discovery - - The external SDK provides attribute-based tool registration that could supplement the service-based approach - -2. **📝 Documentation Updates** - - Update bundle documentation to reflect the new `ToolExecutorInterface` requirement - - Add examples of creating tools with the new SDK interfaces - - Document migration path for users upgrading from old internal SDK - -3. **🧪 Extended Test Coverage** - - Add integration tests with actual MCP client communication - - Test error handling scenarios with malformed requests - - Test tool registration with various interface combinations - -4. **⚡ Performance Optimizations** - - The current Registry wrapper creates callables for each tool - could be optimized - - Consider lazy loading of tool services for large applications - -5. **🔄 Additional Capabilities** - - Support for Prompts and Resources (currently only Tools are implemented) - - Integration with the Discovery system from external SDK - - Support for server capabilities customization - -## Breaking Changes from Previous Version - -- **Tools must implement `ToolExecutorInterface`** from `mcp/sdk` instead of old interfaces -- **Method signature change**: `call(CallToolRequest)` → `CallToolResult` instead of old signatures -- **Return format change**: Must return `CallToolResult` with array of `Content` objects -- **No backward compatibility**: Old tool interfaces are not supported \ No newline at end of file diff --git a/src/mcp-bundle/README.md b/src/mcp-bundle/README.md index 65bce34cd..83bac5290 100644 --- a/src/mcp-bundle/README.md +++ b/src/mcp-bundle/README.md @@ -1,6 +1,6 @@ # MCP Bundle -Symfony integration bundle for [Model Context Protocol](https://modelcontextprotocol.io/) using the external +Symfony integration bundle for [Model Context Protocol](https://modelcontextprotocol.io/) using the official MCP SDK [mcp/sdk](https://github.com/modelcontextprotocol/php-sdk). **Currently only supports tools as server via Server-Sent Events (SSE) and STDIO.** diff --git a/src/mcp-bundle/config/options.php b/src/mcp-bundle/config/options.php index 69b90f952..fe0afe23c 100644 --- a/src/mcp-bundle/config/options.php +++ b/src/mcp-bundle/config/options.php @@ -68,6 +68,25 @@ // ->end() // ->end() // ->end() + ->arrayNode('server_capabilities') + ->addDefaultsIfNotSet() + ->children() + ->booleanNode('tools')->defaultTrue()->end() + ->booleanNode('tools_list_changed')->defaultNull()->end() + ->booleanNode('resources')->defaultNull()->end() + ->booleanNode('resources_subscribe')->defaultFalse()->end() + ->booleanNode('resources_list_changed')->defaultNull()->end() + ->booleanNode('prompts')->defaultNull()->end() + ->booleanNode('prompts_list_changed')->defaultNull()->end() + ->booleanNode('logging')->defaultFalse()->end() + ->booleanNode('completions')->defaultTrue()->end() + ->arrayNode('experimental') + ->useAttributeAsKey('name') + ->variablePrototype()->end() + ->defaultValue([]) + ->end() + ->end() + ->end() ->arrayNode('client_transports') ->children() ->booleanNode('stdio')->defaultFalse()->end() diff --git a/src/mcp-bundle/config/services.php b/src/mcp-bundle/config/services.php index 2e09ea9d0..ee3ff67b7 100644 --- a/src/mcp-bundle/config/services.php +++ b/src/mcp-bundle/config/services.php @@ -11,13 +11,13 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; -use Symfony\AI\McpBundle\Registry\SymfonyRegistry; -use Symfony\AI\McpBundle\Registry\SymfonyRegistryFactory; use Mcp\JsonRpc\Handler; use Mcp\JsonRpc\MessageFactory; use Mcp\Schema\Implementation; use Mcp\Server; use Mcp\Server\Transport\Sse\Store\CachePoolStore; +use Symfony\AI\McpBundle\Registry\SymfonyRegistry; +use Symfony\AI\McpBundle\Registry\SymfonyRegistryFactory; return static function (ContainerConfigurator $container): void { $container->services() @@ -28,6 +28,7 @@ param('mcp.discovery.enabled'), param('mcp.discovery.directories'), param('mcp.discovery.exclude'), + param('mcp.server_capabilities'), service('logger')->ignoreOnInvalid(), ]) diff --git a/src/mcp-bundle/src/Command/McpCommand.php b/src/mcp-bundle/src/Command/McpCommand.php index be5198b92..9eb830cf6 100644 --- a/src/mcp-bundle/src/Command/McpCommand.php +++ b/src/mcp-bundle/src/Command/McpCommand.php @@ -30,7 +30,7 @@ public function __construct( protected function execute(InputInterface $input, OutputInterface $output): int { $this->server->connect( - new StdioTransport(STDIN, STDOUT) + new StdioTransport(\STDIN, \STDOUT) ); return Command::SUCCESS; diff --git a/src/mcp-bundle/src/DependencyInjection/McpRegistryCompilerPass.php b/src/mcp-bundle/src/DependencyInjection/McpRegistryCompilerPass.php index 37daa9d76..8e0a4b4f6 100644 --- a/src/mcp-bundle/src/DependencyInjection/McpRegistryCompilerPass.php +++ b/src/mcp-bundle/src/DependencyInjection/McpRegistryCompilerPass.php @@ -38,5 +38,21 @@ public function process(ContainerBuilder $container): void new Reference($serviceId), ]); } + + // Find all services tagged as 'mcp.prompt' + $taggedServices = $container->findTaggedServiceIds('mcp.prompt'); + foreach ($taggedServices as $serviceId => $tags) { + $registryDefinition->addMethodCall('registerPromptService', [ + new Reference($serviceId), + ]); + } + + // Find all services tagged as 'mcp.resource' + $taggedServices = $container->findTaggedServiceIds('mcp.resource'); + foreach ($taggedServices as $serviceId => $tags) { + $registryDefinition->addMethodCall('registerResourceService', [ + new Reference($serviceId), + ]); + } } } diff --git a/src/mcp-bundle/src/Exception/InvalidArgumentException.php b/src/mcp-bundle/src/Exception/InvalidArgumentException.php new file mode 100644 index 000000000..5ce133055 --- /dev/null +++ b/src/mcp-bundle/src/Exception/InvalidArgumentException.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\McpBundle\Exception; + +/** + * @author Oskar Stark + */ +class InvalidArgumentException extends \InvalidArgumentException +{ +} \ No newline at end of file diff --git a/src/mcp-bundle/src/McpBundle.php b/src/mcp-bundle/src/McpBundle.php index f53e74272..1bfda8911 100644 --- a/src/mcp-bundle/src/McpBundle.php +++ b/src/mcp-bundle/src/McpBundle.php @@ -11,6 +11,10 @@ namespace Symfony\AI\McpBundle; +use Mcp\Capability\Prompt\IdentifierInterface as PromptIdentifierInterface; +use Mcp\Capability\Prompt\PromptGetterInterface; +use Mcp\Capability\Resource\IdentifierInterface as ResourceIdentifierInterface; +use Mcp\Capability\Resource\ResourceReaderInterface; use Mcp\Capability\Tool\IdentifierInterface; use Symfony\AI\McpBundle\Command\McpCommand; use Symfony\AI\McpBundle\Controller\McpController; @@ -49,6 +53,7 @@ public function loadExtension(array $config, ContainerConfigurator $container, C $builder->setParameter('mcp.discovery.enabled', $config['discovery']['enabled'] ?? true); $builder->setParameter('mcp.discovery.directories', $config['discovery']['directories'] ?? ['src']); $builder->setParameter('mcp.discovery.exclude', $config['discovery']['exclude'] ?? ['vendor', 'var', 'tests']); + $builder->setParameter('mcp.server_capabilities', $config['server_capabilities'] ?? []); if (isset($config['client_transports'])) { $this->configureClient($config['client_transports'], $builder); @@ -58,6 +63,26 @@ public function loadExtension(array $config, ContainerConfigurator $container, C ->registerForAutoconfiguration(IdentifierInterface::class) ->addTag('mcp.tool') ; + + $builder + ->registerForAutoconfiguration(PromptIdentifierInterface::class) + ->addTag('mcp.prompt') + ; + + $builder + ->registerForAutoconfiguration(PromptGetterInterface::class) + ->addTag('mcp.prompt') + ; + + $builder + ->registerForAutoconfiguration(ResourceIdentifierInterface::class) + ->addTag('mcp.resource') + ; + + $builder + ->registerForAutoconfiguration(ResourceReaderInterface::class) + ->addTag('mcp.resource') + ; } /** @@ -69,7 +94,6 @@ private function configureClient(array $transports, ContainerBuilder $container) return; } - if ($transports['stdio']) { $container->register('mcp.server.command', McpCommand::class) ->setArguments([ diff --git a/src/mcp-bundle/src/Registry/SymfonyRegistry.php b/src/mcp-bundle/src/Registry/SymfonyRegistry.php index 41a2786c0..52940eb8c 100644 --- a/src/mcp-bundle/src/Registry/SymfonyRegistry.php +++ b/src/mcp-bundle/src/Registry/SymfonyRegistry.php @@ -12,15 +12,25 @@ namespace Symfony\AI\McpBundle\Registry; use Mcp\Capability\Discovery\Discoverer; +use Mcp\Capability\Prompt\IdentifierInterface as PromptIdentifierInterface; +use Mcp\Capability\Prompt\MetadataInterface as PromptMetadataInterface; +use Mcp\Capability\Prompt\PromptGetterInterface; use Mcp\Capability\Registry; +use Mcp\Capability\Resource\IdentifierInterface as ResourceIdentifierInterface; +use Mcp\Capability\Resource\MetadataInterface as ResourceMetadataInterface; +use Mcp\Capability\Resource\ResourceReaderInterface; use Mcp\Capability\Tool\IdentifierInterface; use Mcp\Capability\Tool\MetadataInterface; use Mcp\Capability\Tool\ToolExecutorInterface; -use Mcp\Schema\Request\CallToolRequest; -use Mcp\Schema\Result\CallToolResult; +use Mcp\Schema\Prompt; +use Mcp\Schema\PromptArgument; +use Mcp\Schema\Resource; +use Mcp\Schema\ServerCapabilities; use Mcp\Schema\Tool; +use Mcp\Schema\ToolAnnotations; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; +use Symfony\AI\McpBundle\Exception\InvalidArgumentException; /** * Extended Registry that can register Symfony services as MCP tools. @@ -31,16 +41,46 @@ final class SymfonyRegistry extends Registry { private ?Discoverer $discoverer = null; + /** + * @param array $serverCapabilitiesConfig + */ public function __construct( private readonly LoggerInterface $logger = new NullLogger(), + private readonly array $serverCapabilitiesConfig = [], ) { parent::__construct(logger: $this->logger); $this->discoverer = new Discoverer($this, $this->logger); } + public function getCapabilities(): ServerCapabilities + { + $parentCapabilities = parent::getCapabilities(); + + if (empty($this->serverCapabilitiesConfig)) { + return $parentCapabilities; + } + + return new ServerCapabilities( + tools: $this->serverCapabilitiesConfig['tools'] ?? $parentCapabilities->tools, + toolsListChanged: $this->serverCapabilitiesConfig['tools_list_changed'] ?? $parentCapabilities->toolsListChanged, + resources: $this->serverCapabilitiesConfig['resources'] ?? $parentCapabilities->resources, + resourcesSubscribe: $this->serverCapabilitiesConfig['resources_subscribe'] ?? $parentCapabilities->resourcesSubscribe, + resourcesListChanged: $this->serverCapabilitiesConfig['resources_list_changed'] ?? $parentCapabilities->resourcesListChanged, + prompts: $this->serverCapabilitiesConfig['prompts'] ?? $parentCapabilities->prompts, + promptsListChanged: $this->serverCapabilitiesConfig['prompts_list_changed'] ?? $parentCapabilities->promptsListChanged, + logging: $this->serverCapabilitiesConfig['logging'] ?? $parentCapabilities->logging, + completions: $this->serverCapabilitiesConfig['completions'] ?? $parentCapabilities->completions, + experimental: $this->serverCapabilitiesConfig['experimental'] ?? $parentCapabilities->experimental, + ); + } + /** * Discover MCP tools using attributes in the specified directories. */ + /** + * @param array $directories + * @param array $excludeDirs + */ public function discoverTools(string $basePath, array $directories, array $excludeDirs = []): void { $this->discoverer?->discover($basePath, $directories, $excludeDirs); @@ -52,20 +92,84 @@ public function discoverTools(string $basePath, array $directories, array $exclu public function registerToolService(object $service): void { if (!$service instanceof IdentifierInterface) { - throw new \InvalidArgumentException('Tool service must implement IdentifierInterface'); + throw new InvalidArgumentException('Tool service must implement IdentifierInterface'); } $tool = new Tool( name: $service->getName(), description: $service instanceof MetadataInterface ? $service->getDescription() : null, inputSchema: $service instanceof MetadataInterface ? $service->getInputSchema() : null, + annotations: null, ); if (!$service instanceof ToolExecutorInterface) { - throw new \InvalidArgumentException('Service must implement ToolExecutorInterface'); + throw new InvalidArgumentException('Service must implement ToolExecutorInterface'); } // Register the tool with the service as the callable handler $this->registerTool($tool, [$service, 'call'], true); } + + /** + * Register a Symfony service that implements prompt interfaces. + */ + public function registerPromptService(object $service): void + { + if (!$service instanceof PromptIdentifierInterface) { + throw new InvalidArgumentException('Prompt service must implement PromptIdentifierInterface'); + } + + $arguments = null; + if ($service instanceof PromptMetadataInterface) { + $rawArguments = $service->getArguments(); + if (!empty($rawArguments)) { + // Convert raw arguments array to PromptArgument objects if needed + $arguments = array_map( + fn(array $arg) => new PromptArgument( + name: $arg['name'], + description: $arg['description'] ?? null, + required: $arg['required'] ?? false + ), + $rawArguments + ); + } + } + + $prompt = new Prompt( + name: $service->getName(), + description: $service instanceof PromptMetadataInterface ? $service->getDescription() : null, + arguments: $arguments, + ); + + if (!$service instanceof PromptGetterInterface) { + throw new InvalidArgumentException('Service must implement PromptGetterInterface'); + } + + // Register the prompt with the service as the callable handler + $this->registerPrompt($prompt, [$service, 'get'], []); + } + + /** + * Register a Symfony service that implements resource interfaces. + */ + public function registerResourceService(object $service): void + { + if (!$service instanceof ResourceIdentifierInterface) { + throw new InvalidArgumentException('Resource service must implement ResourceIdentifierInterface'); + } + + $resource = new Resource( + uri: $service->getUri(), + name: $service instanceof ResourceMetadataInterface ? $service->getName() : null, + description: $service instanceof ResourceMetadataInterface ? $service->getDescription() : null, + mimeType: $service instanceof ResourceMetadataInterface ? $service->getMimeType() : null, + ); + + if (!$service instanceof ResourceReaderInterface) { + throw new InvalidArgumentException('Service must implement ResourceReaderInterface'); + } + + // Register the resource with the service as the callable handler + $this->registerResource($resource, [$service, 'read'], true); + } } diff --git a/src/mcp-bundle/src/Registry/SymfonyRegistryFactory.php b/src/mcp-bundle/src/Registry/SymfonyRegistryFactory.php index 8e94563b1..3fd8710f7 100644 --- a/src/mcp-bundle/src/Registry/SymfonyRegistryFactory.php +++ b/src/mcp-bundle/src/Registry/SymfonyRegistryFactory.php @@ -21,18 +21,24 @@ */ final class SymfonyRegistryFactory { + /** + * @param array $serverCapabilitiesConfig + */ public function __construct( private readonly string $projectDir, private readonly bool $discoveryEnabled, + /** @var array */ private readonly array $discoveryDirectories, + /** @var array */ private readonly array $discoveryExclude, + private readonly array $serverCapabilitiesConfig = [], private readonly LoggerInterface $logger = new NullLogger(), ) { } public function create(): SymfonyRegistry { - $registry = new SymfonyRegistry($this->logger); + $registry = new SymfonyRegistry($this->logger, $this->serverCapabilitiesConfig); if ($this->discoveryEnabled) { $registry->discoverTools( diff --git a/src/mcp-bundle/tests/DependencyInjection/McpBundleTest.php b/src/mcp-bundle/tests/DependencyInjection/McpBundleTest.php index e22aa89a2..4cd05bb5e 100644 --- a/src/mcp-bundle/tests/DependencyInjection/McpBundleTest.php +++ b/src/mcp-bundle/tests/DependencyInjection/McpBundleTest.php @@ -12,9 +12,6 @@ namespace Symfony\AI\McpBundle\Tests\DependencyInjection; use Mcp\Capability\Tool\IdentifierInterface; -use Mcp\Server\MethodHandlerInterface as RequestHandlerInterface; -use Mcp\Server\NotificationHandlerInterface; -use Mcp\Server\RequestHandler\ListToolsHandler; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; @@ -124,7 +121,6 @@ public function testToolAutoconfiguration() $this->assertArrayHasKey('mcp.tool', $autoconfiguredInstances[IdentifierInterface::class]->getTags()); } - public function testDefaultPageSizeConfiguration() { $container = $this->buildContainer([]); @@ -165,6 +161,55 @@ public function testCoreServicesRegistered() $this->assertTrue($container->hasDefinition('mcp.server')); } + public function testDefaultServerCapabilitiesConfiguration() + { + $container = $this->buildContainer([]); + + // Test that default server capabilities are set + $serverCapabilities = $container->getParameter('mcp.server_capabilities'); + $this->assertSame([ + 'tools' => true, + 'tools_list_changed' => null, + 'resources' => null, + 'resources_subscribe' => false, + 'resources_list_changed' => null, + 'prompts' => null, + 'prompts_list_changed' => null, + 'logging' => false, + 'completions' => true, + 'experimental' => [], + ], $serverCapabilities); + } + + public function testCustomServerCapabilitiesConfiguration() + { + $container = $this->buildContainer([ + 'mcp' => [ + 'server_capabilities' => [ + 'tools' => false, + 'logging' => true, + 'completions' => false, + 'experimental' => ['custom_feature' => true], + ], + ], + ]); + + // Test that custom server capabilities are set + $serverCapabilities = $container->getParameter('mcp.server_capabilities'); + $this->assertSame([ + 'tools' => false, + 'logging' => true, + 'completions' => false, + 'experimental' => ['custom_feature' => true], + 'tools_list_changed' => null, + 'resources' => null, + 'resources_subscribe' => false, + 'resources_list_changed' => null, + 'prompts' => null, + 'prompts_list_changed' => null, + ], $serverCapabilities); + } + private function buildContainer(array $configuration): ContainerBuilder { $container = new ContainerBuilder(); diff --git a/src/mcp-bundle/tests/Registry/SymfonyRegistryTest.php b/src/mcp-bundle/tests/Registry/SymfonyRegistryTest.php new file mode 100644 index 000000000..396e8edb2 --- /dev/null +++ b/src/mcp-bundle/tests/Registry/SymfonyRegistryTest.php @@ -0,0 +1,84 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\McpBundle\Tests\Registry; + +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\TestCase; +use Psr\Log\NullLogger; +use Symfony\AI\McpBundle\Registry\SymfonyRegistry; + +#[CoversClass(SymfonyRegistry::class)] +class SymfonyRegistryTest extends TestCase +{ + public function testDefaultCapabilities() + { + $registry = new SymfonyRegistry(new NullLogger()); + + $capabilities = $registry->getCapabilities(); + + $this->assertTrue($capabilities->tools); + $this->assertFalse($capabilities->toolsListChanged); + $this->assertFalse($capabilities->resources); + $this->assertFalse($capabilities->resourcesSubscribe); + $this->assertFalse($capabilities->resourcesListChanged); + $this->assertFalse($capabilities->prompts); + $this->assertFalse($capabilities->promptsListChanged); + $this->assertFalse($capabilities->logging); + $this->assertTrue($capabilities->completions); + } + + public function testConfigurableCapabilities() + { + $config = [ + 'tools' => false, + 'logging' => true, + 'completions' => false, + 'experimental' => ['custom_feature' => true], + ]; + + $registry = new SymfonyRegistry(new NullLogger(), $config); + + $capabilities = $registry->getCapabilities(); + + $this->assertFalse($capabilities->tools); + $this->assertTrue($capabilities->logging); + $this->assertFalse($capabilities->completions); + $this->assertSame(['custom_feature' => true], $capabilities->experimental); + } + + public function testEmptyConfigurationFallsBackToDefault() + { + $registry = new SymfonyRegistry(new NullLogger(), []); + + $capabilities = $registry->getCapabilities(); + + // Should be same as default capabilities + $this->assertTrue($capabilities->tools); + $this->assertTrue($capabilities->completions); + } + + public function testPartialConfigurationMixesWithDefaults() + { + $config = [ + 'logging' => true, + // Other values should fall back to defaults + ]; + + $registry = new SymfonyRegistry(new NullLogger(), $config); + + $capabilities = $registry->getCapabilities(); + + $this->assertTrue($capabilities->tools); // Default + $this->assertTrue($capabilities->logging); // Configured + $this->assertTrue($capabilities->completions); // Default + } +} From 52bc4d237f95cc27eef8ad1b7925356572dc2726 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Mon, 8 Sep 2025 15:03:56 +0200 Subject: [PATCH 11/11] Add missing newlines at end of files - Add newlines to README.md, mcp-bundle/README.md, CurrentTimeTool.php, and InvalidArgumentException.php - Follows project coding standards requirement for file endings --- README.md | 1 + demo/src/MCP/Tools/CurrentTimeTool.php | 1 + src/mcp-bundle/README.md | 1 + src/mcp-bundle/src/Exception/InvalidArgumentException.php | 2 +- 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 661f0fe74..e7d47547a 100644 --- a/README.md +++ b/README.md @@ -41,3 +41,4 @@ For testing multi-modal features, the repository contains binary media content, * `tests/Fixture/image.jpg`: Chris F., Creative Commons, see [pexels.com](https://www.pexels.com/photo/blauer-und-gruner-elefant-mit-licht-1680755/) * `tests/Fixture/audio.mp3`: davidbain, Creative Commons, see [freesound.org](https://freesound.org/people/davidbain/sounds/136777/) * `tests/Fixture/document.pdf`: Chem8240ja, Public Domain, see [Wikipedia](https://en.m.wikipedia.org/wiki/File:Re_example.pdf) + diff --git a/demo/src/MCP/Tools/CurrentTimeTool.php b/demo/src/MCP/Tools/CurrentTimeTool.php index 447fee58b..35e986963 100644 --- a/demo/src/MCP/Tools/CurrentTimeTool.php +++ b/demo/src/MCP/Tools/CurrentTimeTool.php @@ -59,3 +59,4 @@ public function getInputSchema(): array ]; } } + diff --git a/src/mcp-bundle/README.md b/src/mcp-bundle/README.md index 83bac5290..cda8bcc22 100644 --- a/src/mcp-bundle/README.md +++ b/src/mcp-bundle/README.md @@ -25,3 +25,4 @@ https://github.com/symfony/ai to create issues or submit pull requests. - [Report issues](https://github.com/symfony/ai/issues) and [send Pull Requests](https://github.com/symfony/ai/pulls) in the [main Symfony AI repository](https://github.com/symfony/ai) + diff --git a/src/mcp-bundle/src/Exception/InvalidArgumentException.php b/src/mcp-bundle/src/Exception/InvalidArgumentException.php index 5ce133055..89a208f75 100644 --- a/src/mcp-bundle/src/Exception/InvalidArgumentException.php +++ b/src/mcp-bundle/src/Exception/InvalidArgumentException.php @@ -16,4 +16,4 @@ */ class InvalidArgumentException extends \InvalidArgumentException { -} \ No newline at end of file +}