Skip to content

Commit 51463c2

Browse files
authored
Merge pull request #1 from php-llm/feat-client-transports
feat: add support for client transports
2 parents 55d1842 + 8669a5d commit 51463c2

File tree

11 files changed

+293
-18
lines changed

11 files changed

+293
-18
lines changed

Makefile

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
.PHONY: deps-stable deps-low cs rector phpstan tests coverage run-examples ci ci-stable ci-lowest
2+
3+
deps-stable:
4+
composer update --prefer-stable
5+
6+
deps-low:
7+
composer update --prefer-lowest
8+
9+
cs:
10+
PHP_CS_FIXER_IGNORE_ENV=true vendor/bin/php-cs-fixer fix --diff --verbose
11+
12+
rector:
13+
vendor/bin/rector
14+
15+
phpstan:
16+
vendor/bin/phpstan --memory-limit=-1
17+
18+
ci: ci-stable
19+
20+
ci-stable: deps-stable rector cs phpstan
21+
22+
ci-lowest: deps-low rector cs phpstan

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

+36
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,38 @@ 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+
/**
34+
* @param array{stdio: bool, sse: bool} $transports
35+
*/
36+
private function configureClient(array $transports, ContainerBuilder $container): void
37+
{
38+
if (!$transports['stdio'] && !$transports['sse']) {
39+
return;
40+
}
41+
42+
if ($transports['stdio']) {
43+
$container->register('mcp.server.command', McpCommand::class)
44+
->setAutowired(true)
45+
->addTag('console.command');
46+
}
47+
48+
if ($transports['sse']) {
49+
$container->register('mcp.server.controller', McpController::class)
50+
->setAutowired(true)
51+
->setPublic(true)
52+
->addTag('controller.service_arguments');
53+
}
54+
55+
$container->register('mcp.server.route_loader', RouteLoader::class)
56+
->setArgument('$sseTransportEnabled', $transports['sse'])
57+
->addTag('routing.route_loader');
2258
}
2359
}

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): void {
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

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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\NotificationHandler\InitializedHandler;
12+
use PhpLlm\McpSdk\Server\RequestHandler;
13+
use PhpLlm\McpSdk\Server\RequestHandler\InitializeHandler;
14+
use PhpLlm\McpSdk\Server\RequestHandler\PingHandler;
15+
use PhpLlm\McpSdk\Server\RequestHandler\ToolCallHandler;
16+
use PhpLlm\McpSdk\Server\RequestHandler\ToolListHandler;
17+
use PhpLlm\McpSdk\Server\Transport\Sse\Store\CachePoolStore;
18+
19+
return static function (ContainerConfigurator $container): void {
20+
$container->services()
21+
->defaults()
22+
->autowire()
23+
->autoconfigure()
24+
->instanceof(NotificationHandler::class)
25+
->tag('mcp.server.notification_handler')
26+
->instanceof(RequestHandler::class)
27+
->tag('mcp.server.request_handler')
28+
29+
->set(InitializedHandler::class)
30+
->set(InitializeHandler::class)
31+
->args([
32+
'$name' => param('mcp.app'),
33+
'$version' => param('mcp.version'),
34+
])
35+
->set(PingHandler::class)
36+
->set(ToolCallHandler::class)
37+
->set(ToolListHandler::class)
38+
39+
->set('mcp.message_factory', Factory::class)
40+
->set('mcp.server.json_rpc', JsonRpcHandler::class)
41+
->args([
42+
'$messageFactory' => service('mcp.message_factory'),
43+
'$requestHandlers' => tagged_iterator('mcp.server.request_handler'),
44+
'$notificationHandlers' => tagged_iterator('mcp.server.notification_handler'),
45+
])
46+
->set('mcp.server', Server::class)
47+
->args([
48+
'$jsonRpcHandler' => service('mcp.server.json_rpc'),
49+
])
50+
->alias(Server::class, 'mcp.server')
51+
->set(CachePoolStore::class)
52+
;
53+
};

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)