Skip to content

Commit cd6ae2a

Browse files
committed
feat: add support for client transports
1 parent 55d1842 commit cd6ae2a

File tree

10 files changed

+263
-18
lines changed

10 files changed

+263
-18
lines changed

README.md

+7-7
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ configuration and usable in your chains.
2626

2727
### Act as Server
2828

29-
To use your application as a MCP server, exposing tools to clients like [Claude Desktop](https://claude.ai/download),
30-
you need to configure in the `client` section the transports you want to use. You can use either STDIO or SSE.
29+
To use your application as an MCP server, exposing tools to clients like [Claude Desktop](https://claude.ai/download),
30+
you need to configure in the `client_transports` section the transports you want to expose to clients.
31+
You can use either STDIO or SSE.
3132

3233
## Configuration
3334

@@ -46,9 +47,8 @@ mcp:
4647
sse:
4748
url: 'http://localhost:8000/sse' # URL to SSE endpoint of MCP server
4849

49-
# Configure this application to act as a MCP server
50-
client:
51-
transports:
52-
stdio: true # Enable STDIO via command
53-
sse: true # Enable Server-Sent Event via controller
50+
# Configure this application to act as an MCP server
51+
client_transports:
52+
stdio: true # Enable STDIO via command
53+
sse: true # Enable Server-Sent Event via controller
5454
```

composer.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,12 @@
1212
"require": {
1313
"php-llm/mcp-sdk": "dev-main",
1414
"symfony/config": "^6.4 || ^7.0",
15+
"symfony/console": "^6.4 || ^7.0",
1516
"symfony/dependency-injection": "^6.4 || ^7.0",
16-
"symfony/framework-bundle": "^6.4 || ^7.0"
17+
"symfony/framework-bundle": "^6.4 || ^7.0",
18+
"symfony/http-foundation": "^6.4 || ^7.0",
19+
"symfony/http-kernel": "^6.4 || ^7.0",
20+
"symfony/routing": "^6.4 || ^7.0"
1721
},
1822
"require-dev": {
1923
"php-cs-fixer/shim": "dev-master",

src/Command/McpCommand.php

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\McpBundle\Command;
6+
7+
use PhpLlm\McpSdk\Server;
8+
use PhpLlm\McpSdk\Server\Transport\Stdio\SymfonyConsoleTransport;
9+
use Symfony\Component\Console\Attribute\AsCommand;
10+
use Symfony\Component\Console\Command\Command;
11+
use Symfony\Component\Console\Input\InputInterface;
12+
use Symfony\Component\Console\Output\OutputInterface;
13+
14+
#[AsCommand('mcp:server', 'Starts an MCP server')]
15+
class McpCommand extends Command
16+
{
17+
public function __construct(
18+
private readonly Server $server,
19+
) {
20+
parent::__construct();
21+
}
22+
23+
protected function execute(InputInterface $input, OutputInterface $output): int
24+
{
25+
$this->server->connect(
26+
new SymfonyConsoleTransport($input, $output)
27+
);
28+
29+
return Command::SUCCESS;
30+
}
31+
}

src/Controller/McpController.php

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\McpBundle\Controller;
6+
7+
use PhpLlm\McpSdk\Server;
8+
use PhpLlm\McpSdk\Server\Transport\Sse\Store\CachePoolStore;
9+
use PhpLlm\McpSdk\Server\Transport\Sse\StreamTransport;
10+
use Symfony\Component\HttpFoundation\Request;
11+
use Symfony\Component\HttpFoundation\Response;
12+
use Symfony\Component\HttpFoundation\StreamedResponse;
13+
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
14+
use Symfony\Component\Uid\Uuid;
15+
16+
final readonly class McpController
17+
{
18+
public function __construct(
19+
private Server $server,
20+
private CachePoolStore $store,
21+
private UrlGeneratorInterface $urlGenerator,
22+
) {
23+
}
24+
25+
public function sse(): StreamedResponse
26+
{
27+
$id = Uuid::v4();
28+
$endpoint = $this->urlGenerator->generate('_mcp_messages', ['id' => $id], UrlGeneratorInterface::ABSOLUTE_URL);
29+
$transport = new StreamTransport($endpoint, $this->store, $id);
30+
31+
return new StreamedResponse(fn () => $this->server->connect($transport), headers: [
32+
'Content-Type' => 'text/event-stream',
33+
'Cache-Control' => 'no-cache',
34+
'X-Accel-Buffering' => 'no',
35+
]);
36+
}
37+
38+
public function messages(Request $request, Uuid $id): Response
39+
{
40+
$this->store->push($id, $request->getContent());
41+
42+
return new Response();
43+
}
44+
}

src/DependencyInjection/Configuration.php

+50-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,56 @@ public function getConfigTreeBuilder(): TreeBuilder
1414
$treeBuilder = new TreeBuilder('mcp');
1515
$rootNode = $treeBuilder->getRootNode();
1616

17-
//$rootNode
17+
$rootNode
18+
->children()
19+
->scalarNode('app')->defaultValue('app')->end()
20+
->scalarNode('version')->defaultValue('0.0.1')->end()
21+
// ->arrayNode('servers')
22+
// ->useAttributeAsKey('name')
23+
// ->arrayPrototype()
24+
// ->children()
25+
// ->enumNode('transport')
26+
// ->values(['stdio', 'sse'])
27+
// ->isRequired()
28+
// ->end()
29+
// ->arrayNode('stdio')
30+
// ->children()
31+
// ->scalarNode('command')->isRequired()->end()
32+
// ->arrayNode('arguments')
33+
// ->scalarPrototype()->end()
34+
// ->defaultValue([])
35+
// ->end()
36+
// ->end()
37+
// ->end()
38+
// ->arrayNode('sse')
39+
// ->children()
40+
// ->scalarNode('url')->isRequired()->end()
41+
// ->end()
42+
// ->end()
43+
// ->end()
44+
// ->validate()
45+
// ->ifTrue(function ($v) {
46+
// if ('stdio' === $v['transport'] && !isset($v['stdio'])) {
47+
// return true;
48+
// }
49+
// if ('sse' === $v['transport'] && !isset($v['sse'])) {
50+
// return true;
51+
// }
52+
//
53+
// return false;
54+
// })
55+
// ->thenInvalid('When transport is "%s", you must configure the corresponding section.')
56+
// ->end()
57+
// ->end()
58+
// ->end()
59+
->arrayNode('client_transports')
60+
->children()
61+
->booleanNode('stdio')->defaultFalse()->end()
62+
->booleanNode('sse')->defaultFalse()->end()
63+
->end()
64+
->end()
65+
->end()
66+
;
1867

1968
return $treeBuilder;
2069
}

src/DependencyInjection/McpExtension.php

+33
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
namespace PhpLlm\McpBundle\DependencyInjection;
66

7+
use PhpLlm\McpBundle\Command\McpCommand;
8+
use PhpLlm\McpBundle\Controller\McpController;
9+
use PhpLlm\McpBundle\Routing\RouteLoader;
710
use Symfony\Component\Config\FileLocator;
811
use Symfony\Component\DependencyInjection\ContainerBuilder;
912
use Symfony\Component\DependencyInjection\Extension\Extension;
@@ -19,5 +22,35 @@ public function load(array $configs, ContainerBuilder $container): void
1922
$configuration = new Configuration();
2023
$config = $this->processConfiguration($configuration, $configs);
2124

25+
$container->setParameter('mcp.app', $config['app']);
26+
$container->setParameter('mcp.version', $config['version']);
27+
28+
if (isset($config['client_transports'])) {
29+
$this->configureClient($config['client_transports'], $container);
30+
}
31+
}
32+
33+
private function configureClient($transports, ContainerBuilder $container): void
34+
{
35+
if (!$transports['stdio'] && !$transports['sse']) {
36+
return;
37+
}
38+
39+
if ($transports['stdio']) {
40+
$container->register('mcp.server.command', McpCommand::class)
41+
->setAutowired(true)
42+
->addTag('console.command');
43+
}
44+
45+
if ($transports['sse']) {
46+
$container->register('mcp.server.controller', McpController::class)
47+
->setAutowired(true)
48+
->setPublic(true)
49+
->addTag('controller.service_arguments');
50+
}
51+
52+
$container->register('mcp.server.route_loader', RouteLoader::class)
53+
->setArgument('$sseTransportEnabled', $transports['sse'])
54+
->addTag('routing.route_loader');
2255
}
2356
}

src/Resources/config/routes.php

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
use PhpLlm\McpBundle\Controller\McpController;
4+
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
5+
6+
return function (RoutingConfigurator $routes) {
7+
$routes->add('_mcp_sse', '/sse')
8+
->controller([McpController::class, 'sse'])
9+
->methods(['GET'])
10+
;
11+
$routes->add('_mcp_messages', '/messages/{id}')
12+
->controller([McpController::class, 'messages'])
13+
->methods(['POST'])
14+
;
15+
};

src/Resources/config/services.php

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
6+
7+
use PhpLlm\McpSdk\Message\Factory;
8+
use PhpLlm\McpSdk\Server;
9+
use PhpLlm\McpSdk\Server\JsonRpcHandler;
10+
use PhpLlm\McpSdk\Server\NotificationHandler;
11+
use PhpLlm\McpSdk\Server\RequestHandler;
12+
use PhpLlm\McpSdk\Server\Transport\Sse\Store\CachePoolStore;
13+
14+
return static function (ContainerConfigurator $container): void {
15+
$container->services()
16+
->defaults()
17+
->autowire()
18+
->autoconfigure()
19+
->instanceof(NotificationHandler::class)
20+
->tag('mcp.server.notification_handler')
21+
->instanceof(RequestHandler::class)
22+
->tag('mcp.server.request_handler')
23+
24+
->set(NotificationHandler\InitializedHandler::class)
25+
->set(RequestHandler\InitializeHandler::class)
26+
->args([
27+
'$name' => param('mcp.app'),
28+
'$version' => param('mcp.version'),
29+
])
30+
->set(RequestHandler\PingHandler::class)
31+
->set(RequestHandler\ToolCallHandler::class)
32+
->set(RequestHandler\ToolListHandler::class)
33+
34+
->set('mcp.message_factory', Factory::class)
35+
->set('mcp.server.json_rpc', JsonRpcHandler::class)
36+
->args([
37+
'$messageFactory' => service('mcp.message_factory'),
38+
'$requestHandlers' => tagged_iterator('mcp.server.request_handler'),
39+
'$notificationHandlers' => tagged_iterator('mcp.server.notification_handler'),
40+
])
41+
->set('mcp.server', Server::class)
42+
->args([
43+
'$jsonRpcHandler' => service('mcp.server.json_rpc'),
44+
])
45+
->alias(Server::class, 'mcp.server')
46+
->set(CachePoolStore::class)
47+
;
48+
};

src/Resources/services.php

-9
This file was deleted.

src/Routing/RouteLoader.php

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\McpBundle\Routing;
6+
7+
use Symfony\Component\Routing\Route;
8+
use Symfony\Component\Routing\RouteCollection;
9+
10+
final readonly class RouteLoader
11+
{
12+
public function __construct(
13+
private bool $sseTransportEnabled,
14+
) {
15+
}
16+
17+
public function __invoke(): RouteCollection
18+
{
19+
if (!$this->sseTransportEnabled) {
20+
return new RouteCollection();
21+
}
22+
23+
$collection = new RouteCollection();
24+
25+
$collection->add('_mcp_sse', new Route('/_mcp/sse', ['_controller' => ['mcp.server.controller', 'sse']], methods: ['GET']));
26+
$collection->add('_mcp_messages', new Route('/_mcp/messages/{id}', ['_controller' => ['mcp.server.controller', 'messages']], methods: ['POST']));
27+
28+
return $collection;
29+
}
30+
}

0 commit comments

Comments
 (0)