Installation
composer require ecourty/mcp-server-bundle
Add to config/bundles.php:
return [
// ...
EdouardCourty\McpServerBundle\McpServerBundle::class => ['all' => true],
];
Basic Configuration
Define a minimal config/packages/mcp_server.yaml:
mcp_server:
protocol_version: '2025-06-18'
host: '0.0.0.0'
port: 8080
First Tool Implementation
Create a tool class (e.g., src/Tool/MyTool.php):
namespace App\Tool;
use EdouardCourty\McpServerBundle\Tool\ToolInterface;
class MyTool implements ToolInterface {
public function execute(array $input): array {
return ['result' => 'Processed: ' . $input['data']];
}
}
Register Tool in Services
# config/services.yaml
services:
App\Tool\MyTool:
tags: ['mcp.tool']
Run the Server
php bin/console mcp:server:start
Create a tool to fetch data from a database and expose it to the MCP client:
// src/Tool/DatabaseQueryTool.php
namespace App\Tool;
use Doctrine\ORM\EntityManagerInterface;
use EdouardCourty\McpServerBundle\Tool\ToolInterface;
class DatabaseQueryTool implements ToolInterface {
public function __construct(private EntityManagerInterface $em) {}
public function execute(array $input): array {
$query = $this->em->createQuery($input['query']);
return ['results' => $query->getResult()];
}
}
Register it with:
services:
App\Tool\DatabaseQueryTool:
arguments: ['@doctrine.orm.entity_manager']
tags: ['mcp.tool']
Interface Adherence
All tools must implement ToolInterface with:
public function execute(array $input): array;
$input (e.g., using Symfony Validator).Dependency Injection Use Symfony’s DI to inject services (e.g., Doctrine, HTTP clients):
use Symfony\Contracts\HttpClient\HttpClientInterface;
class HttpTool implements ToolInterface {
public function __construct(private HttpClientInterface $client) {}
// ...
}
Tool Metadata Annotate tools with metadata (e.g., description, parameters) via YAML:
# config/packages/mcp_tools.yaml
mcp_tools:
database_query:
description: "Execute a Doctrine query."
parameters:
query: { type: string, required: true }
Tool Groups
Organize tools into logical groups (e.g., admin, user) for ACL:
mcp_tools:
groups:
admin:
- database_query
- system_tools
Security Use Symfony’s security system to restrict tools:
# config/packages/security.yaml
access_control:
- { path: ^/mcp/tools/database_query, roles: ROLE_ADMIN }
Messenger for Async Tools Offload long-running tasks to Messenger:
use Symfony\Component\Messenger\MessageBusInterface;
class AsyncTool implements ToolInterface {
public function __construct(private MessageBusInterface $bus) {}
public function execute(array $input): array {
$this->bus->dispatch(new ProcessInputMessage($input));
return ['status' => 'queued'];
}
}
Event Listeners
Hook into MCP events (e.g., McpServerEvent::TOOL_EXECUTED):
use EdouardCourty\McpServerBundle\Event\McpServerEvents;
class ToolLogger implements EventSubscriberInterface {
public static function getSubscribedEvents(): array {
return [
MCPServerEvents::TOOL_EXECUTED => 'onToolExecuted',
];
}
public function onToolExecuted(ToolExecutedEvent $event) {
// Log tool usage
}
}
Dynamic Prompts Use Twig to render prompts dynamically:
{# templates/mcp/prompts/welcome.twig #}
Hello {{ user.name }}! Here are your tools:
{% for tool in tools %}
- {{ tool.description }}
{% endfor %}
Load in config:
mcp_server:
prompts:
welcome: '@mcp/prompts/welcome.twig'
Resource Caching Cache resources (e.g., FAQs) to reduce payload size:
use Symfony\Contracts\Cache\CacheInterface;
class FaqResourceTool implements ToolInterface {
public function __construct(private CacheInterface $cache) {}
public function execute(array $input): array {
return $this->cache->get('faq', function() {
return $this->fetchFaqFromDatabase();
});
}
}
Protocol Version Mismatch
protocol_version doesn’t match.mcp_server:
protocol_version: '2025-06-18'
fallback_version: '2024-12-01'
Tool Input Validation
use Symfony\Component\Validator\Validator\ValidatorInterface;
class ValidatedTool implements ToolInterface {
public function __construct(private ValidatorInterface $validator) {}
public function execute(array $input): array {
$errors = $this->validator->validate($input);
if (count($errors)) {
throw new \RuntimeException((string) $errors);
}
// ...
}
}
CORS and Authentication
# config/packages/security.yaml
firewalls:
mcp:
pattern: ^/mcp
stateless: true
json_login:
check_path: /mcp/auth
Performance with Heavy Tools
ReactPHP for async tools:
use React\Promise\PromiseInterface;
class AsyncHttpTool implements ToolInterface {
public function execute(array $input): array {
return $this->client->request($input['url'])
->then(fn(ResponseInterface $res) => $res->getBody()->getContents());
}
}
Enable Verbose Logging
mcp_server:
debug: true
Logs will appear in var/log/dev.log with tool execution details.
Tool Execution Tracing
Use the McpServerEvents::TOOL_EXECUTED event to trace inputs/outputs:
public function onToolExecuted(ToolExecutedEvent $event) {
\Log::debug('Tool executed', [
'tool' => $event->getToolName(),
'input' => $event->getInput(),
'output' => $event->getOutput(),
'duration_ms' => $event->getDuration(),
]);
}
Test Tools Locally
Use the mcp:tool:test command to simulate tool execution:
php bin/console mcp:tool:test database_query '{"query": "SELECT * FROM users"}'
Custom Tool Factories Override tool instantiation for complex logic:
use EdouardCourty\McpServerBundle\DependencyInjection\Compiler\ToolPass;
class CustomToolFactoryPass extends ToolPass {
public function process(\Symfony\Component\DependencyInjection\ContainerBuilder $container) {
$definition = $container->findDefinition('mcp.tool_factory');
$definition->addMethodCall('addFactory', [
'database_query',
[$container->getDefinition('App\Tool\DatabaseQueryToolFactory')],
]);
}
}
Middleware for Tools Add preprocessing/postprocessing to tools:
use EdouardCourty\McpServerBundle\Tool
How can I help you explore Laravel packages today?