Skip to content

Commit ac96811

Browse files
adriendupuismnoconbarw4dabrt
authored
IBX-11536: MCP Servers (#3106)
--------- Co-authored-by: adriendupuis <adriendupuis@users.noreply.github.com> Co-authored-by: Marek Nocoń <mnocon@users.noreply.github.com> Co-authored-by: Bartek Wajda <bartlomiej.wajda@ibexa.co> Co-authored-by: Tomasz Dąbrowski <64841871+dabrt@users.noreply.github.com>
1 parent 1b6581b commit ac96811

46 files changed

Lines changed: 1410 additions & 54 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
security:
2+
firewalls:
3+
#
4+
ibexa_jwt_mcp:
5+
request_matcher: Ibexa\Mcp\Security\McpRequestMatcher
6+
user_checker: Ibexa\Core\MVC\Symfony\Security\UserChecker
7+
provider: ibexa
8+
stateless: true
9+
jwt: ~
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
ibexa:
2+
repositories:
3+
default:
4+
mcp:
5+
example:
6+
path: /mcp/example
7+
enabled: true
8+
description: 'Example MCP Server'
9+
instructions: 'Use this server to greet someone.'
10+
discovery_cache: cache.tagaware.filesystem
11+
session:
12+
type: psr16
13+
directory: cache.tagaware.filesystem
14+
system:
15+
default:
16+
mcp:
17+
servers:
18+
- example

code_samples/mcp/http.mcp.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"mcpServers": {
3+
"ibexa-example": {
4+
"type": "http",
5+
"url": "http://localhost/mcp/example",
6+
"headers": {
7+
"Authorization": "Bearer <JWT token>"
8+
},
9+
"tools": ["*"]
10+
}
11+
}
12+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/bin/bash
2+
set -e
3+
4+
baseUrl='http://localhost' # Adapt to your test case
5+
6+
jwtToken=$(curl -s -X 'POST' \
7+
"$baseUrl/api/ibexa/v2/user/token/jwt" \
8+
-H 'Content-Type: application/vnd.ibexa.api.JWTInput+json' \
9+
-H 'Accept: application/vnd.ibexa.api.JWT+json' \
10+
-d '{
11+
"JWTInput": {
12+
"_media-type": "application/vnd.ibexa.api.JWTInput+json",
13+
"username": "ibexa-example",
14+
"password": "Ibexa-3xample"
15+
}
16+
}' | jq -r .JWT.token)
17+
18+
exec npx -y supergateway \
19+
--streamableHttp "$baseUrl/mcp/example" \
20+
--oauth2Bearer "$jwtToken" \
21+
--logLevel none

code_samples/mcp/mcp.matrix.yaml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
ibexa:
2+
repositories:
3+
<repository_identifier>:
4+
mcp:
5+
<server_identifier>:
6+
path: <server_route_path>
7+
enabled: true
8+
# Server options…
9+
tools:
10+
- Ibexa\Mcp\Tool\TranslationTools
11+
- Ibexa\Mcp\Tool\SeoTools
12+
discovery_cache: <cache_pool_service>
13+
session:
14+
type: <psr16|file|memory>
15+
# Session options…
16+
mcp_psr16:
17+
discovery_cache: cache.redis.mcp
18+
session:
19+
type: psr16
20+
service: cache.redis.mcp
21+
prefix: 'mcp_<server_identifier>_'
22+
mcp_file:
23+
session:
24+
type: file
25+
directory: '%kernel.cache_dir%/mcp/sessions'
26+
mcp_memory:
27+
session:
28+
type: memory
29+
system:
30+
<siteaccess_scope>:
31+
mcp:
32+
servers:
33+
- <server_identifier>
34+
services:
35+
cache.redis.mcp:
36+
public: true
37+
class: Symfony\Component\Cache\Adapter\RedisTagAwareAdapter
38+
parent: cache.adapter.redis
39+
tags:
40+
- name: cache.pool
41+
clearer: cache.app_clearer
42+
provider: 'redis://mcp.redis:6379'
43+
namespace: 'mcp'

code_samples/mcp/mcp.sh

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#!/bin/bash
2+
set -e
3+
set +x
4+
5+
baseUrl='http://localhost' # Adapt to your test case
6+
username='ibexa-example'
7+
password='Ibexa-3xample'
8+
9+
curl -s -X 'POST' \
10+
"$baseUrl/api/ibexa/v2/user/token/jwt" \
11+
-H 'Content-Type: application/vnd.ibexa.api.JWTInput+json' \
12+
-H 'Accept: application/vnd.ibexa.api.JWT+json' \
13+
-d "{
14+
\"JWTInput\": {
15+
\"_media-type\": \"application/vnd.ibexa.api.JWTInput+json\",
16+
\"username\": \"$username\",
17+
\"password\": \"$password\"
18+
}
19+
}" > response.tmp.txt
20+
21+
cat response.tmp.txt | jq
22+
jwtToken=$(cat response.tmp.txt | jq -r .JWT.token)
23+
rm response.tmp.txt
24+
25+
curl -s -i -X 'POST' "$baseUrl/mcp/example" \
26+
-H "Authorization: Bearer $jwtToken" \
27+
-d '{
28+
"jsonrpc": "2.0",
29+
"id": 1,
30+
"method": "initialize",
31+
"params": {
32+
"protocolVersion": "2025-03-26",
33+
"capabilities": {},
34+
"clientInfo": {
35+
"name": "test-curl-client",
36+
"version": "1.0.0"
37+
}
38+
}
39+
}' > response.tmp.txt
40+
41+
sed '$d' response.tmp.txt
42+
tail -n 1 response.tmp.txt | jq
43+
mcpSessionId=$(cat response.tmp.txt | grep 'Mcp-Session-Id:' | sed 's/Mcp-Session-Id: \([0-9a-f-]*\).*/\1/')
44+
rm response.tmp.txt
45+
46+
curl -s -i -X 'POST' "$baseUrl/mcp/example" \
47+
-H "Authorization: Bearer $jwtToken" \
48+
-H "Mcp-Session-Id: $mcpSessionId" \
49+
-d '{
50+
"jsonrpc": "2.0",
51+
"method": "notifications/initialized"
52+
}'
53+
54+
curl -s -X 'POST' "$baseUrl/mcp/example" \
55+
-H "Authorization: Bearer $jwtToken" \
56+
-H "Mcp-Session-Id: $mcpSessionId" \
57+
-d '{
58+
"jsonrpc": "2.0",
59+
"id": 2,
60+
"method": "tools/list"
61+
}' | jq
62+
63+
curl -s -X 'POST' "$baseUrl/mcp/example" \
64+
-H "Authorization: Bearer $jwtToken" \
65+
-H "Mcp-Session-Id: $mcpSessionId" \
66+
-d '{
67+
"jsonrpc": "2.0",
68+
"id": 3,
69+
"method": "tools/call",
70+
"params": {
71+
"name": "greet",
72+
"arguments": {
73+
"name": "World"
74+
}
75+
}
76+
}' | jq
77+
78+
curl -s -X 'POST' "$baseUrl/mcp/example" \
79+
-H "Authorization: Bearer $jwtToken" \
80+
-H "Mcp-Session-Id: $mcpSessionId" \
81+
-d '{
82+
"jsonrpc": "2.0",
83+
"id": 4,
84+
"method": "prompts/list"
85+
}' | jq
86+
87+
curl -s -X 'POST' "$baseUrl/mcp/example" \
88+
-H "Authorization: Bearer $jwtToken" \
89+
-H "Mcp-Session-Id: $mcpSessionId" \
90+
-d '{
91+
"jsonrpc": "2.0",
92+
"id": 5,
93+
"method": "prompts/get",
94+
"params": {
95+
"name": "greet",
96+
"arguments": {
97+
"name": "Firstname Lastname"
98+
}
99+
}
100+
}' | jq

code_samples/mcp/mcp.sh.output.txt

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
{
2+
"JWT": {
3+
"_media-type": "application/vnd.ibexa.api.JWT+json",
4+
"_token": "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ.abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyz1234567890ABCD.EFGHIJKL-MNOPQRSTUVWXYZ12345678901234567890",
5+
"token": "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ.abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyz1234567890ABCD.EFGHIJKL-MNOPQRSTUVWXYZ12345678901234567890"
6+
}
7+
}
8+
HTTP/1.1 200 OK
9+
Access-Control-Allow-Headers: Content-Type, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID, Authorization, Accept
10+
Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS
11+
Access-Control-Allow-Origin: *
12+
Access-Control-Expose-Headers: Mcp-Session-Id
13+
Cache-Control: no-cache, private
14+
Content-Type: application/json
15+
Date: Tue, 28 Apr 2026 09:53:27 GMT
16+
Mcp-Session-Id: 12345678-9abc-def0-1234-56789abcdef0
17+
Server: Apache/2.4.66 (Debian)
18+
Vary: cookie,authorization
19+
X-Cache-Debug: 1
20+
X-Debug-Token: 123456
21+
X-Debug-Token-Link: http://localhost/_profiler/123456
22+
X-Powered-By: Ibexa Commerce v5
23+
X-Robots-Tag: noindex
24+
Transfer-Encoding: chunked
25+
26+
{
27+
"jsonrpc": "2.0",
28+
"id": 1,
29+
"result": {
30+
"protocolVersion": "2025-06-18",
31+
"capabilities": {
32+
"logging": {},
33+
"completions": {},
34+
"prompts": {
35+
"listChanged": true
36+
},
37+
"resources": {
38+
"listChanged": true
39+
},
40+
"tools": {
41+
"listChanged": true
42+
}
43+
},
44+
"serverInfo": {
45+
"name": "example",
46+
"version": "1.0.0",
47+
"description": "Example MCP Server"
48+
},
49+
"instructions": "Use this server to greet someone."
50+
}
51+
}
52+
HTTP/1.1 202 Accepted
53+
Access-Control-Allow-Headers: Content-Type, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID, Authorization, Accept
54+
Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS
55+
Access-Control-Allow-Origin: *
56+
Access-Control-Expose-Headers: Mcp-Session-Id
57+
Cache-Control: no-cache, private
58+
Content-Length: 0
59+
Content-Type: text/html; charset=UTF-8
60+
Date: Fri, 24 Apr 2026 11:16:27 GMT
61+
Server: Apache/2.4.66 (Debian)
62+
Vary: cookie,authorization
63+
X-Cache-Debug: 1
64+
X-Debug-Token: 7890ab
65+
X-Debug-Token-Link: http://localhost/_profiler/7890ab
66+
X-Powered-By: Ibexa Commerce v5
67+
X-Robots-Tag: noindex
68+
69+
{
70+
"jsonrpc": "2.0",
71+
"id": 2,
72+
"result": {
73+
"tools": [
74+
{
75+
"name": "greet",
76+
"inputSchema": {
77+
"type": "object",
78+
"properties": {
79+
"name": {
80+
"type": "string",
81+
"description": "The name of the person to greet"
82+
}
83+
},
84+
"required": [
85+
"name"
86+
]
87+
},
88+
"description": "Greet a user by name",
89+
"annotations": {
90+
"readOnlyHint": true,
91+
"destructiveHint": false,
92+
"idempotentHint": true,
93+
"openWorldHint": false
94+
},
95+
"icons": [
96+
{
97+
"src": "https://openmoji.org/data/color/svg/1F44B.svg"
98+
}
99+
],
100+
"outputSchema": {
101+
"type": "object",
102+
"properties": {
103+
"general": {
104+
"type": "string",
105+
"description": "the safe way to greet someone"
106+
},
107+
"close": {
108+
"type": "string",
109+
"description": "when you're close to the person, like friends or relatives"
110+
},
111+
"morning": {
112+
"type": "string",
113+
"description": "when it's in the morning"
114+
},
115+
"afternoon": {
116+
"type": "string",
117+
"description": "when it's the afternoon"
118+
},
119+
"evening": {
120+
"type": "string",
121+
"description": "when it's late in the day"
122+
}
123+
}
124+
}
125+
}
126+
]
127+
}
128+
}
129+
{
130+
"jsonrpc": "2.0",
131+
"id": 3,
132+
"result": {
133+
"content": [
134+
{
135+
"type": "text",
136+
"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}"
137+
}
138+
],
139+
"isError": false,
140+
"structuredContent": {
141+
"general": "Hello, World!",
142+
"close": "Hey, World!",
143+
"morning": "Good morning, World!",
144+
"afternoon": "Good afternoon, World!",
145+
"evening": "Good evening, World!"
146+
}
147+
}
148+
}
149+
{
150+
"jsonrpc": "2.0",
151+
"id": 4,
152+
"result": {
153+
"prompts": [
154+
{
155+
"name": "greet",
156+
"description": "Prompt to be greeted by the `greet` tool",
157+
"arguments": [
158+
{
159+
"name": "name",
160+
"description": "The name you want to be greeted by",
161+
"required": true
162+
}
163+
],
164+
"icons": [
165+
{
166+
"src": "https://openmoji.org/data/color/svg/1F91D.svg"
167+
}
168+
]
169+
}
170+
]
171+
}
172+
}
173+
{
174+
"jsonrpc": "2.0",
175+
"id": 5,
176+
"result": {
177+
"messages": [
178+
{
179+
"role": "user",
180+
"content": {
181+
"type": "text",
182+
"text": "Hi. My name is Firstname Lastname. Please, greet me."
183+
}
184+
}
185+
]
186+
}
187+
}

0 commit comments

Comments
 (0)