Skip to content

IBX-11536: MCP Servers#3106

Open
adriendupuis wants to merge 91 commits into
5.0from
mcp
Open

IBX-11536: MCP Servers#3106
adriendupuis wants to merge 91 commits into
5.0from
mcp

Conversation

@adriendupuis
Copy link
Copy Markdown
Contributor

@adriendupuis adriendupuis commented Mar 26, 2026

Question Answer
JIRA Ticket IBX-11068 > IBX-11536
Versions 5.0
Edition All

Document built-in MCP Servers and how to create custom ones.
Also enhance the JWT documentation.

Related PRs:

Checklist

  • Text renders correctly
  • Text has been checked with vale
  • Description metadata is up to date
  • Redirects cover removed/moved pages
  • Code samples are working
  • PHP code samples have been fixed with PHP CS fixer
  • Added link to this PR in relevant JIRA ticket or code PR

Comment thread composer.json Outdated
Comment thread mkdocs.yml Outdated
Apply SonarCloud Code Analysis warning's suggestion
Comment thread docs/ai/mcp/mcp_guide.md Outdated
@adriendupuis adriendupuis changed the title IBX-11068: MCP Servers IBX-11536: MCP Servers Mar 27, 2026
Comment thread docs/ai/mcp/mcp_config.md Outdated
Comment thread code_samples/mcp/config/packages/mcp.yaml Outdated
Comment thread docs/ai/mcp/mcp_config.md Outdated
Comment thread code_samples/mcp/src/Mcp/ExampleTools.php Outdated
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 5, 2026

Quality Gate Failed Quality Gate failed

Failed conditions
18 Security Hotspots
43.6% Duplication on New Code (required ≤ 3%)
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 5, 2026

code_samples/ change report

Before (on target branch)After (in current PR)

code_samples/mcp/config/packages/mcp.yaml


code_samples/mcp/config/packages/mcp.yaml

docs/ai/mcp/mcp_config.md@219:``` yaml
docs/ai/mcp/mcp_config.md@220:[[= include_code('code_samples/mcp/config/packages/mcp.yaml') =]]
docs/ai/mcp/mcp_config.md@221:```

001⫶ibexa:
002⫶ repositories:
003⫶ default:
004⫶ mcp:
005⫶ example:
006⫶ path: /mcp/example
007⫶ enabled: true
008⫶ description: 'Example MCP Server'
009⫶ instructions: 'Use this server to greet someone.'
010⫶ discovery_cache: cache.tagaware.filesystem
011⫶ session:
012⫶ type: psr16
013⫶ directory: cache.tagaware.filesystem
014⫶ system:
015⫶ default:
016⫶ mcp:
017⫶ servers:
018⫶ - example


code_samples/mcp/http.mcp.json


code_samples/mcp/http.mcp.json

docs/ai/mcp/mcp_config.md@453:``` json
docs/ai/mcp/mcp_config.md@454:[[= include_code('code_samples/mcp/http.mcp.json') =]]
docs/ai/mcp/mcp_config.md@455:```

001⫶{
002⫶ "mcpServers": {
003⫶ "ibexa-example": {
004⫶ "type": "http",
005⫶ "url": "http://localhost/mcp/example",
006⫶ "headers": {
007⫶ "Authorization": "Bearer <JWT token>"
008⫶ },
009⫶ "tools": ["*"]
010⫶ }
011⫶ }
012⫶}


code_samples/mcp/mcp-ibexa-example-wrapper.sh


code_samples/mcp/mcp-ibexa-example-wrapper.sh

docs/ai/mcp/mcp_config.md@480:``` bash
docs/ai/mcp/mcp_config.md@481:[[= include_code('code_samples/mcp/mcp-ibexa-example-wrapper.sh') =]]
docs/ai/mcp/mcp_config.md@482:```

001⫶#!/bin/bash
002⫶set -e
003⫶
004⫶baseUrl='http://localhost' # Adapt to your test case
005⫶
006⫶jwtToken=$(curl -s -X 'POST' \
007⫶ "$baseUrl/api/ibexa/v2/user/token/jwt" \
008⫶ -H 'Content-Type: application/vnd.ibexa.api.JWTInput+json' \
009⫶ -H 'Accept: application/vnd.ibexa.api.JWT+json' \
010⫶ -d '{
011⫶ "JWTInput": {
012⫶ "_media-type": "application/vnd.ibexa.api.JWTInput+json",
013⫶ "username": "ibexa-example",
014⫶ "password": "Ibexa-3xample"
015⫶ }
016⫶ }' | jq -r .JWT.token)
017⫶
018⫶exec npx -y supergateway \
019⫶ --streamableHttp "$baseUrl/mcp/example" \
020⫶ --oauth2Bearer "$jwtToken" \
021⫶ --logLevel none


code_samples/mcp/mcp.matrix.yaml


code_samples/mcp/mcp.matrix.yaml

docs/ai/mcp/mcp_config.md@42:``` yaml
docs/ai/mcp/mcp_config.md@43:[[= include_code('code_samples/mcp/mcp.matrix.yaml', 1, 8) =]]
docs/ai/mcp/mcp_config.md@44:[[= include_code('code_samples/mcp/mcp.matrix.yaml', 12, 15) =]]
docs/ai/mcp/mcp_config.md@45:[[= include_code('code_samples/mcp/mcp.matrix.yaml', 29, 33) =]]
docs/ai/mcp/mcp_config.md@46:```

001⫶ibexa:
002⫶ repositories:
003⫶ <repository_identifier>:
004⫶ mcp:
005⫶ <server_identifier>:
006⫶ path: <server_route_path>
007⫶ enabled: true
008⫶ # Server options…
009⫶ discovery_cache: <cache_pool_service>
010⫶ session:
011⫶ type: <psr16|file|memory>
012⫶ # Session options…
013⫶ system:
014⫶ <siteaccess_scope>:
015⫶ mcp:
016⫶ servers:
017⫶ - <server_identifier>

docs/ai/mcp/mcp_config.md@93:``` yaml
docs/ai/mcp/mcp_config.md@94:[[= include_code('code_samples/mcp/mcp.matrix.yaml', 9, 11) =]]
docs/ai/mcp/mcp_config.md@95:```

001⫶ tools:
002⫶ - Ibexa\Mcp\Tool\TranslationTools
003⫶ - Ibexa\Mcp\Tool\SeoTools

docs/ai/mcp/mcp_config.md@104:``` yaml
docs/ai/mcp/mcp_config.md@105:[[= include_code('code_samples/mcp/mcp.matrix.yaml', 17, 17) =]]
docs/ai/mcp/mcp_config.md@106:```

001⫶ discovery_cache: cache.redis.mcp

docs/ai/mcp/mcp_config.md@128:``` yaml
docs/ai/mcp/mcp_config.md@129:[[= include_code('code_samples/mcp/mcp.matrix.yaml', 18, 21) =]]
docs/ai/mcp/mcp_config.md@130:[[= include_code('code_samples/mcp/mcp.matrix.yaml', 34, 43) =]]
docs/ai/mcp/mcp_config.md@131:```

001⫶ session:
002⫶ type: psr16
003⫶ service: cache.redis.mcp
004⫶ prefix: 'mcp_<server_identifier>_'
005⫶services:
006⫶ cache.redis.mcp:
007⫶ public: true
008⫶ class: Symfony\Component\Cache\Adapter\RedisTagAwareAdapter
009⫶ parent: cache.adapter.redis
010⫶ tags:
011⫶ - name: cache.pool
012⫶ clearer: cache.app_clearer
013⫶ provider: 'redis://mcp.redis:6379'
014⫶ namespace: 'mcp'

docs/ai/mcp/mcp_config.md@140:``` yaml
docs/ai/mcp/mcp_config.md@141:[[= include_code('code_samples/mcp/mcp.matrix.yaml', 23, 25) =]]
docs/ai/mcp/mcp_config.md@142:```

001⫶ session:
002⫶ type: file
003⫶ directory: '%kernel.cache_dir%/mcp/sessions'

docs/ai/mcp/mcp_config.md@149:``` yaml
docs/ai/mcp/mcp_config.md@150:[[= include_code('code_samples/mcp/mcp.matrix.yaml', 27, 28) =]]
docs/ai/mcp/mcp_config.md@151:```

001⫶ session:
002⫶ type: memory


code_samples/mcp/mcp.sh


code_samples/mcp/mcp.sh

docs/ai/mcp/mcp_config.md@282:``` bash
docs/ai/mcp/mcp_config.md@283:[[= include_code('code_samples/mcp/mcp.sh', 5, 7) =]]
docs/ai/mcp/mcp_config.md@284:```

001⫶baseUrl='http://localhost' # Adapt to your test case
002⫶username='ibexa-example'
003⫶password='Ibexa-3xample'

docs/ai/mcp/mcp_config.md@288:``` bash
docs/ai/mcp/mcp_config.md@289:[[= include_code('code_samples/mcp/mcp.sh', 9, 23) =]]
docs/ai/mcp/mcp_config.md@290:```

001⫶curl -s -X 'POST' \
002⫶ "$baseUrl/api/ibexa/v2/user/token/jwt" \
003⫶ -H 'Content-Type: application/vnd.ibexa.api.JWTInput+json' \
004⫶ -H 'Accept: application/vnd.ibexa.api.JWT+json' \
005⫶ -d "{
006⫶ \"JWTInput\": {
007⫶ \"_media-type\": \"application/vnd.ibexa.api.JWTInput+json\",
008⫶ \"username\": \"$username\",
009⫶ \"password\": \"$password\"
010⫶ }
011⫶ }" > response.tmp.txt
012⫶
013⫶cat response.tmp.txt | jq
014⫶jwtToken=$(cat response.tmp.txt | jq -r .JWT.token)
015⫶rm response.tmp.txt

docs/ai/mcp/mcp_config.md@292:``` json
docs/ai/mcp/mcp_config.md@293:[[= include_code('code_samples/mcp/mcp.sh.output.txt', 1, 7) =]]
docs/ai/mcp/mcp_config.md@294:```

001⫶{
002⫶ "JWT": {
003⫶ "_media-type": "application/vnd.ibexa.api.JWT+json",
004⫶ "_token": "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ.abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyz1234567890ABCD.EFGHIJKL-MNOPQRSTUVWXYZ12345678901234567890",
005⫶ "token": "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ.abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyz1234567890ABCD.EFGHIJKL-MNOPQRSTUVWXYZ12345678901234567890"
006⫶ }
007⫶}

docs/ai/mcp/mcp_config.md@298:``` bash
docs/ai/mcp/mcp_config.md@299:[[= include_code('code_samples/mcp/mcp.sh', 21, 44) =]]
docs/ai/mcp/mcp_config.md@300:```

001⫶cat response.tmp.txt | jq
002⫶jwtToken=$(cat response.tmp.txt | jq -r .JWT.token)
003⫶rm response.tmp.txt
004⫶
005⫶curl -s -i -X 'POST' "$baseUrl/mcp/example" \
006⫶ -H "Authorization: Bearer $jwtToken" \
007⫶ -d '{
008⫶ "jsonrpc": "2.0",
009⫶ "id": 1,
010⫶ "method": "initialize",
011⫶ "params": {
012⫶ "protocolVersion": "2025-03-26",
013⫶ "capabilities": {},
014⫶ "clientInfo": {
015⫶ "name": "test-curl-client",
016⫶ "version": "1.0.0"
017⫶ }
018⫶ }
019⫶ }' > response.tmp.txt
020⫶
021⫶sed '$d' response.tmp.txt
022⫶tail -n 1 response.tmp.txt | jq
023⫶mcpSessionId=$(cat response.tmp.txt | grep 'Mcp-Session-Id:' | sed 's/Mcp-Session-Id: \([0-9a-f-]*\).*/\1/')
024⫶rm response.tmp.txt

docs/ai/mcp/mcp_config.md@302:``` http
docs/ai/mcp/mcp_config.md@303:[[= include_code('code_samples/mcp/mcp.sh.output.txt', 8, 16) =]]
docs/ai/mcp/mcp_config.md@304:```

001⫶HTTP/1.1 200 OK
002⫶Access-Control-Allow-Headers: Content-Type, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID, Authorization, Accept
003⫶Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS
004⫶Access-Control-Allow-Origin: *
005⫶Access-Control-Expose-Headers: Mcp-Session-Id
006⫶Cache-Control: no-cache, private
007⫶Content-Type: application/json
008⫶Date: Tue, 28 Apr 2026 09:53:27 GMT
009⫶Mcp-Session-Id: 12345678-9abc-def0-1234-56789abcdef0

docs/ai/mcp/mcp_config.md@306:``` json
docs/ai/mcp/mcp_config.md@307:[[= include_code('code_samples/mcp/mcp.sh.output.txt', 26, 51) =]]
docs/ai/mcp/mcp_config.md@308:```

001⫶{
002⫶ "jsonrpc": "2.0",
003⫶ "id": 1,
004⫶ "result": {
005⫶ "protocolVersion": "2025-06-18",
006⫶ "capabilities": {
007⫶ "logging": {},
008⫶ "completions": {},
009⫶ "prompts": {
010⫶ "listChanged": true
011⫶ },
012⫶ "resources": {
013⫶ "listChanged": true
014⫶ },
015⫶ "tools": {
016⫶ "listChanged": true
017⫶ }
018⫶ },
019⫶ "serverInfo": {
020⫶ "name": "example",
021⫶ "version": "1.0.0",
022⫶ "description": "Example MCP Server"
023⫶ },
024⫶ "instructions": "Use this server to greet someone."
025⫶ }
026⫶}

docs/ai/mcp/mcp_config.md@312:``` bash
docs/ai/mcp/mcp_config.md@313:[[= include_code('code_samples/mcp/mcp.sh', 46, 52) =]]
docs/ai/mcp/mcp_config.md@314:```

001⫶curl -s -i -X 'POST' "$baseUrl/mcp/example" \
002⫶ -H "Authorization: Bearer $jwtToken" \
003⫶ -H "Mcp-Session-Id: $mcpSessionId" \
004⫶ -d '{
005⫶ "jsonrpc": "2.0",
006⫶ "method": "notifications/initialized"
007⫶ }'

docs/ai/mcp/mcp_config.md@316:``` http
docs/ai/mcp/mcp_config.md@317:[[= include_code('code_samples/mcp/mcp.sh.output.txt', 52, 56) =]]
docs/ai/mcp/mcp_config.md@318:```

001⫶HTTP/1.1 202 Accepted
002⫶Access-Control-Allow-Headers: Content-Type, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID, Authorization, Accept
003⫶Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS
004⫶Access-Control-Allow-Origin: *
005⫶Access-Control-Expose-Headers: Mcp-Session-Id

docs/ai/mcp/mcp_config.md@322:``` bash
docs/ai/mcp/mcp_config.md@323:[[= include_code('code_samples/mcp/mcp.sh', 54, 61) =]]
docs/ai/mcp/mcp_config.md@324:```

001⫶curl -s -X 'POST' "$baseUrl/mcp/example" \
002⫶ -H "Authorization: Bearer $jwtToken" \
003⫶ -H "Mcp-Session-Id: $mcpSessionId" \
004⫶ -d '{
005⫶ "jsonrpc": "2.0",
006⫶ "id": 2,
007⫶ "method": "tools/list"
008⫶ }' | jq

docs/ai/mcp/mcp_config.md@326:``` json
docs/ai/mcp/mcp_config.md@327:[[= include_code('code_samples/mcp/mcp.sh.output.txt', 69, 128) =]]
docs/ai/mcp/mcp_config.md@328:```

001⫶{
002⫶ "jsonrpc": "2.0",
003⫶ "id": 2,
004⫶ "result": {
005⫶ "tools": [
006⫶ {
007⫶ "name": "greet",
008⫶ "inputSchema": {
009⫶ "type": "object",
010⫶ "properties": {
011⫶ "name": {
012⫶ "type": "string",
013⫶ "description": "The name of the person to greet"
014⫶ }
015⫶ },
016⫶ "required": [
017⫶ "name"
018⫶ ]
019⫶ },
020⫶ "description": "Greet a user by name",
021⫶ "annotations": {
022⫶ "readOnlyHint": true,
023⫶ "destructiveHint": false,
024⫶ "idempotentHint": true,
025⫶ "openWorldHint": false
026⫶ },
027⫶ "icons": [
028⫶ {
029⫶ "src": "https://openmoji.org/data/color/svg/1F44B.svg"
030⫶ }
031⫶ ],
032⫶ "outputSchema": {
033⫶ "type": "object",
034⫶ "properties": {
035⫶ "general": {
036⫶ "type": "string",
037⫶ "description": "the safe way to greet someone"
038⫶ },
039⫶ "close": {
040⫶ "type": "string",
041⫶ "description": "when you're close to the person, like friends or relatives"
042⫶ },
043⫶ "morning": {
044⫶ "type": "string",
045⫶ "description": "when it's in the morning"
046⫶ },
047⫶ "afternoon": {
048⫶ "type": "string",
049⫶ "description": "when it's the afternoon"
050⫶ },
051⫶ "evening": {
052⫶ "type": "string",
053⫶ "description": "when it's late in the day"
054⫶ }
055⫶ }
056⫶ }
057⫶ }
058⫶ ]
059⫶ }
060⫶}

docs/ai/mcp/mcp_config.md@332:``` bash
docs/ai/mcp/mcp_config.md@333:[[= include_code('code_samples/mcp/mcp.sh', 63, 76) =]]
docs/ai/mcp/mcp_config.md@334:```

001⫶curl -s -X 'POST' "$baseUrl/mcp/example" \
002⫶ -H "Authorization: Bearer $jwtToken" \
003⫶ -H "Mcp-Session-Id: $mcpSessionId" \
004⫶ -d '{
005⫶ "jsonrpc": "2.0",
006⫶ "id": 3,
007⫶ "method": "tools/call",
008⫶ "params": {
009⫶ "name": "greet",
010⫶ "arguments": {
011⫶ "name": "World"
012⫶ }
013⫶ }
014⫶ }' | jq

docs/ai/mcp/mcp_config.md@336:``` json
docs/ai/mcp/mcp_config.md@337:[[= include_code('code_samples/mcp/mcp.sh.output.txt', 129, 148) =]]
docs/ai/mcp/mcp_config.md@338:```

001⫶{
002⫶ "jsonrpc": "2.0",
003⫶ "id": 3,
004⫶ "result": {
005⫶ "content": [
006⫶ {
007⫶ "type": "text",
008⫶ "text": "{\n \"general\": \"Hello, World!\",\n \"close\": \"Hey, World!\",\n \"morning\": \"Good morning, World!\",\n \"afternoon\": \"Good afternoon, World!\",\n \"evening\": \"Good evening, World!\"\n}"
009⫶ }
010⫶ ],
011⫶ "isError": false,
012⫶ "structuredContent": {
013⫶ "general": "Hello, World!",
014⫶ "close": "Hey, World!",
015⫶ "morning": "Good morning, World!",
016⫶ "afternoon": "Good afternoon, World!",
017⫶ "evening": "Good evening, World!"
018⫶ }
019⫶ }
020⫶}

docs/ai/mcp/mcp_config.md@342:``` bash
docs/ai/mcp/mcp_config.md@343:[[= include_code('code_samples/mcp/mcp.sh', 78, 85) =]]
docs/ai/mcp/mcp_config.md@344:```

001⫶curl -s -X 'POST' "$baseUrl/mcp/example" \
002⫶ -H "Authorization: Bearer $jwtToken" \
003⫶ -H "Mcp-Session-Id: $mcpSessionId" \
004⫶ -d '{
005⫶ "jsonrpc": "2.0",
006⫶ "id": 4,
007⫶ "method": "prompts/list"
008⫶ }' | jq

docs/ai/mcp/mcp_config.md@346:``` json
docs/ai/mcp/mcp_config.md@347:[[= include_code('code_samples/mcp/mcp.sh.output.txt', 149, 172) =]]
docs/ai/mcp/mcp_config.md@348:```

001⫶{
002⫶ "jsonrpc": "2.0",
003⫶ "id": 4,
004⫶ "result": {
005⫶ "prompts": [
006⫶ {
007⫶ "name": "greet",
008⫶ "description": "Prompt to be greeted by the `greet` tool",
009⫶ "arguments": [
010⫶ {
011⫶ "name": "name",
012⫶ "description": "The name you want to be greeted by",
013⫶ "required": true
014⫶ }
015⫶ ],
016⫶ "icons": [
017⫶ {
018⫶ "src": "https://openmoji.org/data/color/svg/1F91D.svg"
019⫶ }
020⫶ ]
021⫶ }
022⫶ ]
023⫶ }
024⫶}

docs/ai/mcp/mcp_config.md@352:``` bash
docs/ai/mcp/mcp_config.md@353:[[= include_code('code_samples/mcp/mcp.sh', 87, 100) =]]
docs/ai/mcp/mcp_config.md@354:```

001⫶curl -s -X 'POST' "$baseUrl/mcp/example" \
002⫶ -H "Authorization: Bearer $jwtToken" \
003⫶ -H "Mcp-Session-Id: $mcpSessionId" \
004⫶ -d '{
005⫶ "jsonrpc": "2.0",
006⫶ "id": 5,
007⫶ "method": "prompts/get",
008⫶ "params": {
009⫶ "name": "greet",
010⫶ "arguments": {
011⫶ "name": "Firstname Lastname"
012⫶ }
013⫶ }
014⫶ }' | jq

docs/ai/mcp/mcp_config.md@356:``` json
docs/ai/mcp/mcp_config.md@357:[[= include_code('code_samples/mcp/mcp.sh.output.txt', 173, 187) =]]
docs/ai/mcp/mcp_config.md@358:```

001⫶{
002⫶ "jsonrpc": "2.0",
003⫶ "id": 5,
004⫶ "result": {
005⫶ "messages": [
006⫶ {
007⫶ "role": "user",
008⫶ "content": {
009⫶ "type": "text",
010⫶ "text": "Hi. My name is Firstname Lastname. Please, greet me."
011⫶ }
012⫶ }
013⫶ ]
014⫶ }
015⫶}


code_samples/mcp/mcp.sh.output.txt


code_samples/mcp/mcp.sh.output.txt

docs/ai/mcp/mcp_config.md@292:``` json
docs/ai/mcp/mcp_config.md@293:[[= include_code('code_samples/mcp/mcp.sh.output.txt', 1, 7) =]]
docs/ai/mcp/mcp_config.md@294:```

001⫶{
002⫶ "JWT": {
003⫶ "_media-type": "application/vnd.ibexa.api.JWT+json",
004⫶ "_token": "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ.abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyz1234567890ABCD.EFGHIJKL-MNOPQRSTUVWXYZ12345678901234567890",
005⫶ "token": "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ.abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyz1234567890ABCD.EFGHIJKL-MNOPQRSTUVWXYZ12345678901234567890"
006⫶ }
007⫶}

docs/ai/mcp/mcp_config.md@302:``` http
docs/ai/mcp/mcp_config.md@303:[[= include_code('code_samples/mcp/mcp.sh.output.txt', 8, 16) =]]
docs/ai/mcp/mcp_config.md@304:```

001⫶HTTP/1.1 200 OK
002⫶Access-Control-Allow-Headers: Content-Type, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID, Authorization, Accept
003⫶Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS
004⫶Access-Control-Allow-Origin: *
005⫶Access-Control-Expose-Headers: Mcp-Session-Id
006⫶Cache-Control: no-cache, private
007⫶Content-Type: application/json
008⫶Date: Tue, 28 Apr 2026 09:53:27 GMT
009⫶Mcp-Session-Id: 12345678-9abc-def0-1234-56789abcdef0

docs/ai/mcp/mcp_config.md@306:``` json
docs/ai/mcp/mcp_config.md@307:[[= include_code('code_samples/mcp/mcp.sh.output.txt', 26, 51) =]]
docs/ai/mcp/mcp_config.md@308:```

001⫶{
002⫶ "jsonrpc": "2.0",
003⫶ "id": 1,
004⫶ "result": {
005⫶ "protocolVersion": "2025-06-18",
006⫶ "capabilities": {
007⫶ "logging": {},
008⫶ "completions": {},
009⫶ "prompts": {
010⫶ "listChanged": true
011⫶ },
012⫶ "resources": {
013⫶ "listChanged": true
014⫶ },
015⫶ "tools": {
016⫶ "listChanged": true
017⫶ }
018⫶ },
019⫶ "serverInfo": {
020⫶ "name": "example",
021⫶ "version": "1.0.0",
022⫶ "description": "Example MCP Server"
023⫶ },
024⫶ "instructions": "Use this server to greet someone."
025⫶ }
026⫶}

docs/ai/mcp/mcp_config.md@316:``` http
docs/ai/mcp/mcp_config.md@317:[[= include_code('code_samples/mcp/mcp.sh.output.txt', 52, 56) =]]
docs/ai/mcp/mcp_config.md@318:```

001⫶HTTP/1.1 202 Accepted
002⫶Access-Control-Allow-Headers: Content-Type, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID, Authorization, Accept
003⫶Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS
004⫶Access-Control-Allow-Origin: *
005⫶Access-Control-Expose-Headers: Mcp-Session-Id

docs/ai/mcp/mcp_config.md@326:``` json
docs/ai/mcp/mcp_config.md@327:[[= include_code('code_samples/mcp/mcp.sh.output.txt', 69, 128) =]]
docs/ai/mcp/mcp_config.md@328:```

001⫶{
002⫶ "jsonrpc": "2.0",
003⫶ "id": 2,
004⫶ "result": {
005⫶ "tools": [
006⫶ {
007⫶ "name": "greet",
008⫶ "inputSchema": {
009⫶ "type": "object",
010⫶ "properties": {
011⫶ "name": {
012⫶ "type": "string",
013⫶ "description": "The name of the person to greet"
014⫶ }
015⫶ },
016⫶ "required": [
017⫶ "name"
018⫶ ]
019⫶ },
020⫶ "description": "Greet a user by name",
021⫶ "annotations": {
022⫶ "readOnlyHint": true,
023⫶ "destructiveHint": false,
024⫶ "idempotentHint": true,
025⫶ "openWorldHint": false
026⫶ },
027⫶ "icons": [
028⫶ {
029⫶ "src": "https://openmoji.org/data/color/svg/1F44B.svg"
030⫶ }
031⫶ ],
032⫶ "outputSchema": {
033⫶ "type": "object",
034⫶ "properties": {
035⫶ "general": {
036⫶ "type": "string",
037⫶ "description": "the safe way to greet someone"
038⫶ },
039⫶ "close": {
040⫶ "type": "string",
041⫶ "description": "when you're close to the person, like friends or relatives"
042⫶ },
043⫶ "morning": {
044⫶ "type": "string",
045⫶ "description": "when it's in the morning"
046⫶ },
047⫶ "afternoon": {
048⫶ "type": "string",
049⫶ "description": "when it's the afternoon"
050⫶ },
051⫶ "evening": {
052⫶ "type": "string",
053⫶ "description": "when it's late in the day"
054⫶ }
055⫶ }
056⫶ }
057⫶ }
058⫶ ]
059⫶ }
060⫶}

docs/ai/mcp/mcp_config.md@336:``` json
docs/ai/mcp/mcp_config.md@337:[[= include_code('code_samples/mcp/mcp.sh.output.txt', 129, 148) =]]
docs/ai/mcp/mcp_config.md@338:```

001⫶{
002⫶ "jsonrpc": "2.0",
003⫶ "id": 3,
004⫶ "result": {
005⫶ "content": [
006⫶ {
007⫶ "type": "text",
008⫶ "text": "{\n \"general\": \"Hello, World!\",\n \"close\": \"Hey, World!\",\n \"morning\": \"Good morning, World!\",\n \"afternoon\": \"Good afternoon, World!\",\n \"evening\": \"Good evening, World!\"\n}"
009⫶ }
010⫶ ],
011⫶ "isError": false,
012⫶ "structuredContent": {
013⫶ "general": "Hello, World!",
014⫶ "close": "Hey, World!",
015⫶ "morning": "Good morning, World!",
016⫶ "afternoon": "Good afternoon, World!",
017⫶ "evening": "Good evening, World!"
018⫶ }
019⫶ }
020⫶}

docs/ai/mcp/mcp_config.md@346:``` json
docs/ai/mcp/mcp_config.md@347:[[= include_code('code_samples/mcp/mcp.sh.output.txt', 149, 172) =]]
docs/ai/mcp/mcp_config.md@348:```

001⫶{
002⫶ "jsonrpc": "2.0",
003⫶ "id": 4,
004⫶ "result": {
005⫶ "prompts": [
006⫶ {
007⫶ "name": "greet",
008⫶ "description": "Prompt to be greeted by the `greet` tool",
009⫶ "arguments": [
010⫶ {
011⫶ "name": "name",
012⫶ "description": "The name you want to be greeted by",
013⫶ "required": true
014⫶ }
015⫶ ],
016⫶ "icons": [
017⫶ {
018⫶ "src": "https://openmoji.org/data/color/svg/1F91D.svg"
019⫶ }
020⫶ ]
021⫶ }
022⫶ ]
023⫶ }
024⫶}

docs/ai/mcp/mcp_config.md@356:``` json
docs/ai/mcp/mcp_config.md@357:[[= include_code('code_samples/mcp/mcp.sh.output.txt', 173, 187) =]]
docs/ai/mcp/mcp_config.md@358:```

001⫶{
002⫶ "jsonrpc": "2.0",
003⫶ "id": 5,
004⫶ "result": {
005⫶ "messages": [
006⫶ {
007⫶ "role": "user",
008⫶ "content": {
009⫶ "type": "text",
010⫶ "text": "Hi. My name is Firstname Lastname. Please, greet me."
011⫶ }
012⫶ }
013⫶ ]
014⫶ }
015⫶}


code_samples/mcp/src/Command/McpServerListCommand.php


code_samples/mcp/src/Command/McpServerListCommand.php

docs/ai/mcp/mcp_config.md@264:``` php
docs/ai/mcp/mcp_config.md@265:[[= include_code('code_samples/mcp/src/Command/McpServerListCommand.php') =]]
docs/ai/mcp/mcp_config.md@266:```

001⫶<?php declare(strict_types=1);
002⫶
003⫶namespace App\mcp\src\Command;
004⫶
005⫶use Ibexa\Contracts\Mcp\McpServerConfigurationRegistryInterface;
006⫶use Symfony\Component\Console\Attribute\AsCommand;
007⫶use Symfony\Component\Console\Command\Command;
008⫶use Symfony\Component\Console\Style\SymfonyStyle;
009⫶
010⫶#[AsCommand(name: 'app:mcp:server_list', description: 'List MCP servers')]
011⫶class McpServerListCommand
012⫶{
013⫶ public function __construct(private readonly McpServerConfigurationRegistryInterface $configRegistry)
014⫶ {
015⫶ }
016⫶
017⫶ public function __invoke(SymfonyStyle $io): int
018⫶ {
019⫶ foreach($this->configRegistry->getServerConfigurations() as $serverConfiguration) {
020⫶ $io->title($serverConfiguration->identifier);
021⫶ dump($serverConfiguration);
022⫶ }
023⫶
024⫶ return Command::SUCCESS;
025⫶ }
026⫶}


code_samples/mcp/src/Mcp/ExampleCapabilities.php


code_samples/mcp/src/Mcp/ExampleCapabilities.php

docs/ai/mcp/mcp_config.md@236:``` php
docs/ai/mcp/mcp_config.md@237:[[= include_code('code_samples/mcp/src/Mcp/ExampleCapabilities.php') =]]
docs/ai/mcp/mcp_config.md@238:```

001⫶<?php declare(strict_types=1);
002⫶
003⫶namespace App\Mcp;
004⫶
005⫶use Ibexa\Contracts\Mcp\Attribute\McpPrompt;
006⫶use Ibexa\Contracts\Mcp\Attribute\McpTool;
007⫶use Ibexa\Contracts\Mcp\McpCapabilityInterface;
008⫶use Mcp\Schema\Icon;
009⫶use Mcp\Schema\ToolAnnotations;
010⫶
011⫶final readonly class ExampleCapabilities implements McpCapabilityInterface
012⫶{
013⫶ /**
014⫶ * @param string $name The name of the person to greet
015⫶ *
016⫶ * @return array<string, string>
017⫶ */
018⫶ #[McpTool(
019⫶ servers: ['example'],
020⫶ name: 'greet',
021⫶ description: 'Greet a user by name',
022⫶ annotations: new ToolAnnotations(
023⫶ readOnlyHint: true,
024⫶ destructiveHint: false,
025⫶ idempotentHint: true,
026⫶ openWorldHint: false,
027⫶ ),
028⫶ icons: [new Icon(
029⫶ src: 'https://openmoji.org/data/color/svg/1F44B.svg',
030⫶ )],
031⫶ outputSchema: [
032⫶ 'type' => 'object',
033⫶ 'properties' => [
034⫶ 'general' => [
035⫶ 'type' => 'string',
036⫶ 'description' => 'the safe way to greet someone',
037⫶ ],
038⫶ 'close' => [
039⫶ 'type' => 'string',
040⫶ 'description' => 'when you\'re close to the person, like friends or relatives',
041⫶ ],
042⫶ 'morning' => [
043⫶ 'type' => 'string',
044⫶ 'description' => 'when it\'s in the morning',
045⫶ ],
046⫶ 'afternoon' => [
047⫶ 'type' => 'string',
048⫶ 'description' => 'when it\'s the afternoon',
049⫶ ],
050⫶ 'evening' => [
051⫶ 'type' => 'string',
052⫶ 'description' => 'when it\'s late in the day',
053⫶ ],
054⫶ ],
055⫶ ],
056⫶ )]
057⫶ public function greetByName(string $name): array
058⫶ {
059⫶ return [
060⫶ 'general' => sprintf('Hello, %s!', $name),
061⫶ 'close' => sprintf('Hey, %s!', $name),
062⫶ 'morning' => sprintf('Good morning, %s!', $name),
063⫶ 'afternoon' => sprintf('Good afternoon, %s!', $name),
064⫶ 'evening' => sprintf('Good evening, %s!', $name),
065⫶ ];
066⫶ }
067⫶
068⫶ /**
069⫶ * @param string $name The name you want to be greeted by
070⫶ *
071⫶ * @return array<string, mixed>
072⫶ */
073⫶ #[McpPrompt(
074⫶ servers: ['example'],
075⫶ name: 'greet',
076⫶ description: 'Prompt to be greeted by the `greet` tool',
077⫶ icons: [new Icon(
078⫶ src: 'https://openmoji.org/data/color/svg/1F91D.svg',
079⫶ )],
080⫶ )]
081⫶ public function getGreetPrompt(string $name): array
082⫶ {
083⫶ return [
084⫶ 'role' => 'user',
085⫶ 'content' => [
086⫶ 'type' => 'text',
087⫶ 'text' => "Hi. My name is $name. Please, greet me.",
088⫶ ],
089⫶ ];
090⫶ }
091⫶}


code_samples/mcp/stdio.mcp.json


code_samples/mcp/stdio.mcp.json

docs/ai/mcp/mcp_config.md@472:``` json
docs/ai/mcp/mcp_config.md@473:[[= include_code('code_samples/mcp/stdio.mcp.json') =]]
docs/ai/mcp/mcp_config.md@474:```

001⫶{
002⫶ "mcpServers": {
003⫶ "ibexa-example": {
004⫶ "type": "stdio",
005⫶ "command": "bash",
006⫶ "args": ["mcp-ibexa-example-wrapper.sh"],
007⫶ "tools": ["*"]
008⫶ }
009⫶ }
010⫶}

Download colorized diff

@adriendupuis adriendupuis requested review from barw4 and mnocon May 6, 2026 08:04
<base href="../">
<link rel="icon" href="images/favicon.png"/>
<link rel="stylesheet" href="css/base.css">
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css">
<link rel="icon" href="images/favicon.png"/>
<link rel="stylesheet" href="css/base.css">
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css">
<script src="https://cdn.jsdelivr.net/npm/fuse.js@3.4.6"></script>
<base href="../">
<link rel="icon" href="images/favicon.png"/>
<link rel="stylesheet" href="css/base.css">
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css">
<link rel="icon" href="images/favicon.png"/>
<link rel="stylesheet" href="css/base.css">
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css">
<script src="https://cdn.jsdelivr.net/npm/fuse.js@3.4.6"></script>
<base href="../">
<link rel="icon" href="images/favicon.png"/>
<link rel="stylesheet" href="css/base.css">
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css">
<link rel="icon" href="images/favicon.png"/>
<link rel="stylesheet" href="css/base.css">
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css">
<script src="https://cdn.jsdelivr.net/npm/fuse.js@3.4.6"></script>
<base href="../">
<link rel="icon" href="images/favicon.png"/>
<link rel="stylesheet" href="css/base.css">
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css">
<link rel="icon" href="images/favicon.png"/>
<link rel="stylesheet" href="css/base.css">
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css">
<script src="https://cdn.jsdelivr.net/npm/fuse.js@3.4.6"></script>
<base href="../">
<link rel="icon" href="images/favicon.png"/>
<link rel="stylesheet" href="css/base.css">
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css">
<link rel="icon" href="images/favicon.png"/>
<link rel="stylesheet" href="css/base.css">
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css">
<script src="https://cdn.jsdelivr.net/npm/fuse.js@3.4.6"></script>
Copy link
Copy Markdown
Contributor

@mnocon mnocon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a partial review for now, starting with https://ez-systems-developer-documentation--3106.com.readthedocs.build/en/3106/ai/mcp/mcp_config/#example I've just read quickly till the end to get the general idea.

The most important comment is this one:
https://github.com/ibexa/documentation-developer/pull/3106/changes#r3219594627

I like the content, though at the end I felt it was more of a draft, not always following how we write.

Two additional comments:

  • let's choose AI agents or AI applications and use them consistency
  • Let's use JWT authentication, not just "JWT"

Comment thread code_samples/mcp/src/Command/McpServerListCommand.php Outdated
#!/bin/bash
set -e

baseUrl='http://localhost' # Adapt to your test case
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just reading the example for now, is this SiteAccess-aware? How do I pass it - in the URL or with some header, like in REST?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's SiteAccess aware through regular URL matching.
I have this sentence in "Configure MCP server":

It's accessible with the path /mcp/example (for example, on http://localhost/mcp/example and http://localhost/admin/mcp/example).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And since c7937ee I now have this in MCP server options path description:

MCP server endpoint path (appended to SiteAccess-aware base URL)

Comment thread mkdocs.yml
Comment thread mkdocs.yml Outdated
Comment thread docs/infrastructure_and_maintenance/security/security_checklist.md Outdated
Comment thread docs/ai/mcp/mcp_config.md Outdated
Comment thread docs/ai/mcp/mcp_config.md Outdated
Comment thread docs/ai/mcp/mcp_config.md Outdated
Comment thread code_samples/mcp/src/Mcp/ExampleCapabilities.php Outdated
Comment thread docs/ai/mcp/mcp_config.md Outdated
Comment thread docs/ai/mcp/mcp_config.md Outdated
@adriendupuis adriendupuis requested review from dabrt and mnocon May 13, 2026 10:33
Copy link
Copy Markdown
Contributor

@barw4 barw4 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome work 👏🏻

One more global comment, should we also document Resources like we did with Prompts?

Comment thread docs/ai/mcp/mcp_guide.md
- create MCP servers [by using YAML configuration](mcp_config.md#mcp-server-configuration)
- assign different [tools](mcp_config.md#built-in-tools), prompts, and resources to different MCP servers, varying them for each site
- [create custom server capabilities](mcp_usage.md#create-capability-class) with PHP API

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add the information that 'you can utilize predefined Ibexa tools from src/lib/Tools directory by adding them to your server' - obviously with better wording.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can split to emphase on the built-in tools link.
See https://github.com/ibexa/documentation-developer/pull/3106/changes#r3234431059

Comment thread docs/ai/mcp/mcp_guide.md
While [AI actions](ai_actions_guide.md) integrate AI to the back office,
[[= product_name =]]'s [MCP servers](https://modelcontextprotocol.io/docs/learn/server-concepts) offer an API usable by AI agents outside the system.

Some AI agents can use directly REST API or GraphQL API if their users explain to them how to do it in prompts, in skill files, etc.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's worth mentioning the advantages of MCP vs REST - here is unified cheatsheet: https://glama.ai/blog/2025-06-06-mcp-vs-api

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this resource. I should be able to strengthen the next sentence.

Comment thread docs/ai/mcp/mcp_config.md Outdated
Comment thread docs/ai/mcp/mcp_config.md Outdated
Comment thread docs/ai/mcp/mcp_config.md
In `config/packages/security.yaml`,

- uncomment the `ibexa_jwt_rest` firewall to allow the request of JWT tokens through REST or GraphQL
- uncomment the `ibexa_jwt_mcp` firewall to allow the use of JWT authentication against MCP servers
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this part was removed from recipes therefore there will be no ibexa_jwt_mcp configuration to be uncommented - as it's opt in package I assume it has to be added manually, wdyt?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some LTS Update seem to have recipe entries. Maybe it can be done. It should be investigated.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In theory, it should be doable with recipes - with the add-lines configurator.

Doc: symfony/flex#975

Example usage: https://github.com/symfony/recipes/blob/main/symfony/panther/1.0/manifest.json#L28

Comment thread docs/ai/mcp/mcp_config.md

- `Ibexa\Mcp\Tool\TranslationTools`
- `list_languages`: Lists all languages in the current SiteAccess
- `list_content_translations`: Lists languages in which given content item has translations
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/ibexa/mcp/pull/8 another tool by @bnowak in progress, something to keep an eye on

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread docs/ai/mcp/mcp_config.md
### Discovery cache

Discovery is cached to avoid scanning for capabilities on every request.
A PSR-6 or PSR-16 cache pool must be provided for this caching.
Copy link
Copy Markdown
Contributor

@barw4 barw4 May 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

worth mentioning that when using tagaware.filesystem cache in order to see changes we need to also clear var/share: https://symfony.com/blog/new-in-symfony-7-4-share-directory (something to verify)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread docs/ai/mcp/mcp_usage.md Outdated
Comment thread docs/ai/mcp/mcp_usage.md
For prompt, the `servers` parameter is required.
So, the example prompt has to use it to be associated with the `example` server.

During development and testing, you may have to clear the cache to make sure new or modified capabilities are properly re-discovered.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/var/share probably :)

Comment thread docs/ai/mcp/mcp_usage.md
[[= include_code('code_samples/mcp/stdio.mcp.json') =]]
```

The `mcp-ibexa-example-wrapper.sh` is a script asking for a JWT token then establishing a connection with the MCP server.
Copy link
Copy Markdown
Contributor

@barw4 barw4 May 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I have some concerns with providing example testing scripts that touch security. Since MCP ideally should be utilized with OAuth flow and we are not yet there, maybe for our own safety we could skip this part?

Comment thread docs/ai/mcp/mcp_guide.md Outdated
Co-authored-by: Bartek Wajda <bartlomiej.wajda@ibexa.co>
Co-authored-by: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com>
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
43.6% Duplication on New Code (required ≤ 3%)
B Security Rating on New Code (required ≥ A)
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants