From 8669a5d8e4d5885e91161864c1debd349b4f85bf Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Sun, 30 Mar 2025 21:57:59 +0200 Subject: [PATCH] feat: add support for client transports --- Makefile | 22 ++++++++++ README.md | 14 +++--- composer.json | 6 ++- src/Command/McpCommand.php | 31 +++++++++++++ src/Controller/McpController.php | 44 +++++++++++++++++++ src/DependencyInjection/Configuration.php | 51 +++++++++++++++++++++- src/DependencyInjection/McpExtension.php | 36 +++++++++++++++ src/Resources/config/routes.php | 15 +++++++ src/Resources/config/services.php | 53 +++++++++++++++++++++++ src/Resources/services.php | 9 ---- src/Routing/RouteLoader.php | 30 +++++++++++++ 11 files changed, 293 insertions(+), 18 deletions(-) create mode 100644 Makefile create mode 100644 src/Command/McpCommand.php create mode 100644 src/Controller/McpController.php create mode 100644 src/Resources/config/routes.php create mode 100644 src/Resources/config/services.php delete mode 100644 src/Resources/services.php create mode 100644 src/Routing/RouteLoader.php diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ba57c28 --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ +.PHONY: deps-stable deps-low cs rector phpstan tests coverage run-examples ci ci-stable ci-lowest + +deps-stable: + composer update --prefer-stable + +deps-low: + composer update --prefer-lowest + +cs: + PHP_CS_FIXER_IGNORE_ENV=true vendor/bin/php-cs-fixer fix --diff --verbose + +rector: + vendor/bin/rector + +phpstan: + vendor/bin/phpstan --memory-limit=-1 + +ci: ci-stable + +ci-stable: deps-stable rector cs phpstan + +ci-lowest: deps-low rector cs phpstan diff --git a/README.md b/README.md index 15af19b..ee344af 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,9 @@ configuration and usable in your chains. ### Act as Server -To use your application as a MCP server, exposing tools to clients like [Claude Desktop](https://claude.ai/download), -you need to configure in the `client` section the transports you want to use. You can use either STDIO or SSE. +To use your application as an MCP server, exposing tools to clients like [Claude Desktop](https://claude.ai/download), +you need to configure in the `client_transports` section the transports you want to expose to clients. +You can use either STDIO or SSE. ## Configuration @@ -46,9 +47,8 @@ mcp: sse: url: 'http://localhost:8000/sse' # URL to SSE endpoint of MCP server - # Configure this application to act as a MCP server - client: - transports: - stdio: true # Enable STDIO via command - sse: true # Enable Server-Sent Event via controller + # Configure this application to act as an MCP server + client_transports: + stdio: true # Enable STDIO via command + sse: true # Enable Server-Sent Event via controller ``` diff --git a/composer.json b/composer.json index ac61e4f..a34d369 100644 --- a/composer.json +++ b/composer.json @@ -12,8 +12,12 @@ "require": { "php-llm/mcp-sdk": "dev-main", "symfony/config": "^6.4 || ^7.0", + "symfony/console": "^6.4 || ^7.0", "symfony/dependency-injection": "^6.4 || ^7.0", - "symfony/framework-bundle": "^6.4 || ^7.0" + "symfony/framework-bundle": "^6.4 || ^7.0", + "symfony/http-foundation": "^6.4 || ^7.0", + "symfony/http-kernel": "^6.4 || ^7.0", + "symfony/routing": "^6.4 || ^7.0" }, "require-dev": { "php-cs-fixer/shim": "dev-master", diff --git a/src/Command/McpCommand.php b/src/Command/McpCommand.php new file mode 100644 index 0000000..1cfc6f7 --- /dev/null +++ b/src/Command/McpCommand.php @@ -0,0 +1,31 @@ +server->connect( + new SymfonyConsoleTransport($input, $output) + ); + + return Command::SUCCESS; + } +} diff --git a/src/Controller/McpController.php b/src/Controller/McpController.php new file mode 100644 index 0000000..3457943 --- /dev/null +++ b/src/Controller/McpController.php @@ -0,0 +1,44 @@ +urlGenerator->generate('_mcp_messages', ['id' => $id], UrlGeneratorInterface::ABSOLUTE_URL); + $transport = new StreamTransport($endpoint, $this->store, $id); + + return new StreamedResponse(fn () => $this->server->connect($transport), headers: [ + 'Content-Type' => 'text/event-stream', + 'Cache-Control' => 'no-cache', + 'X-Accel-Buffering' => 'no', + ]); + } + + public function messages(Request $request, Uuid $id): Response + { + $this->store->push($id, $request->getContent()); + + return new Response(); + } +} diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 68e30bd..b6cb3ad 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -14,7 +14,56 @@ public function getConfigTreeBuilder(): TreeBuilder $treeBuilder = new TreeBuilder('mcp'); $rootNode = $treeBuilder->getRootNode(); - //$rootNode + $rootNode + ->children() + ->scalarNode('app')->defaultValue('app')->end() + ->scalarNode('version')->defaultValue('0.0.1')->end() + // ->arrayNode('servers') + // ->useAttributeAsKey('name') + // ->arrayPrototype() + // ->children() + // ->enumNode('transport') + // ->values(['stdio', 'sse']) + // ->isRequired() + // ->end() + // ->arrayNode('stdio') + // ->children() + // ->scalarNode('command')->isRequired()->end() + // ->arrayNode('arguments') + // ->scalarPrototype()->end() + // ->defaultValue([]) + // ->end() + // ->end() + // ->end() + // ->arrayNode('sse') + // ->children() + // ->scalarNode('url')->isRequired()->end() + // ->end() + // ->end() + // ->end() + // ->validate() + // ->ifTrue(function ($v) { + // if ('stdio' === $v['transport'] && !isset($v['stdio'])) { + // return true; + // } + // if ('sse' === $v['transport'] && !isset($v['sse'])) { + // return true; + // } + // + // return false; + // }) + // ->thenInvalid('When transport is "%s", you must configure the corresponding section.') + // ->end() + // ->end() + // ->end() + ->arrayNode('client_transports') + ->children() + ->booleanNode('stdio')->defaultFalse()->end() + ->booleanNode('sse')->defaultFalse()->end() + ->end() + ->end() + ->end() + ; return $treeBuilder; } diff --git a/src/DependencyInjection/McpExtension.php b/src/DependencyInjection/McpExtension.php index e0d6c5a..a083d6f 100644 --- a/src/DependencyInjection/McpExtension.php +++ b/src/DependencyInjection/McpExtension.php @@ -4,6 +4,9 @@ namespace PhpLlm\McpBundle\DependencyInjection; +use PhpLlm\McpBundle\Command\McpCommand; +use PhpLlm\McpBundle\Controller\McpController; +use PhpLlm\McpBundle\Routing\RouteLoader; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\Extension; @@ -19,5 +22,38 @@ public function load(array $configs, ContainerBuilder $container): void $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs); + $container->setParameter('mcp.app', $config['app']); + $container->setParameter('mcp.version', $config['version']); + + if (isset($config['client_transports'])) { + $this->configureClient($config['client_transports'], $container); + } + } + + /** + * @param array{stdio: bool, sse: bool} $transports + */ + private function configureClient(array $transports, ContainerBuilder $container): void + { + if (!$transports['stdio'] && !$transports['sse']) { + return; + } + + if ($transports['stdio']) { + $container->register('mcp.server.command', McpCommand::class) + ->setAutowired(true) + ->addTag('console.command'); + } + + if ($transports['sse']) { + $container->register('mcp.server.controller', McpController::class) + ->setAutowired(true) + ->setPublic(true) + ->addTag('controller.service_arguments'); + } + + $container->register('mcp.server.route_loader', RouteLoader::class) + ->setArgument('$sseTransportEnabled', $transports['sse']) + ->addTag('routing.route_loader'); } } diff --git a/src/Resources/config/routes.php b/src/Resources/config/routes.php new file mode 100644 index 0000000..3559008 --- /dev/null +++ b/src/Resources/config/routes.php @@ -0,0 +1,15 @@ +add('_mcp_sse', '/sse') + ->controller([McpController::class, 'sse']) + ->methods(['GET']) + ; + $routes->add('_mcp_messages', '/messages/{id}') + ->controller([McpController::class, 'messages']) + ->methods(['POST']) + ; +}; diff --git a/src/Resources/config/services.php b/src/Resources/config/services.php new file mode 100644 index 0000000..23ddcfc --- /dev/null +++ b/src/Resources/config/services.php @@ -0,0 +1,53 @@ +services() + ->defaults() + ->autowire() + ->autoconfigure() + ->instanceof(NotificationHandler::class) + ->tag('mcp.server.notification_handler') + ->instanceof(RequestHandler::class) + ->tag('mcp.server.request_handler') + + ->set(InitializedHandler::class) + ->set(InitializeHandler::class) + ->args([ + '$name' => param('mcp.app'), + '$version' => param('mcp.version'), + ]) + ->set(PingHandler::class) + ->set(ToolCallHandler::class) + ->set(ToolListHandler::class) + + ->set('mcp.message_factory', Factory::class) + ->set('mcp.server.json_rpc', JsonRpcHandler::class) + ->args([ + '$messageFactory' => service('mcp.message_factory'), + '$requestHandlers' => tagged_iterator('mcp.server.request_handler'), + '$notificationHandlers' => tagged_iterator('mcp.server.notification_handler'), + ]) + ->set('mcp.server', Server::class) + ->args([ + '$jsonRpcHandler' => service('mcp.server.json_rpc'), + ]) + ->alias(Server::class, 'mcp.server') + ->set(CachePoolStore::class) + ; +}; diff --git a/src/Resources/services.php b/src/Resources/services.php deleted file mode 100644 index c813853..0000000 --- a/src/Resources/services.php +++ /dev/null @@ -1,9 +0,0 @@ -sseTransportEnabled) { + return new RouteCollection(); + } + + $collection = new RouteCollection(); + + $collection->add('_mcp_sse', new Route('/_mcp/sse', ['_controller' => ['mcp.server.controller', 'sse']], methods: ['GET'])); + $collection->add('_mcp_messages', new Route('/_mcp/messages/{id}', ['_controller' => ['mcp.server.controller', 'messages']], methods: ['POST'])); + + return $collection; + } +}