diff --git a/demo/src/Mcp/Tools/CurrentTimeTool.php b/demo/src/Mcp/Tools/CurrentTimeTool.php index bd52edc72..6bbbeae79 100644 --- a/demo/src/Mcp/Tools/CurrentTimeTool.php +++ b/demo/src/Mcp/Tools/CurrentTimeTool.php @@ -12,12 +12,18 @@ namespace App\Mcp\Tools; use Mcp\Capability\Attribute\McpTool; +use Psr\Log\LoggerInterface; /** * @author Tom Hart */ class CurrentTimeTool { + public function __construct( + private readonly LoggerInterface $logger, + ) { + } + /** * Returns the current time in UTC. * @@ -26,6 +32,8 @@ class CurrentTimeTool #[McpTool(name: 'current-time')] public function getCurrentTime(string $format = 'Y-m-d H:i:s'): string { + $this->logger->info('CurrentTimeTool called', ['format' => $format]); + return (new \DateTime('now', new \DateTimeZone('UTC')))->format($format); } } diff --git a/docs/bundles/mcp-bundle.rst b/docs/bundles/mcp-bundle.rst index 5c641a532..40bdd4f63 100644 --- a/docs/bundles/mcp-bundle.rst +++ b/docs/bundles/mcp-bundle.rst @@ -113,6 +113,33 @@ Dynamic resources with parameters: All capabilities are automatically discovered in the ``src/`` directory when the server starts. +Attribute Placement Patterns +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The MCP SDK, and therefore the MCP Bundle, supports two patterns for placing attributes on your capabilities: + +**Invokable Pattern** - Attribute on a class with ``__invoke()`` method:: + + #[McpTool(name: 'my-tool')] + class MyTool + { + public function __invoke(string $param): string + { + // Implementation + } + } + +**Method-Based Pattern** - Multiple attributes on individual methods:: + + class MyTools + { + #[McpTool(name: 'tool-one')] + public function toolOne(): string { } + + #[McpTool(name: 'tool-two')] + public function toolTwo(): string { } + } + Transport Types ............... diff --git a/src/mcp-bundle/src/DependencyInjection/McpPass.php b/src/mcp-bundle/src/DependencyInjection/McpPass.php index 1061de6d1..4ebcb6c53 100644 --- a/src/mcp-bundle/src/DependencyInjection/McpPass.php +++ b/src/mcp-bundle/src/DependencyInjection/McpPass.php @@ -14,6 +14,7 @@ use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; final class McpPass implements CompilerPassInterface { @@ -35,7 +36,12 @@ public function process(ContainerBuilder $container): void return; } - $serviceLocatorRef = ServiceLocatorTagPass::register($container, $allMcpServices); + $serviceReferences = []; + foreach (array_keys($allMcpServices) as $serviceId) { + $serviceReferences[$serviceId] = new Reference($serviceId); + } + + $serviceLocatorRef = ServiceLocatorTagPass::register($container, $serviceReferences); $container->getDefinition('mcp.server.builder') ->addMethodCall('setContainer', [$serviceLocatorRef]); diff --git a/src/mcp-bundle/src/McpBundle.php b/src/mcp-bundle/src/McpBundle.php index 0359b79d5..f810719a0 100644 --- a/src/mcp-bundle/src/McpBundle.php +++ b/src/mcp-bundle/src/McpBundle.php @@ -75,7 +75,7 @@ private function registerMcpAttributes(ContainerBuilder $builder): void foreach ($mcpAttributes as $attributeClass => $tag) { $builder->registerAttributeForAutoconfiguration( $attributeClass, - static function (ChildDefinition $definition) use ($tag): void { + static function (ChildDefinition $definition, object $attribute, \Reflector $reflector) use ($tag): void { $definition->addTag($tag); } ); diff --git a/src/mcp-bundle/tests/DependencyInjection/McpPassTest.php b/src/mcp-bundle/tests/DependencyInjection/McpPassTest.php index 0982e8054..1f3a1e088 100644 --- a/src/mcp-bundle/tests/DependencyInjection/McpPassTest.php +++ b/src/mcp-bundle/tests/DependencyInjection/McpPassTest.php @@ -13,8 +13,10 @@ use PHPUnit\Framework\TestCase; use Symfony\AI\McpBundle\DependencyInjection\McpPass; +use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; /** * @covers \Symfony\AI\McpBundle\DependencyInjection\McpPass @@ -53,6 +55,18 @@ public function testCreatesServiceLocatorForAllMcpServices() $this->assertArrayHasKey('prompt_service', $services); $this->assertArrayHasKey('resource_service', $services); $this->assertArrayHasKey('template_service', $services); + + // Verify services are ServiceClosureArguments wrapping References + $this->assertInstanceOf(ServiceClosureArgument::class, $services['tool_service']); + $this->assertInstanceOf(ServiceClosureArgument::class, $services['prompt_service']); + $this->assertInstanceOf(ServiceClosureArgument::class, $services['resource_service']); + $this->assertInstanceOf(ServiceClosureArgument::class, $services['template_service']); + + // Verify the underlying values are References + $this->assertInstanceOf(Reference::class, $services['tool_service']->getValues()[0]); + $this->assertInstanceOf(Reference::class, $services['prompt_service']->getValues()[0]); + $this->assertInstanceOf(Reference::class, $services['resource_service']->getValues()[0]); + $this->assertInstanceOf(Reference::class, $services['template_service']->getValues()[0]); } public function testDoesNothingWhenNoMcpServicesTagged() @@ -115,5 +129,13 @@ public function testHandlesPartialMcpServices() $this->assertArrayHasKey('prompt_service', $services); $this->assertArrayNotHasKey('resource_service', $services); $this->assertArrayNotHasKey('template_service', $services); + + // Verify services are ServiceClosureArguments wrapping References + $this->assertInstanceOf(ServiceClosureArgument::class, $services['tool_service']); + $this->assertInstanceOf(ServiceClosureArgument::class, $services['prompt_service']); + + // Verify the underlying values are References + $this->assertInstanceOf(Reference::class, $services['tool_service']->getValues()[0]); + $this->assertInstanceOf(Reference::class, $services['prompt_service']->getValues()[0]); } }