Skip to content

Commit 6eeba0a

Browse files
author
klapaudius
committed
Introduce conditional route loading and custom MCP route loader
- Added `ConditionalRoutePass` to register routes dynamically based on enabled server providers. - Introduced `McpRouteLoader` for conditional route loading and streamlined routing configuration. - Updated service definitions to include the new route loader and expose relevant controller services conditionally. - Adjusted tests to verify dynamic route behavior and ensure proper removal of disabled controllers. - Simplified `routes.php` by utilizing the custom route loader.
1 parent 4f8dee0 commit 6eeba0a

File tree

11 files changed

+348
-27
lines changed

11 files changed

+348
-27
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace KLP\KlpMcpServer\DependencyInjection\CompilerPass;
6+
7+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
8+
use Symfony\Component\DependencyInjection\ContainerBuilder;
9+
use Symfony\Component\Routing\RouteCollection;
10+
11+
/**
12+
* Compiler pass to conditionally register routes based on enabled server providers
13+
*/
14+
class ConditionalRoutePass implements CompilerPassInterface
15+
{
16+
public function process(ContainerBuilder $container): void
17+
{
18+
if (!$container->hasParameter('klp_mcp_server.providers')) {
19+
return;
20+
}
21+
22+
$enabledProviders = $container->getParameter('klp_mcp_server.providers');
23+
$defaultPath = $container->getParameter('klp_mcp_server.default_path');
24+
$routeLoader = $container->findDefinition('klp_mcp_server.route_loader');
25+
26+
// Pass the enabled providers and default path to the route loader
27+
$routeLoader->addMethodCall('setEnabledProviders', [$enabledProviders]);
28+
$routeLoader->addMethodCall('setDefaultPath', [$defaultPath]);
29+
}
30+
}

src/DependencyInjection/KlpMcpServerExtension.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,25 @@ public function load(array $configs, ContainerBuilder $container): void
4343
$container->setParameter('klp_mcp_server.resources', $config['resources'] ?? []);
4444
// Set parameters for resource templates
4545
$container->setParameter('klp_mcp_server.resources_templates', $config['resources_templates'] ?? []);
46+
47+
// Conditionally remove controller services based on enabled providers
48+
$this->removeDisabledControllers($container, $providers);
49+
}
50+
51+
private function removeDisabledControllers(ContainerBuilder $container, array $enabledProviders): void
52+
{
53+
// Remove SSE controllers if SSE provider is not enabled
54+
if (!in_array('klp_mcp_server.provider.sse', $enabledProviders, true)) {
55+
$container->removeDefinition('KLP\KlpMcpServer\Controllers\SseController');
56+
$container->removeAlias('klp_mcp_server.controller.sse');
57+
$container->removeDefinition('KLP\KlpMcpServer\Controllers\MessageController');
58+
$container->removeAlias('klp_mcp_server.controller.message');
59+
}
60+
61+
// Remove StreamableHTTP controller if StreamableHTTP provider is not enabled
62+
if (!in_array('klp_mcp_server.provider.streamable_http', $enabledProviders, true)) {
63+
$container->removeDefinition('KLP\KlpMcpServer\Controllers\StreamableHttpController');
64+
$container->removeAlias('klp_mcp_server.controller.streamable_http');
65+
}
4666
}
4767
}

src/KlpMcpServerBundle.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use KLP\KlpMcpServer\DependencyInjection\CompilerPass\ResourcesDefinitionCompilerPass;
66
use KLP\KlpMcpServer\DependencyInjection\CompilerPass\ToolsDefinitionCompilerPass;
7+
use KLP\KlpMcpServer\DependencyInjection\CompilerPass\ConditionalRoutePass;
78
use Symfony\Component\DependencyInjection\ContainerBuilder;
89
use Symfony\Component\HttpKernel\Bundle\Bundle;
910

@@ -14,5 +15,6 @@ public function build(ContainerBuilder $container): void
1415
parent::build($container);
1516
$container->addCompilerPass(new ToolsDefinitionCompilerPass);
1617
$container->addCompilerPass(new ResourcesDefinitionCompilerPass);
18+
$container->addCompilerPass(new ConditionalRoutePass);
1719
}
1820
}

src/Resources/config/routes.php

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,14 @@
11
<?php
22

3-
use KLP\KlpMcpServer\Controllers\MessageController;
4-
use KLP\KlpMcpServer\Controllers\SseController;
5-
use KLP\KlpMcpServer\Controllers\StreamableHttpController;
63
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
74

85
/**
9-
* Configures routes for the application.
6+
* Configures routes for the application using a custom route loader.
107
*
118
* @param RoutingConfigurator $routes The routing configurator used to define routes.
129
* @return void
1310
*/
1411
return function (RoutingConfigurator $routes) {
15-
$defaultPath = '%klp_mcp_server.default_path%';
16-
17-
// SSE (Procole version: 2024-11-05)
18-
$routes->add('sse_route', "/$defaultPath/sse")
19-
->controller([SseController::class, 'handle'])
20-
->methods(['GET', 'POST']);
21-
22-
$routes->add('message_route', "/$defaultPath/messages")
23-
->controller([MessageController::class, 'handle'])
24-
->methods(['POST']);
25-
26-
// Streamable Http (Procole version: 2025-03-26)
27-
$routes->add('streamable_http_route', "/$defaultPath")
28-
->controller([StreamableHttpController::class, 'handle'])
29-
->methods(['GET', 'POST']);
12+
// Use the custom route loader to conditionally load routes based on enabled providers
13+
$routes->import('.', 'mcp');
3014
};

src/Resources/config/services.xml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,20 @@
88
<argument type="service" id="klp_mcp_server.server" />
99
<tag name="controller.service_arguments" />
1010
</service>
11-
<service id="klp_mcp_server.controller.sse" alias="KLP\KlpMcpServer\Controllers\SseController" />
11+
<service id="klp_mcp_server.controller.sse" alias="KLP\KlpMcpServer\Controllers\SseController" public="true" />
1212
<service id="KLP\KlpMcpServer\Controllers\StreamableHttpController" class="KLP\KlpMcpServer\Controllers\StreamableHttpController">
1313
<argument type="service" id="klp_mcp_server.server" />
1414
<argument type="service" id="logger" on-invalid="null" />
1515
<tag name="controller.service_arguments" />
1616
</service>
17-
<service id="klp_mcp_server.controller.steamable_http" alias="KLP\KlpMcpServer\Controllers\StreamableHttpController" />
17+
<service id="klp_mcp_server.controller.streamable_http" alias="KLP\KlpMcpServer\Controllers\StreamableHttpController" public="true" />
1818

1919
<service id="KLP\KlpMcpServer\Controllers\MessageController" class="KLP\KlpMcpServer\Controllers\MessageController">
2020
<argument type="service" id="klp_mcp_server.server" />
2121
<argument type="service" id="logger" on-invalid="null" />
2222
<tag name="controller.service_arguments" />
2323
</service>
24-
<service id="klp_mcp_server.controller.message" alias="KLP\KlpMcpServer\Controllers\MessageController" />
24+
<service id="klp_mcp_server.controller.message" alias="KLP\KlpMcpServer\Controllers\MessageController" public="true" />
2525

2626
<service id="KLP\KlpMcpServer\Server\ServerCapabilitiesInterface" class="KLP\KlpMcpServer\Server\ServerCapabilities" />
2727
<service id="klp_mcp_server.command.make" class="KLP\KlpMcpServer\Command\MakeMcpToolCommand">
@@ -33,6 +33,10 @@
3333
<tag name="console.command" />
3434
</service>
3535

36+
<service id="klp_mcp_server.route_loader" class="KLP\KlpMcpServer\Routing\Loader\McpRouteLoader">
37+
<tag name="routing.loader" />
38+
</service>
39+
3640
<service id="klp_mcp_server.adapter.factory" class="KLP\KlpMcpServer\Transports\SseAdapters\SseAdapterFactory">
3741
<argument type="service" id="service_container" />
3842
<argument type="service" id="logger" on-invalid="null" />

src/Routing/Loader/McpRouteLoader.php

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace KLP\KlpMcpServer\Routing\Loader;
6+
7+
use Symfony\Component\Config\Loader\LoaderInterface;
8+
use Symfony\Component\Config\Loader\LoaderResolverInterface;
9+
use Symfony\Component\Routing\Route;
10+
use Symfony\Component\Routing\RouteCollection;
11+
12+
/**
13+
* Custom route loader that conditionally loads routes based on enabled server providers
14+
*/
15+
class McpRouteLoader implements LoaderInterface
16+
{
17+
private array $enabledProviders = [];
18+
private string $defaultPath = 'mcp';
19+
private bool $loaded = false;
20+
21+
public function setEnabledProviders(array $providers): void
22+
{
23+
$this->enabledProviders = $providers;
24+
}
25+
26+
public function setDefaultPath(string $defaultPath): void
27+
{
28+
$this->defaultPath = $defaultPath;
29+
}
30+
31+
public function load($resource, string|null $type = null): RouteCollection
32+
{
33+
if ($this->loaded) {
34+
throw new \RuntimeException('MCP routes already loaded');
35+
}
36+
37+
$routes = new RouteCollection();
38+
39+
// StreamableHTTP routes
40+
if ($this->isProviderEnabled('streamable_http')) {
41+
$routes->add('klp_mcp_server_streamable_http', new Route(
42+
'/' . $this->defaultPath,
43+
['_controller' => 'klp_mcp_server.controller.streamable_http::handle']
44+
));
45+
}
46+
47+
// SSE routes
48+
if ($this->isProviderEnabled('sse')) {
49+
$routes->add('klp_mcp_server_sse', new Route(
50+
'/' . $this->defaultPath . '/sse',
51+
['_controller' => 'klp_mcp_server.controller.sse::handle']
52+
));
53+
54+
$routes->add('klp_mcp_server_sse_message', new Route(
55+
'/' . $this->defaultPath . '/messages',
56+
['_controller' => 'klp_mcp_server.controller.message::handle'],
57+
[],
58+
[],
59+
'',
60+
[],
61+
['POST']
62+
));
63+
}
64+
65+
$this->loaded = true;
66+
return $routes;
67+
}
68+
69+
public function supports($resource, string|null $type = null): bool
70+
{
71+
return 'mcp' === $type;
72+
}
73+
74+
public function getResolver(): LoaderResolverInterface
75+
{
76+
// Return a dummy resolver as it's required by the interface
77+
return new class implements LoaderResolverInterface {
78+
public function resolve($resource, string|null $type = null): LoaderInterface|false
79+
{
80+
return false;
81+
}
82+
83+
public function addLoader(LoaderInterface $loader): void
84+
{
85+
// Not needed
86+
}
87+
88+
public function getLoaders(): array
89+
{
90+
return [];
91+
}
92+
};
93+
}
94+
95+
public function setResolver(LoaderResolverInterface $resolver): void
96+
{
97+
// Not needed for this implementation
98+
}
99+
100+
private function isProviderEnabled(string $provider): bool
101+
{
102+
return in_array('klp_mcp_server.provider.' . $provider, $this->enabledProviders, true);
103+
}
104+
}

src/Transports/SseTransport.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ public function pushMessage(string $clientId, array $message): void
9898

9999
protected function getEndpoint(string $sessionId): string
100100
{
101-
return $this->router->generate('message_route', ['sessionId' => $sessionId]);
101+
return $this->router->generate('klp_mcp_server_sse_message', ['sessionId' => $sessionId]);
102102
}
103103

104104
/**
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace KLP\KlpMcpServer\Tests\DependencyInjection;
6+
7+
use KLP\KlpMcpServer\DependencyInjection\CompilerPass\ConditionalRoutePass;
8+
use PHPUnit\Framework\TestCase;
9+
use Symfony\Component\DependencyInjection\ContainerBuilder;
10+
11+
class ServerProviderIntegrationTest extends TestCase
12+
{
13+
public function testCompilerPassSetsEnabledProviders(): void
14+
{
15+
$container = new ContainerBuilder();
16+
17+
// Set up the providers parameter
18+
$container->setParameter('klp_mcp_server.providers', [
19+
'klp_mcp_server.provider.sse'
20+
]);
21+
$container->setParameter('klp_mcp_server.default_path', 'mcp');
22+
23+
// Register a mock route loader service
24+
$routeLoader = $container->register('klp_mcp_server.route_loader', \stdClass::class);
25+
26+
// Apply the compiler pass
27+
$pass = new ConditionalRoutePass();
28+
$pass->process($container);
29+
30+
// Verify the method calls were added
31+
$methodCalls = $routeLoader->getMethodCalls();
32+
$this->assertCount(2, $methodCalls);
33+
$this->assertEquals('setEnabledProviders', $methodCalls[0][0]);
34+
$this->assertEquals([['klp_mcp_server.provider.sse']], $methodCalls[0][1]);
35+
$this->assertEquals('setDefaultPath', $methodCalls[1][0]);
36+
$this->assertEquals(['mcp'], $methodCalls[1][1]);
37+
}
38+
39+
public function testCompilerPassWithMultipleProviders(): void
40+
{
41+
$container = new ContainerBuilder();
42+
43+
// Set up multiple providers
44+
$container->setParameter('klp_mcp_server.providers', [
45+
'klp_mcp_server.provider.sse',
46+
'klp_mcp_server.provider.streamable_http'
47+
]);
48+
$container->setParameter('klp_mcp_server.default_path', 'mcp');
49+
50+
// Register a mock route loader service
51+
$routeLoader = $container->register('klp_mcp_server.route_loader', \stdClass::class);
52+
53+
// Apply the compiler pass
54+
$pass = new ConditionalRoutePass();
55+
$pass->process($container);
56+
57+
// Verify the method calls were added
58+
$methodCalls = $routeLoader->getMethodCalls();
59+
$this->assertCount(2, $methodCalls);
60+
$this->assertEquals('setEnabledProviders', $methodCalls[0][0]);
61+
$this->assertEquals([[
62+
'klp_mcp_server.provider.sse',
63+
'klp_mcp_server.provider.streamable_http'
64+
]], $methodCalls[0][1]);
65+
$this->assertEquals('setDefaultPath', $methodCalls[1][0]);
66+
$this->assertEquals(['mcp'], $methodCalls[1][1]);
67+
}
68+
69+
public function testCompilerPassWithoutProviderParameter(): void
70+
{
71+
$container = new ContainerBuilder();
72+
73+
// Don't set the providers parameter
74+
75+
// Register a mock route loader service
76+
$routeLoader = $container->register('klp_mcp_server.route_loader', \stdClass::class);
77+
78+
// Apply the compiler pass
79+
$pass = new ConditionalRoutePass();
80+
$pass->process($container);
81+
82+
// Verify no method call was added
83+
$methodCalls = $routeLoader->getMethodCalls();
84+
$this->assertCount(0, $methodCalls);
85+
}
86+
}

tests/KlpMcpServerBundleTest.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace KLP\KlpMcpServer\Tests;
44

5+
use KLP\KlpMcpServer\DependencyInjection\CompilerPass\ConditionalRoutePass;
56
use KLP\KlpMcpServer\DependencyInjection\CompilerPass\ResourcesDefinitionCompilerPass;
67
use KLP\KlpMcpServer\DependencyInjection\CompilerPass\ToolsDefinitionCompilerPass;
78
use KLP\KlpMcpServer\KlpMcpServerBundle;
@@ -14,7 +15,7 @@
1415
class KlpMcpServerBundleTest extends TestCase
1516
{
1617
/**
17-
* Tests that the build method adds the ToolsDefinitionCompilerPass to the container.
18+
* Tests that the build method adds all compiler passes to the container.
1819
*/
1920
public function test_build_adds_compiler_pass()
2021
{
@@ -24,9 +25,10 @@ public function test_build_adds_compiler_pass()
2425
$invocations = [
2526
ToolsDefinitionCompilerPass::class,
2627
ResourcesDefinitionCompilerPass::class,
28+
ConditionalRoutePass::class,
2729
];
2830
$containerBuilder
29-
->expects($matcher = $this->exactly(2))
31+
->expects($matcher = $this->exactly(3))
3032
->method('addCompilerPass')
3133
->with($this->callback(function ($argument) use ($invocations, $matcher) {
3234
$this->assertInstanceOf($invocations[$matcher->numberOfInvocations() - 1], $argument);

0 commit comments

Comments
 (0)