Skip to content

feat(symfony): add bridge for Symfony #43

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ This SDK enables you to expose your PHP application's functionality as standardi
- **🧪 Completion Providers**: Built-in support for argument completion in tools and prompts
- **🔌 Dependency Injection**: Full PSR-11 container support with auto-wiring
- **📋 Comprehensive Testing**: Extensive test suite with integration tests for all transports
- **🔌 Symfony integration**: This SDK can be easily integrated into Symfony applications using the bridge included.

This package supports the **2025-03-26** version of the Model Context Protocol with backward compatibility.

Expand Down Expand Up @@ -1213,6 +1214,61 @@ sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d mcp.yourdomain.com
```

## Symfony integration

This SDK provides a Symfony bundle for easy integration into Symfony applications. The bundle automatically registers MCP elements and provides a convenient way to configure the server.

### Step 1: Installation

```php
// config/bundles.php

return [
// ...
PhpMcp\Server\Bridge\Symfony\McpServerBundle::class => ['all' => true],
// ...
];
```

### Step 2: Configuration

```yaml
# config/packages/mcp_server.yaml
mcp_server:
logger: logger # default value, must be a service ID
server_info:
name: MCP Server
version: 1.0.0
```

### Step 3: Registering MCP Elements

In order to register MCP elements, you must implement the `McpElementInterface` in your services. The bundle will automatically discovers these services and registers them with the MCP server.

```php
namespace App\Mcp;

use PhpMcp\Server\Attributes\McpTool;
use PhpMcp\Server\Contracts\McpElementInterface;

class MyMcpService implements McpElementInterface
{
#[McpTool(name: 'my_tool')]
public function process(): string
{
// Your tool logic here
}
}
```

### Step 4: Running the server

The bundle provides a command to run the MCP server:

```bash
bin/console mcp-server:start
```

## 📚 Examples & Use Cases

Explore comprehensive examples in the [`examples/`](./examples/) directory:
Expand Down
9 changes: 8 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,17 @@
"pestphp/pest": "^2.36.0|^3.5.0",
"react/async": "^4.0",
"react/child-process": "^0.6.6",
"symfony/config": "^6.4|^7.1",
"symfony/dependency-injection": "^6.4|^7.1",
"symfony/http-kernel": "^6.4|^7.1",
"symfony/var-dumper": "^6.4.11|^7.1.5"
},
"suggest": {
"react/http": "Required for using the ReactPHP HTTP transport handler (^1.11 recommended)."
"react/http": "Required for using the ReactPHP HTTP transport handler (^1.11 recommended).",
"symfony/config": "To use the Symfony bundle shipped with this package",
"symfony/console": "To use the Symfony bundle shipped with this package",
"symfony/dependency-injection": "To use the Symfony bundle shipped with this package",
"symfony/http-kernel": "To use the Symfony bundle shipped with this package"
},
"autoload": {
"psr-4": {
Expand Down
28 changes: 28 additions & 0 deletions src/Bridge/Symfony/Command/McpServerStartCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Bridge\Symfony\Command;

use PhpMcp\Server\Server;
use PhpMcp\Server\Transports\StdioServerTransport;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

#[AsCommand(name: 'mcp-server:start', description: 'Starts MCP server')]
class McpServerStartCommand extends Command
{
public function __construct(private Server $server)
{
parent::__construct();
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->server->listen(new StdioServerTransport());

return Command::SUCCESS;
}
}
87 changes: 87 additions & 0 deletions src/Bridge/Symfony/DependencyInjection/Compiler/McpServerPass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Bridge\Symfony\DependencyInjection\Compiler;

use PhpMcp\Server\Attributes\McpPrompt;
use PhpMcp\Server\Attributes\McpResource;
use PhpMcp\Server\Attributes\McpResourceTemplate;
use PhpMcp\Server\Attributes\McpTool;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Reference;

class McpServerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
$serverBuilderDefinition = $container->getDefinition('mcp_server.server_builder');

$mcpElements = [];

foreach ($container->findTaggedServiceIds('mcp_server.server_element') as $serviceId => $tags) {
$definition = $container->findDefinition($serviceId);

$mcpElements[$definition->getClass()] = new Reference($serviceId);

$reflectionClass = new \ReflectionClass($definition->getClass());

foreach ([McpPrompt::class, McpResource::class, McpResourceTemplate::class, McpTool::class] as $attributeClass) {
if ([] !== $reflectionAttributes = $reflectionClass->getAttributes($attributeClass, \ReflectionAttribute::IS_INSTANCEOF)) {
if (!$reflectionClass->hasMethod('__invoke')) {
throw new LogicException(sprintf('The class "%s" has attribute "%s" but method "__invoke" is missing, please declare it.', $definition->getClass(), $attributeClass));
}

$this->mapElements($attributeClass, $serverBuilderDefinition, [$definition->getClass(), '__invoke'], $reflectionAttributes[0]->getArguments());

break;
}

foreach ($reflectionClass->getMethods() as $reflectionMethod) {
if ([] !== $reflectionAttributes = $reflectionMethod->getAttributes($attributeClass, \ReflectionAttribute::IS_INSTANCEOF)) {
$this->mapElements($attributeClass, $serverBuilderDefinition, [$definition->getClass(), $reflectionMethod->getName()], $reflectionAttributes[0]->getArguments());
}
}
}
}

$serverBuilderDefinition->addMethodCall('withContainer', [ServiceLocatorTagPass::register($container, $mcpElements)]);
}

private function mapElements(string $attributeClass, Definition $serverBuilderDefinition, array $handler, array $attributeArgs): void
{
match ($attributeClass) {
McpPrompt::class => $serverBuilderDefinition->addMethodCall('withPrompt', [
$handler,
$attributeArgs['name'] ?? null,
$attributeArgs['description'] ?? null,
]),
McpResource::class => $serverBuilderDefinition->addMethodCall('withResource', [
$handler,
$attributeArgs['uri'] ?? null,
$attributeArgs['name'] ?? null,
$attributeArgs['description'] ?? null,
$attributeArgs['mimeType'] ?? null,
$attributeArgs['size'] ?? null,
$attributeArgs['annotations'] ?? [],
]),
McpResourceTemplate::class => $serverBuilderDefinition->addMethodCall('withResourceTemplate', [
$handler,
$attributeArgs['uriTemplate'] ?? null,
$attributeArgs['name'] ?? null,
$attributeArgs['description'] ?? null,
$attributeArgs['mimeType'] ?? null,
$attributeArgs['annotations'] ?? [],
]),
McpTool::class => $serverBuilderDefinition->addMethodCall('withTool', [
$handler,
$attributeArgs['name'] ?? null,
$attributeArgs['description'] ?? null,
]),
};
}
}
60 changes: 60 additions & 0 deletions src/Bridge/Symfony/McpServerBundle.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Bridge\Symfony;

use PhpMcp\Server\Bridge\Symfony\Command\McpServerStartCommand;
use PhpMcp\Server\Bridge\Symfony\DependencyInjection\Compiler\McpServerPass;
use PhpMcp\Server\Contracts\McpElementInterface;
use PhpMcp\Server\Server;
use PhpMcp\Server\ServerBuilder;
use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;
use function Symfony\Component\DependencyInjection\Loader\Configurator\service;

class McpServerBundle extends AbstractBundle
{
public function configure(DefinitionConfigurator $definition): void
{
$definition->rootNode()
->children()
->scalarNode('logger')->cannotBeEmpty()->defaultValue('logger')->end()
->arrayNode('server_info')
->isRequired()
->children()
->scalarNode('name')->isRequired()->cannotBeEmpty()->end()
->scalarNode('version')->isRequired()->cannotBeEmpty()->end()
->end()
->end()
->end();
}

public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void
{
$container->services()
->set('mcp_server.server_builder', ServerBuilder::class)
->factory([Server::class, 'make'])
->call('withLogger', [service($config['logger'])])
->call('withServerInfo', [$config['server_info']['name'], $config['server_info']['version']])

->set('mcp_server.server', Server::class)
->factory([service('mcp_server.server_builder'), 'build'])
->alias(Server::class, 'mcp_server.server')

->set('mcp_server.command.server_start', McpServerStartCommand::class)
->args([service('mcp_server.server')])
->tag('console.command');

$builder->registerForAutoconfiguration(McpElementInterface::class)->addTag('mcp_server.server_element');
}

public function build(ContainerBuilder $container): void
{
parent::build($container);

$container->addCompilerPass(new McpServerPass());
}
}
9 changes: 9 additions & 0 deletions src/Contracts/McpElementInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace PhpMcp\Server\Contracts;

interface McpElementInterface
{
}
Loading