Skip to content

Set up an MCP server

JWT

MCP servers use JWT for authentication.

TODO: Enable authorization header in config/packages/lexik_jwt_authentication.yaml.

In config/packages/security.yaml, uncomment the ibexa_jwt_mcp firewall.

TODO: Config to get a JWT token in the first place. Through REST, GraphQL or something else?

MCP Server configuration

MCP servers are configured per repository then enabled per SiteAccess scope.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
ibexa:
    repositories:
        <repository_identifier>:
            mcp:
                <server_identifier>:
                    path: <server_route_path>
                    enabled: true
                    # Server options…
                    discovery_cache: <cache_pool_service>
                    session:
                        type: <psr16|file|memory>
                        # Session options…
    system:
        <siteaccess_scope>:
            mcp:
                servers:
                    - <server_identifier>

TODO: ddev php bin/console debug:router --siteaccess=<within_scope_siteaccess> should list some ibexa.mcp.<server_identifier> GET|POST|DELETE|OPTIONS <server_route_path>

TODO: Maybe explain that routes are built automatically from MCP server path configs thank to config/routes/ibexa_mcp.yaml and \Ibexa\Bundle\Mcp\Routing\McpRouteLoader

MCP server options

Option Type Required Default Description
path string Yes MCP server endpoint path
enabled boolean No false Whether the server is enabled
version string No 1.0.0 MCP server version
description string No null Human-readable server description
instructions string No null Instructions dedicated for LLM interaction
discovery_cache string Yes PSR-6 ou PSR-16 cache pool service identifier
session object Yes Session storage configuration

Notice that a server is disabled by default, it needs to be explicitly enabled.

MCP server discovery cache

TODO

MCP server session storage

Options

Option Type Default Description
type enum memory Session store type: psr16, file, or memory
service string null PSR-16 cache service ID for psr16 session store
prefix string mcp_ Key prefix for psr16 session store
directory string null Directory path for file session store
ttl integer 3600 Session TTL in seconds

PSR-16

Sessions are stored using a PSR-16 compatible cache implementation. Requires service option pointing to a valid cache service ID.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
                    session:
                        type: psr16
                        service: cache.redis.mcp
                        prefix: 'mcp_<server_identifier>_'
services:
    cache.redis.mcp:
        public: true
        class: Symfony\Component\Cache\Adapter\RedisTagAwareAdapter
        parent: cache.adapter.redis
        tags:
            -   name: cache.pool
                clearer: cache.app_clearer
                provider: 'redis://mcp.redis:6379'
                namespace: 'mcp'

File

Sessions are persisted to the filesystem. Requires directory option to be set.

1
2
3
                    session:
                        type: file
                        directory: '%kernel.cache_dir%/mcp/sessions'

Memory

Sessions are stored in memory. Suitable for development and STDIO transport.

TODO: Might not work with DDEV or Docker

1
2
                    session:
                        type: memory

MCP server capabilities

TODO: Ibexa\Contracts\Mcp\McpCapabilityInterface

TODO: Ibexa\Contracts\Mcp\Attribute namespace

Example

This example introduce an example MCP server with a single greet tool. It's enabled on all SiteAccesses. It's accessible with the path /mcp/example (for example, on http://localhost/mcp/example and http://localhost/admin/mcp/example). It uses files for both discovery cache and session storage.

In a new config/packages/mcp.yaml file, the configuration of the MCP server:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
ibexa:
    repositories:
        default:
            mcp:
                example:
                    path: /mcp/example
                    enabled: true
                    description: 'Example MCP Server'
                    instructions: 'Use this server to greet someone.'
                    discovery_cache: cache.tagaware.filesystem
                    session:
                        type: file
                        directory: '%kernel.cache_dir%/mcp/sessions'
    system:
        default:
            mcp:
                servers:
                    - example

Then, a McpCapabilityInterfacecontaining a greet function with a McpTool attribute associating with the example server:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?php declare(strict_types=1);

namespace App\mcp\src\Mcp;

use Ibexa\Contracts\Mcp\Attribute\McpTool;
use Ibexa\Contracts\Mcp\McpCapabilityInterface;

final readonly class ExampleTools implements McpCapabilityInterface
{
    #[McpTool(servers: ['example'], description: 'Greet a user by name')]
    public function greet(string $name): string
    {
        return sprintf('Hello, %s!', $name);
    }
}

To check the server configuration, a short command using the MCP server configuration registry (injected through McpServerConfigurationRegistryInterface and autowiring):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php declare(strict_types=1);

namespace App\mcp\src\Command;

use Ibexa\Contracts\Mcp\McpServerConfigurationRegistryInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Style\SymfonyStyle;

#[AsCommand(name: 'app:mcp:server_list', description: 'List MCP servers')]
class McpServerListCommand
{
    public function __construct(private readonly McpServerConfigurationRegistryInterface $configRegistry)
    {
    }

    public function __invoke(SymfonyStyle $io): int
    {
        foreach($this->configRegistry->getServerConfigurations() as $serverConfiguration) {
            $io->title($serverConfiguration->identifier);
            dump($serverConfiguration);
        }

        return Command::SUCCESS;
    }
}

To test the example MCP server, a sequence of curl commands is used to simulate an AI to MCP server communication.

  • Ask for a JWT token through REST
  • Initialize a connection to the MCP server
  • Validate the MCP Session ID
  • List the available tools
  • Call a tool

jq, grep, and sed are also used to parse or display outputs.

The initialization:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
baseUrl='http://localhost' # Adapt to your test case

jwtToken=$(curl -s -X 'POST' \
  "$baseUrl/api/ibexa/v2/user/token/jwt" \
  -H 'Content-Type: application/json' \
  -d '{
        "JWTInput": {
          "_media-type": "application/vnd.ibexa.api.JWTInput",
          "username": "admin",
          "password": "publish"
        }
      }' | jq -r .JWT.token)

mcpSessionId=$(curl -s -i -X 'POST' "$baseUrl/mcp/example" \
  -H "Authorization: Bearer $jwtToken" \
  -d '{
        "jsonrpc": "2.0",
        "id": 1,
        "method": "initialize",
        "params": {
          "protocolVersion": "2025-03-26",
          "capabilities": {},
          "clientInfo": {
            "name": "test-curl-client",
            "version": "1.0.0"
          }
        }
      }' | grep 'Mcp-Session-Id:' | sed 's/Mcp-Session-Id: \([0-9a-f-]*\).*/\1/')

curl -s -i -X 'POST' "$baseUrl/mcp/example" \
  -H "Authorization: Bearer $jwtToken" \
  -H "Mcp-Session-Id: $mcpSessionId" \
  -d '{
        "jsonrpc": "2.0",
        "method": "notifications/initialized"
      }'
1
2
3
4
HTTP/1.1 202 Accepted
Access-Control-Allow-Headers: Content-Type, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID, Authorization, Accept
Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS
Access-Control-Expose-Headers: Mcp-Session-Id

The list of tools:

1
2
3
4
5
6
7
8
curl -s -X 'POST' "$baseUrl/mcp/example" \
  -H "Authorization: Bearer $jwtToken" \
  -H "Mcp-Session-Id: $mcpSessionId" \
  -d '{
        "jsonrpc": "2.0",
        "id": 2,
        "method": "tools/list"
      }' | jq
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "tools": [
      {
        "name": "greet",
        "inputSchema": {
          "type": "object",
          "properties": {
            "name": {
              "type": "string"
            }
          },
          "required": [
            "name"
          ]
        },
        "description": "Greet a user by name"
      }
    ]
  }
}

The greet tool usage:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
curl -s -X 'POST' "$baseUrl/mcp/example" \
  -H "Authorization: Bearer $jwtToken" \
  -H "Mcp-Session-Id: $mcpSessionId" \
  -d '{
        "jsonrpc": "2.0",
        "id": 3,
        "method": "tools/call",
        "params": {
          "name": "greet",
          "arguments": {
            "name": "World"
          }
        }
      }' | jq
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Hello, World!"
      }
    ],
    "isError": false
  }
}

TODO: Connect an AI client to the MCP server. Copilot CLI MCP server addition is strangely asking for some OAuth ID even with a proper JWT/Bearer header.