---
title: "Symfony bundles : How does it work ?"
date: 2026-02-02
description: What is a bundle in the Symfony ecosystem ? What role does it play and how does it work ?
categories:
  - symfony
  - php
---

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](https://github.com/lexik/LexikJWTAuthenticationBundle), which uses the [lcobucci/jwt](https://github.com/lcobucci/jwt) library to generate, analyze and validate JWT.

## Libraries & Bundles

About the above example, we won't directly install the [lcobucci/jwt](https://github.com/lcobucci/jwt) library. Of course, we could, but this library was not designed specifically for Symfony. In [the documentation](https://lcobucci-jwt.readthedocs.io/en/latest/), this is the first thing mentioned:

> `lcobucci/jwt` is 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](https://github.com/lexik/LexikJWTAuthenticationBundle), designed for Symfony, relies on the [lcobucci/jwt](https://github.com/lcobucci/jwt) library, we can also discover in the [Laravel](https://www.laravel.com) ecosystem that the package [tymon/jwt-auth](https://github.com/tymondesigns/jwt-auth) uses the [same library](https://github.com/tymondesigns/jwt-auth/blob/2.x/composer.json#L31) !

:::important

A bundle does not necessarily rely on a library. Initially, it's meant to provide reusable features. If there is actually a library behind it, then the role of the bundle is to ensure **its integration** into the framework.

In the `LexikJWTAuthenticationBundle`, once installed, we will be able to :

- run a command to generate our private/public key pair
- access a `/api/login_check` route to generate a JWT
- use the provided `JWTAuthenticator` or write your own class that inherits from it
- etc...

:::

### The FrameworkBundle

The most important bundle for our application, the base one, is the [`symfony/framework-bundle`](https://github.com/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](https://symfony.com/doc/7.4/setup.html#symfony-flex).

## Example : McpBundle

Let's explore the code of the [McpBundle](https://github.com/symfony/mcp-bundle), which allows us to create [MCP servers](https://modelcontextprotocol.io/docs/learn/server-concepts).

:::warning

This bundle is still **experimental** at the time of writing this article

:::

The McpBundle relies on the [MCP SDK](https://github.com/modelcontextprotocol/php-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**.

:::tip

What we will try to understand is **how** it makes the features provided by the MCP SDK available to a Symfony application

:::

### Entrypoint

Like the other bundles, the McpBundle follows the [PSR-4](/en/articles/psr#auto-loading) recommendation for loading classes.

The entrypoint of the bundle will be found in the class that inherits from the `AbstractBundle` class :

```php
<?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](https://symfony.com/doc/7.4/bundles/best_practices.html#bundles-naming-conventions), 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`](https://symfony.com/doc/7.4/bundles/extension.html#loading-services-directly-in-your-bundle-class) 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
<?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.

:::tip

In development environment (typically `APP_ENV=dev` or `kernel.debug` to `true`), another type of service will be registered : `TraceableRegistry`.

This service will provide information about the real instance of `Registry` to the Symfony profiler, where a dedicated section for MCP is available. It allows you to view the different elements available in the MCP server (tools, resources, prompts, etc...).

The `TraceableRegistry` service contains an instance of the `Registry` service : it's a [decorator](https://refactoring.guru/design-patterns/decorator).

:::

### PHP8 attributes

Still in the `loadExtension` method, we can find a call to a `registerMcpAttributes` method, which contains the following code :

```php
<?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](/en/articles/symfony-autoconfiguration) that use these attributes, to properly classify them in the service container.

:::tip

Configuration and definition of services and parameters is performed during the compilation of the container

:::

### `build`

Once services have been defined and tags applied, a **compiler pass** is executed. The `build` method will register it :

```php
//...
use Symfony\AI\McpBundle\DependencyInjection\McpPass;
//...

final class McpBundle extends AbstractBundle
{
    //...

    public function build(ContainerBuilder $container): void
    {
        $container->addCompilerPass(new McpPass());
    }

    //...
}
```

:::note

A [compiler pass](https://symfony.com/doc/7.4/service_container/compiler_passes.html) allows us to manipulate definitions of services that have already been registered in the container.

:::

Here, the tags previously applied are very important during the execution of [the `process` method](https://github.com/symfony/mcp-bundle/blob/main/src/DependencyInjection/McpPass.php) :

- Collect all services that have the `mcp.tool`, `mcp.prompt`, `mcp.resource` and `mcp.resource_template` tags
- Create a [service locator](https://symfony.com/doc/7.4/service_container/service_subscribers_locators.html) that will contain all these services
- Inject this locator into the `Server Builder` of 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`](https://github.com/symfony/mcp-bundle/blob/main/src/Controller/McpController.php), we find the controller that transfers the request to the MCP server
- In [`src/Routing/RouteLoader.php`](https://github.com/symfony/mcp-bundle/blob/main/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` :

```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 :

```json
"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](https://symfony.com/bundles) dedicated to various features, for instance :

- [Doctrine ORM integration](https://symfony.com/bundles/DoctrineBundle/current/index.html)
- [Two-factor authentication](https://symfony.com/bundles/SchebTwoFactorBundle/current/index.html)
- [Administration interface](https://symfony.com/bundles/EasyAdminBundle/current/index.html)
- [Tailwind integration with the `AssetMapper` component](https://symfony.com/bundles/TailwindBundle/current/index.html)

Each of them adds a feature to an application, by declaring routes, services, entities, parameters...
