Skip to content

Commit ae670c0

Browse files
committed
feature #866 [MCP Bundle] Add MCP profiler support to display server capabilities (camilleislasse)
This PR was squashed before being merged into the main branch. Discussion ---------- [MCP Bundle] Add MCP profiler support to display server capabilities | Q | A | ------------- | --- | Bug fix? |no | New feature? | yes <!-- please update src/**/CHANGELOG.md files --> | Docs? | no <!-- required for new features --> | Issues | Fix #529 | License | MIT ## Description This PR adds a Web Profiler integration for the MCP Bundle that displays MCP server capabilities (tools, prompts, resources, resource templates) in the Symfony debug toolbar and profiler panel. ## Features - ProfilingLoader to capture Registry reference during server build - DataCollector to collect and display MCP capabilities Note: This implementation depends on[ the loader pattern](modelcontextprotocol/php-sdk#111). If [MCP SDK PR #146](modelcontextprotocol/php-sdk#146) (direct Registry injection via DI) is merged, this approach may need adjustment. <img width="1338" height="911" alt="Capture d’écran 2025-11-13 à 20 10 00" src="https://github.com/user-attachments/assets/1059ea50-bf65-43b7-a3dc-cb60f0162cec" /> Commits ------- 1860e5f [MCP Bundle] Add MCP profiler support to display server capabilities
2 parents 917cab3 + 1860e5f commit ae670c0

File tree

14 files changed

+617
-4
lines changed

14 files changed

+617
-4
lines changed

docs/bundles/ai-bundle.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1040,7 +1040,7 @@ Profiler
10401040

10411041
The profiler panel provides insights into the agent's execution:
10421042

1043-
.. image:: profiler.png
1043+
.. image:: images/profiler-ai.png
10441044
:alt: Profiler Panel
10451045

10461046
Message stores
File renamed without changes.
423 KB
Loading

docs/bundles/mcp-bundle.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,23 @@ You can customize the logging level and destination according to your needs:
255255
channels: ['mcp']
256256
webhook_url: '%env(SLACK_WEBHOOK)%'
257257
258+
Profiler
259+
--------
260+
261+
When the Symfony Web Profiler is enabled, the MCP Bundle automatically adds a dedicated panel showing all registered MCP capabilities in your application:
262+
263+
.. image:: images/profiler-mcp.png
264+
:alt: MCP Profiler Panel
265+
266+
The profiler displays:
267+
268+
- **Tools**: All registered MCP tools with their descriptions and input schemas
269+
- **Prompts**: Available prompts with their arguments and requirements
270+
- **Resources**: Static resources with their URIs and MIME types
271+
- **Resource Templates**: Dynamic resource templates with URI patterns
272+
273+
This makes it easy to inspect and debug your MCP server capabilities during development.
274+
258275
Event System
259276
------------
260277

src/mcp-bundle/config/services.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
1313

14+
use Mcp\Capability\Registry;
1415
use Mcp\Server;
1516
use Mcp\Server\Builder;
1617

@@ -21,18 +22,20 @@
2122
->args(['mcp'])
2223
->tag('monolog.logger', ['channel' => 'mcp'])
2324

25+
->set('mcp.registry', Registry::class)
26+
->args([service('event_dispatcher'), service('monolog.logger.mcp')])
27+
2428
->set('mcp.server.builder', Builder::class)
2529
->factory([Server::class, 'builder'])
2630
->call('setServerInfo', [param('mcp.app'), param('mcp.version')])
2731
->call('setPaginationLimit', [param('mcp.pagination_limit')])
2832
->call('setInstructions', [param('mcp.instructions')])
2933
->call('setLogger', [service('monolog.logger.mcp')])
3034
->call('setEventDispatcher', [service('event_dispatcher')])
35+
->call('setRegistry', [service('mcp.registry')])
3136
->call('setSession', [service('mcp.session.store')])
3237
->call('setDiscovery', [param('kernel.project_dir'), param('mcp.discovery.scan_dirs'), param('mcp.discovery.exclude_dirs')])
3338

3439
->set('mcp.server', Server::class)
35-
->factory([service('mcp.server.builder'), 'build'])
36-
37-
;
40+
->factory([service('mcp.server.builder'), 'build']);
3841
};

src/mcp-bundle/src/McpBundle.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@
2121
use Symfony\AI\McpBundle\Command\McpCommand;
2222
use Symfony\AI\McpBundle\Controller\McpController;
2323
use Symfony\AI\McpBundle\DependencyInjection\McpPass;
24+
use Symfony\AI\McpBundle\Profiler\DataCollector;
25+
use Symfony\AI\McpBundle\Profiler\TraceableRegistry;
2426
use Symfony\AI\McpBundle\Routing\RouteLoader;
2527
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
2628
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
2729
use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
2830
use Symfony\Component\DependencyInjection\ChildDefinition;
2931
use Symfony\Component\DependencyInjection\ContainerBuilder;
3032
use Symfony\Component\DependencyInjection\ContainerInterface;
33+
use Symfony\Component\DependencyInjection\Definition;
3134
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
3235
use Symfony\Component\DependencyInjection\Reference;
3336
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;
@@ -55,6 +58,19 @@ public function loadExtension(array $config, ContainerConfigurator $container, C
5558

5659
$this->registerMcpAttributes($builder);
5760

61+
if ($builder->getParameter('kernel.debug')) {
62+
$traceableRegistry = (new Definition('mcp.traceable_registry'))
63+
->setClass(TraceableRegistry::class)
64+
->setArguments([new Reference('.inner')])
65+
->setDecoratedService('mcp.registry');
66+
$builder->setDefinition('mcp.traceable_registry', $traceableRegistry);
67+
68+
$dataCollector = (new Definition(DataCollector::class))
69+
->setArguments([new Reference('mcp.traceable_registry')])
70+
->addTag('data_collector');
71+
$builder->setDefinition('mcp.data_collector', $dataCollector);
72+
}
73+
5874
if (isset($config['client_transports'])) {
5975
$this->configureClient($config['client_transports'], $config['http'], $builder);
6076
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\AI\McpBundle\Profiler;
13+
14+
use Symfony\Bundle\FrameworkBundle\DataCollector\AbstractDataCollector;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpFoundation\Response;
17+
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
18+
19+
/**
20+
* Collects MCP server capabilities for the Web Profiler.
21+
*
22+
* @author Camille Islasse <guiziweb@gmail.com>
23+
*/
24+
final class DataCollector extends AbstractDataCollector implements LateDataCollectorInterface
25+
{
26+
public function __construct(
27+
private readonly TraceableRegistry $registry,
28+
) {
29+
}
30+
31+
public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
32+
{
33+
}
34+
35+
public function lateCollect(): void
36+
{
37+
$tools = [];
38+
foreach ($this->registry->getTools()->references as $tool) {
39+
$tools[] = [
40+
'name' => $tool->name,
41+
'description' => $tool->description,
42+
'inputSchema' => $tool->inputSchema,
43+
];
44+
}
45+
46+
$prompts = [];
47+
foreach ($this->registry->getPrompts()->references as $prompt) {
48+
$prompts[] = [
49+
'name' => $prompt->name,
50+
'description' => $prompt->description,
51+
'arguments' => array_map(fn ($arg) => [
52+
'name' => $arg->name,
53+
'description' => $arg->description,
54+
'required' => $arg->required,
55+
], $prompt->arguments ?? []),
56+
];
57+
}
58+
59+
$resources = [];
60+
foreach ($this->registry->getResources()->references as $resource) {
61+
$resources[] = [
62+
'uri' => $resource->uri,
63+
'name' => $resource->name,
64+
'description' => $resource->description,
65+
'mimeType' => $resource->mimeType,
66+
];
67+
}
68+
69+
$resourceTemplates = [];
70+
foreach ($this->registry->getResourceTemplates()->references as $template) {
71+
$resourceTemplates[] = [
72+
'uriTemplate' => $template->uriTemplate,
73+
'name' => $template->name,
74+
'description' => $template->description,
75+
'mimeType' => $template->mimeType,
76+
];
77+
}
78+
79+
$this->data = [
80+
'tools' => $tools,
81+
'prompts' => $prompts,
82+
'resources' => $resources,
83+
'resourceTemplates' => $resourceTemplates,
84+
];
85+
}
86+
87+
/**
88+
* @return array<array{name: string, description: ?string, inputSchema: array<mixed>}>
89+
*/
90+
public function getTools(): array
91+
{
92+
return $this->data['tools'] ?? [];
93+
}
94+
95+
/**
96+
* @return array<array{name: string, description: ?string, arguments: array<mixed>}>
97+
*/
98+
public function getPrompts(): array
99+
{
100+
return $this->data['prompts'] ?? [];
101+
}
102+
103+
/**
104+
* @return array<array{uri: string, name: string, description: ?string, mimeType: ?string}>
105+
*/
106+
public function getResources(): array
107+
{
108+
return $this->data['resources'] ?? [];
109+
}
110+
111+
/**
112+
* @return array<array{uriTemplate: string, name: string, description: ?string, mimeType: ?string}>
113+
*/
114+
public function getResourceTemplates(): array
115+
{
116+
return $this->data['resourceTemplates'] ?? [];
117+
}
118+
119+
public function getTotalCount(): int
120+
{
121+
return \count($this->getTools()) + \count($this->getPrompts()) + \count($this->getResources()) + \count($this->getResourceTemplates());
122+
}
123+
124+
public static function getTemplate(): string
125+
{
126+
return '@Mcp/data_collector.html.twig';
127+
}
128+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\AI\McpBundle\Profiler;
13+
14+
use Mcp\Capability\Discovery\DiscoveryState;
15+
use Mcp\Capability\Registry\PromptReference;
16+
use Mcp\Capability\Registry\ResourceReference;
17+
use Mcp\Capability\Registry\ResourceTemplateReference;
18+
use Mcp\Capability\Registry\ToolReference;
19+
use Mcp\Capability\RegistryInterface;
20+
use Mcp\Schema\Page;
21+
use Mcp\Schema\Prompt;
22+
use Mcp\Schema\Resource;
23+
use Mcp\Schema\ResourceTemplate;
24+
use Mcp\Schema\Tool;
25+
26+
/**
27+
* Decorator for Registry that provides access to capabilities for the profiler.
28+
*
29+
* @author Camille Islasse <guiziweb@gmail.com>
30+
*/
31+
final class TraceableRegistry implements RegistryInterface
32+
{
33+
public function __construct(
34+
private readonly RegistryInterface $registry,
35+
) {
36+
}
37+
38+
public function registerTool(Tool $tool, callable|array|string $handler, bool $isManual = false): void
39+
{
40+
$this->registry->registerTool($tool, $handler, $isManual);
41+
}
42+
43+
public function registerResource(Resource $resource, callable|array|string $handler, bool $isManual = false): void
44+
{
45+
$this->registry->registerResource($resource, $handler, $isManual);
46+
}
47+
48+
public function registerResourceTemplate(
49+
ResourceTemplate $template,
50+
callable|array|string $handler,
51+
array $completionProviders = [],
52+
bool $isManual = false,
53+
): void {
54+
$this->registry->registerResourceTemplate($template, $handler, $completionProviders, $isManual);
55+
}
56+
57+
public function registerPrompt(
58+
Prompt $prompt,
59+
callable|array|string $handler,
60+
array $completionProviders = [],
61+
bool $isManual = false,
62+
): void {
63+
$this->registry->registerPrompt($prompt, $handler, $completionProviders, $isManual);
64+
}
65+
66+
public function clear(): void
67+
{
68+
$this->registry->clear();
69+
}
70+
71+
public function getDiscoveryState(): DiscoveryState
72+
{
73+
return $this->registry->getDiscoveryState();
74+
}
75+
76+
public function setDiscoveryState(DiscoveryState $state): void
77+
{
78+
$this->registry->setDiscoveryState($state);
79+
}
80+
81+
public function hasTools(): bool
82+
{
83+
return $this->registry->hasTools();
84+
}
85+
86+
public function getTools(?int $limit = null, ?string $cursor = null): Page
87+
{
88+
return $this->registry->getTools($limit, $cursor);
89+
}
90+
91+
public function getTool(string $name): ToolReference
92+
{
93+
return $this->registry->getTool($name);
94+
}
95+
96+
public function hasResources(): bool
97+
{
98+
return $this->registry->hasResources();
99+
}
100+
101+
public function getResources(?int $limit = null, ?string $cursor = null): Page
102+
{
103+
return $this->registry->getResources($limit, $cursor);
104+
}
105+
106+
public function getResource(string $uri, bool $includeTemplates = true): ResourceReference|ResourceTemplateReference
107+
{
108+
return $this->registry->getResource($uri, $includeTemplates);
109+
}
110+
111+
public function hasResourceTemplates(): bool
112+
{
113+
return $this->registry->hasResourceTemplates();
114+
}
115+
116+
public function getResourceTemplates(?int $limit = null, ?string $cursor = null): Page
117+
{
118+
return $this->registry->getResourceTemplates($limit, $cursor);
119+
}
120+
121+
public function getResourceTemplate(string $uriTemplate): ResourceTemplateReference
122+
{
123+
return $this->registry->getResourceTemplate($uriTemplate);
124+
}
125+
126+
public function hasPrompts(): bool
127+
{
128+
return $this->registry->hasPrompts();
129+
}
130+
131+
public function getPrompts(?int $limit = null, ?string $cursor = null): Page
132+
{
133+
return $this->registry->getPrompts($limit, $cursor);
134+
}
135+
136+
public function getPrompt(string $name): PromptReference
137+
{
138+
return $this->registry->getPrompt($name);
139+
}
140+
}

0 commit comments

Comments
 (0)