In a Symfony application, bundles allow you to install and use features that have already been developed, without having to rewrite them yourself.
To be able to use the features it provides, the bundle will register services, parameters, routes, etc… in our application.
For example, if we want to implement a JWT authentication, we can install the LexikJWTAuthenticationBundle, which uses the lcobucci/jwt library to generate, analyze and validate JWT.
Libraries & Bundles
About the above example, we won’t directly install the lcobucci/jwt library. Of course, we could, but this library was not designed specifically for Symfony. In the documentation, this is the first thing mentioned:
lcobucci/jwtis a framework-agnostic PHP library that allows you to issue, parse, and validate JSON Web Tokens based on the RFC 7519.
“framework-agnostic” means that this library is not specific to any framework. In fact, it can therefore facilitate its integration into any framework.
Consequently, just like the LexikJWTAuthenticationBundle, designed for Symfony, relies on the lcobucci/jwt library, we can also discover in the Laravel ecosystem that the package tymon/jwt-auth uses the same library !
The FrameworkBundle
The most important bundle for our application, the base one, is the symfony/framework-bundle : it will gather the required components and initialize everything necessary for the application to work : the service container, the configuration of the other bundles and components in YAML format, the event system, the console commands, etc…
Enabling a bundle
Each bundle is enabled by environment in a Symfony application, in the config/bundles.php file. When we install a bundle with composer require ..., the bundle is automatically added to the file by Symfony Flex.
Example : McpBundle
Let’s explore the code of the McpBundle, which allows us to create MCP servers.
The McpBundle relies on the MCP SDK. In the SDK description, we can read :
[…] It provides a framework-agnostic API for implementing MCP servers and clients in PHP.
The purpose of the McpBundle is not to implement the mechanism required by the protocol (it’s already there in the SDK), but to make the SDK’s features available within a Symfony application.
Entrypoint
Like the other bundles, the McpBundle follows the PSR-4 recommendation for loading classes.
The entrypoint of the bundle will be found in the class that inherits from the AbstractBundle class :
<?php
// src/McpBundle.php
//...
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;
//...
final class McpBundle extends AbstractBundle
{
//...
}
This class will be registered and enabled in the config/bundles.php file.
About naming, the bundle follows Symfony’s best practices, for example, suffixing the name with Bundle, not exceeding 2 words…
In this class, we will focus on 3 methods : configure, loadExtension and build.
Configuration
The configure method allows us to define the configuration format of the bundle (properties, default values…).
In McpBundle, we load the config/options.php file which contains the entire structure that we will then apply in config/packages/mcp.yaml, within a Symfony application.
When we install the bundle in an application, it is therefore possible to change various parameters that will control the execution of the bundle’s logic.
Configuration is meant for this : changing the values of parameters without touching the code.
loadExtension
After the bundle’s configuration is loaded, loadExtension will declare the services and parameters that will be integrated into the container.
config/services.php is imported and executed, it only returns a function that is responsible for defining the services. For example :
<?php
// config/services.php
//...
use Mcp\Capability\Registry;
//...
return static function (ContainerConfigurator $container): void {
$container->services()
->set('mcp.registry', Registry::class)
->args([service('event_dispatcher'), service('logger')])
->tag('monolog.logger', ['channel' => 'mcp'])
// other services definitions
;
};
The Registry class comes from the MCP SDK : we are creating the connection with the Symfony framework. The service container, in production, will have a mcp.registry service available (->set('mcp.registry', Registry::class)). This service will contain all the tools, resources, resource templates and prompts we want to declare in our MCP server.
PHP8 attributes
Still in the loadExtension method, we can find a call to a registerMcpAttributes method, which contains the following code :
<?php
//...
final class McpBundle extends AbstractBundle
{
//...
private function registerMcpAttributes(ContainerBuilder $builder): void
{
$mcpAttributes = [
McpTool::class => 'mcp.tool',
McpPrompt::class => 'mcp.prompt',
McpResource::class => 'mcp.resource',
McpResourceTemplate::class => 'mcp.resource_template',
];
foreach ($mcpAttributes as $attributeClass => $tag) {
$builder->registerAttributeForAutoconfiguration(
$attributeClass,
static function (ChildDefinition $definition, object $attribute, \Reflector $reflector) use ($tag): void {
$definition->addTag($tag);
}
);
}
}
//...
}
Each of the McpTool, McpPrompt, McpResource and McpResourceTemplate classes is a PHP8 attribute coming from the MCP SDK. Here, each attribute is therefore associated with a tag, which will then be used during autoconfiguration of classes that use these attributes, to properly classify them in the service container.
build
Once services have been defined and tags applied, a compiler pass is executed. The build method will register it :
//...
use Symfony\AI\McpBundle\DependencyInjection\McpPass;
//...
final class McpBundle extends AbstractBundle
{
//...
public function build(ContainerBuilder $container): void
{
$container->addCompilerPass(new McpPass());
}
//...
}
Here, the tags previously applied are very important during the execution of the process method :
- Collect all services that have the
mcp.tool,mcp.prompt,mcp.resourceandmcp.resource_templatetags - Create a service locator that will contain all these services
- Inject this locator into the
Server Builderof the MCP SDK, as a PSR-11 service container
Route
In addition to services, autoconfiguration or registered parameters, the McpBundle defines a controller to handle requests to the MCP server :
- In
src/Controller/McpController.php, we find the controller that transfers the request to the MCP server - In
src/Routing/RouteLoader.php, we will have the logic that registers this controller with the Symfony router
The declared route type is simply mcp, so in a Symfony project, to activate this loader, in config/routes.yaml :
mcp:
resource: .
type: mcp
Cursor
The route is accessible behind /_mcp URL, so when the server is started, if we are in HTTP mode (STDIO is also supported), in Cursor we can add the followingto the configuration :
"Symfony MCP": {
"type": "http",
"url": "http://localhost:8000/_mcp"
}
Then, we will see all the tools, prompts, resources and resource templates written in the Symfony application.
Summary
There are many bundles dedicated to various features, for instance :
- Doctrine ORM integration
- Two-factor authentication
- Administration interface
- Tailwind integration with the
AssetMappercomponent
Each of them adds a feature to an application, by declaring routes, services, entities, parameters…