A Symfony Bundle to create MCP servers
A powerful Symfony bundle for handling MCP (Message Control Protocol) server implementations, providing tools for JSON-RPC request handling and tool management.
Read the official MCP specification.
[!WARNING]
The specification of the Model Context Protocol (MCP) changes frequently. This bundle will evolve along with the specification, so please ensure you are using the latest version of the bundle.
The CHANGELOG can be found here.
The MCP Server Bundle provides a structured way to create and manage tools that can be used by clients via JSON-RPC requests.
It includes features for MCP tool management, and JSON-RPC method handling.
This bundle is designed to be flexible and extensible, allowing developers to create custom tool handlers and method handlers as needed.
MethodHandlers and ToolHandlers are registered and autowired using attributes, making it easy to define and manage your own tools.
composer require ecourty/mcp-server-bundle
config/bundles.php (if not using Symfony Flex):return [
// ...
Ecourty\McpServerBundle\McpServerBundle::class => ['all' => true],
];
config/routes/mcp.yaml:mcp_controller:
path: /mcp
controller: mcp_server.entrypoint_controller
You can customize the MCP Server Bundle configuration in config/packages/mcp_server.yaml:
mcp_server:
server:
name: 'My MCP Server' # The name of your MCP server, used in the initialization response
title: 'My MCP Server Display Name' # The title of your MCP server, used in the initialization response
version: '1.0.0' # The version of your MCP server, used in the initialization response
Tools are the core components of the MCP Server Bundle. They allow you to define and manage custom logic that can be triggered by clients.
#[AsTool] attribute to register your tool__invoke method to handle the tool logic and return a ToolResultAs Tool classes are services within the Symfony application, any dependency can be injected in it, using the constructor, like any other service.
Example:
#[OA\Schema(required: ['emailAddress', 'username'])]
class CreateUserSchema
{
#[Assert\Email]
#[Assert\NotBlank]
#[Assert\Length(min: 5, max: 255)]
#[OA\Property(description: 'The email address of the user', type: 'string', maxLength: 255, minLength: 5, nullable: false)]
public string $emailAddress;
#[Assert\NotBlank]
#[Assert\Length(min: 3, max: 50)]
#[OA\Property(description: 'The username of the user', type: 'string', maxLength: 50, minLength: 3, nullable: false)]
public string $username;
}
use App\Schema\CreateUserSchema;
use Ecourty\McpServerBundle\Attribute\AsTool;
use Ecourty\McpServerBundle\Attribute\ToolAnnotations;
use Ecourty\McpServerBundle\IO\TextToolResult;
use Ecourty\McpServerBundle\IO\ToolResult;
#[AsTool(
name: 'create_user', # Unique identifier for the tool, used by clients to call it
description: 'Creates a new user in the system', # This description is used by LLMs to understand the tool's purpose
annotations: new ToolAnnotations(
title: 'Create a user', // A human-readable title for the tool, useful for documentation
readOnlyHint: false, // Defines the request is not read-only (creates a user)
destructiveHint: false, // Defines the request is not destructive (does not delete data)
idempotentHint: false, // Defines the request cannot be repeated without changing the state
openWorldHint: false, // The tool does not interact with external systems
)
)]
class CreateUserTool
{
public function __invoke(CreateUserSchema $createUserSchema): ToolResult
{
// Your logic here...
return new ToolResult([new TextToolResult('User created successfully!')]);
}
}
The MCP specification states that tool results should consist of an array of objects.
The bundle provides several result types that can be combined in a single ToolResult object:
TextToolResult: For text-based resultsImageToolResult: For image resultsAudioToolResult: For audio resultsResourceToolResult: For file or resource resultsAll tool results must be wrapped in a ToolResult object, which can contain multiple results and handle error state.
Example:
<?php
use App\Schema\ReadFileSchema;
use Ecourty\McpServerBundle\Attribute\AsTool;
use Ecourty\McpServerBundle\IO\TextToolResult;
use Ecourty\McpServerBundle\IO\ToolResult;
#[AsTool(name: 'read_file', description: 'Reads a file and returns its content')]
class MyTool
{
public function __invoke(ReadFileSchema $payload): ToolResult
{
$fileContent = file_get_contents($payload->filePath);
$anotherFileContent = file_get_contents($payload->anotherFilePath);
// Create individual results
$textResult = new TextToolResult($fileContent);
$anotherTextResult = new TextToolResult($anotherFileContent);
// Combine them in a ToolResult
return new ToolResult([
$textResult,
$anotherTextResult,
]);
}
}
Error handling example:
use Ecourty\McpServerBundle\IO\TextToolResult;
use Ecourty\McpServerBundle\IO\ToolResult;
class MyTool
{
public function __invoke(): ToolResult
{
try {
// Your logic here...
return new ToolResult([
new TextToolResult('Success!')
]);
} catch (\Exception $e) {
return new ToolResult(
[new TextToolResult($e->getMessage())],
isError: true
);
}
}
}
The ToolResult class provides the following features:
The bundle provides several events that you can listen to:
ToolCallEvent: Dispatched before a tool is called, contains the tool name and input dataToolResultEvent: Dispatched after a tool has been called, contains the result of the tool callToolCallExceptionEvent: Dispatched when a tool throws an exception, contains the tool name, input data and throwableExample of event listener:
use Ecourty\McpServerBundle\Event\ToolCallEvent;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
#[AsEventListener(event: ToolCallEvent::class)]
class ToolCallListener
{
public function __invoke(ToolCallEvent $event): void
{
// Your logic here...
}
}
The bundle provides robust input validation and sanitization through schema-based deserialization.
Input schemas are extracted from the __invoke method of classes with the #[AsTool] attribute, allowing you to define the expected input structure and validation rules.
<?php
use OpenApi\Attributes as OA;
use Symfony\Component\Validator\Constraints as Assert;
class CreateUser
{
#[Assert\NotBlank]
#[Assert\Length(min: 2, max: 50)]
#[OA\Property(type: 'string', description: 'The name of the user', nullable: false)]
private string $name;
#[Assert\NotBlank]
#[Assert\Email]
#[OA\Property(type: 'string', description: 'The email address of the user', nullable: false)]
private string $email;
// Getters and setters...
}
tools/listThis ensures that your tool handlers always receive properly validated and sanitized data.
tools/list: Lists all available tools and their definitions.tools/call: Executes a tool by name, with the provided input data.Resources are data sources that can be accessed by clients via their URI.
They can represent files, database records, or any other data that can be identified by a URI.
#[AsResource] attribute to register your resource__invoke method to handle the resource logic and return a ResourceResultAs Resource classes are services within the Symfony application, any dependency can be injected in it, using the constructor, like any other service.
Static resources have a fixed URI that doesn't change. They are useful for resources that don't require parameters.
Example:
<?php
use Ecourty\McpServerBundle\Attribute\AsResource;
use Ecourty\McpServerBundle\IO\Resource\ResourceResult;
use Ecourty\McpServerBundle\IO\Resource\TextResource;
#[AsResource(
uri: 'file://robots.txt',
name: 'robots_txt',
title: 'Get the Robots.txt file',
description: 'This resource returns the content of the robots.txt file.',
mimeType: 'text/plain',
)]
class RobotsFileResource
{
private const string FILE_PATH = __DIR__ . '/../Resources/robots.txt';
public function __invoke(): ResourceResult
{
$fileContent = (string) file_get_contents(self::FILE_PATH);
$encodedFileContent = base64_encode($fileContent);
return new ResourceResult([
new BinaryResource('file://robots.txt', 'text/plain', $encodedFileContent),
]);
}
}
Templated resources use URI templates with parameters enclosed in curly braces (e.g., {id}). These parameters are automatically extracted from the URI and passed to the __invoke method as arguments.
Example:
<?php
use Ecourty\McpServerBundle\Attribute\AsResource;
use Ecourty\McpServerBundle\IO\Resource\ResourceResult;
use Ecourty\McpServerBundle\IO\Resource\TextResource;
#[AsResource(
uri: 'database://user/{id}',
name: 'user_data',
title: 'Get User Data',
description: 'Gathers the data of a user by their ID.',
mimeType: 'application/json',
)]
class UserResource
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly SerializerInterface $serializer,
) {
}
public function __invoke(int $id): ResourceResult
{
$user = $this->entityManager->find(User::class, $id);
if ($user === null) {
throw new \RuntimeException('User not found');
}
$stringifiedUserData = $this->serializer->serialize($user, 'json');
return new ResourceResult([
new TextResource(
uri: 'database://user/' . $id,
mimeType: 'application/json',
text: $stringifiedUserData,
),
]);
}
}
In this example:
database://user/{id} defines a parameter named iddatabase://user/123, the parameter 123 is extracted__invoke method receives 123 as an int parameter (automatic type casting is performed)You can define multiple parameters in a single URI template:
#[AsResource(
uri: 'api://users/{userId}/posts/{postId}',
name: 'user_post',
title: 'Get User Post',
description: 'Retrieves a specific post by a user.',
mimeType: 'application/json',
)]
class UserPostResource
{
public function __invoke(int $userId, int $postId): ResourceResult
{
// Your logic here...
$post = $this->postRepository->findByUserAndPost($userId, $postId);
return new ResourceResult([
new TextResource(
uri: "api://users/{$userId}/posts/{$postId}",
mimeType: 'application/json',
text: json_encode($post),
),
]);
}
}
The bundle automatically casts URI parameters to the appropriate types based on the method signature:
int parameters are cast to integersfloat parameters are cast to floatsbool parameters are cast to booleansstring parameters remain as stringsarray parameters are JSON-decoded into arrays using json_decode if they are JSON stringsThe MCP specification states that resource results should consist of an array of resource objects.
The bundle provides several result types that can be combined in a single ResourceResult object:
TextResource: For text-based content (JSON, XML, plain text, etc.)BinaryResource: For binary content (images, audio, video, files, etc.), should be base-64 encodedAll resource results must be wrapped in a ResourceResult object, which can contain multiple resources.
Example:
<?php
use Ecourty\McpServerBundle\IO\Resource\ResourceResult;
use Ecourty\McpServerBundle\IO\Resource\TextResource;
use Ecourty\McpServerBundle\IO\Resource\BinaryResource;
#[AsResource(
// ...
)]
class MyResource
{
public function __invoke(): ResourceResult
{
$jsonData = json_encode(['status' => 'success']);
$imageData = base64_encode(file_get_contents('image.jpg'));
return new ResourceResult([
new TextResource(
uri: 'api://data/status',
mimeType: 'application/json',
text: $jsonData,
),
new BinaryResource(
uri: 'file://image.jpg',
mimeType: 'image/jpeg',
blob: $imageData,
),
]);
}
}
The ResourceResult class provides the following features:
The bundle provides several events that you can listen to:
ResourceReadEvent: Dispatched before a resource is read, contains the URIResourceReadResultEvent: Dispatched after a resource has been read, contains the URI and resultsExample of event listener:
<?php
use Ecourty\McpServerBundle\Event\ResourceReadEvent;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
#[AsEventListener(event: ResourceReadEvent::class)]
class ResourceReadListener
{
public function __invoke(ResourceReadEvent $event): void
{
// Your logic here...
// Log the resource access, add caching, etc.
}
}
resources/list: Lists all available direct (static) resources and their definitions.resources/templates/list: Lists all available templated resources and their definitions.resources/read: Retrieves a resource by its URI, automatically matching templated resources and extracting parameters.Prompts are reusable templates that can be dynamically generated and returned by the MCP server.
They are useful for providing context, instructions, or any structured message to clients, and can accept arguments for dynamic content.
#[AsPrompt] attribute to register your prompt.__invoke method, which receives an ArgumentCollection and returns a PromptResult.Argument class (name, description, required, allowUnsafe), within the #[AsPrompt] declaration.Example:
<?php
namespace App\Prompt;
use Ecourty\McpServerBundle\Attribute\AsPrompt;
use Ecourty\McpServerBundle\Enum\PromptRole;
use Ecourty\McpServerBundle\IO\Prompt\Content\TextContent;
use Ecourty\McpServerBundle\Prompt\Argument;
use Ecourty\McpServerBundle\IO\Prompt\PromptResult;
use Ecourty\McpServerBundle\IO\Prompt\PromptMessage;
use Ecourty\McpServerBundle\Prompt\ArgumentCollection;
#[AsPrompt(
name: 'code_review', // Unique identifier for the prompt,
description: 'Ask for a code review on a provided piece of code', // Description of the prompt
arguments: [
new Argument(name: 'code', description: 'The code snippets to review', required: true, allowUnsafe: true), // Required argument
new Argument(name: 'language', description: 'The name of the person to greet', required: false), // Optional argument
new Argument(name: 'reviewer_level', description: 'The level of review (senior / intermediate / junior...)', required: false), // Optional argument
],
)]
class CodeReviewPrompt
{
public function __invoke(ArgumentCollection $arguments): PromptResult
{
$code = $arguments->get('code');
$language = $arguments->get('language');
$reviewerLevel = $arguments->get('reviewer_level') ?: 'senior';
$systemMessage = <<<PROMPT
You are a $reviewerLevel code reviewer.
You will be provided with a piece of code in $language.
Your task is to review the code and provide feedback on its quality, readability, and any potential issues.
PROMPT;
$userMessage = <<<PROMPT
Review the following code snippet: $code
PROMPT;
return new PromptResult(
description: 'Code Review Prompt',
messages: [
new PromptMessage(
role: PromptRole::SYSTEM,
content: new TextContent($systemMessage),
),
new PromptMessage(
role: PromptRole::USER,
content: new TextContent($userMessage),
),
]
);
}
}
A prompt must return an instance of PromptResult, which contains:
description (string)PromptMessage objects (each with a role and content)Example:
return new PromptResult(
description: 'Greeting',
messages: [
new PromptMessage(role: PromptRole::SYSTEM, content: new TextContent('You are a friendly assistant.')),
new PromptMessage(role: PromptRole::USER, content: new TextContent('Hello, how are you?')),
]
);
The bundle provides several events for prompts:
PromptGetEvent: Dispatched before a prompt is generatedPromptResultEvent: Dispatched after a prompt is generatedPromptExceptionEvent: Dispatched if an error occurs during prompt generationExample of event listener:
use Ecourty\McpServerBundle\Event\Prompt\PromptExceptionEvent;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
#[AsEventListener(event: PromptExceptionEvent::class)]
class PromptGetListener
{
public function __invoke(PromptExceptionEvent $event): void
{
// Your logic here...
}
}
prompts/list: Lists all available prompts and their definitions.prompts/get: Retrieves and generates a prompt by name, with arguments.The bundle provides a robust system for handling JSON-RPC requests.
initialize
tools/list
tools/call
prompts/list
prompts/get
resources/list
resources/templates/list
resources/read
These methods are automatically registered and handled by the bundle. You don't need to implement them yourself.
You can create your own JSON-RPC method handlers for additional functionality:
MethodHandlerInterface#[AsMethodHandler] attribute to register your handlerExample:
<?php
use Ecourty\McpServerBundle\Attribute\AsMethodHandler;
use Ecourty\McpServerBundle\MethodHandler\MethodHandlerInterface;
#[AsMethodHandler(
method: 'my_method',
)]
class MyMethodHandler implements MethodHandlerInterface
{
public function handle(JsonRpcRequest $params): array
{
// Your request handling logic here
// ...
return ['result' => 'success'];
}
}
The #[AsMethodHandler] attribute supports:
method (string, required): The JSON-RPC method nameThe bundle provides several tools to help you during development:
debug:mcp-tools command helps you inspect and debug your MCP tools:# List all registered tools
php bin/console debug:mcp-tools
# Get detailed information about a specific tool
php bin/console debug:mcp-tools my_tool_name
This command is particularly useful for:
debug:mcp-prompts command helps you inspect and debug your MCP prompts:# List all registered prompts
php bin/console debug:mcp-prompts
# Get detailed information about a specific prompt
php bin/console debug:mcp-prompts my_prompt_name
This command is particularly useful for:
debug:mcp-resources command helps you inspect and debug your MCP resources:# List all registered resources
php bin/console debug:mcp-resources
# Get detailed information about a specific resource
php bin/console debug:mcp-resources my_resource_name
This command is particularly useful for:
Contributions to the MCP Server Bundle are welcome! Here's how you can help:
Please ensure your code follows our coding standards and includes appropriate tests.
composer install
Make your chages
Fix the code style and run PHPStan
composer fix-cs
composer phpstan
composer test
This bundle is licensed under the MIT License. See the LICENSE file for details.
No configuration available
Related projects feature coming soon
Will recommend related projects based on sub-categories