POST /mcp HTTP/1.1
Host: mcp.example.com
Authorization: Bearer <MCP_AUTH_TOKEN>
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "mesh_list_projects",
"arguments": {}
}
}
The Gentics Mesh MCP Server exposes published Gentics Mesh content to language models through the Model Context Protocol (MCP). It is a small, separate Java service that sits between an AI client and Gentics Mesh: the client calls a handful of well-defined tools, the server translates them into Gentics Mesh REST and GraphQL requests, and the language model never talks to the Gentics Mesh API directly.
| The MCP Server is part of the commercial Gentics Mesh Enterprise distribution. It is shipped as its own container image and runs as a separate process next to Gentics Mesh. |
The server offers six read-only tools. Whatever an AI client does, the set of operations it can trigger is fixed, every call is checked against an allowlist of projects and languages before Gentics Mesh sees it, and write operations are not available at all.
| Part | Responsibility |
|---|---|
AI client |
Claude Desktop, Cursor, OpenAI Agents, LangChain, n8n, Open WebUI or any other MCP-capable client. Discovers the tools, decides when to call them and interprets the results. |
MCP Server |
Speaks MCP over stdio or HTTP, validates tool input, enforces the policy layer and the protection layer, and calls Gentics Mesh. |
Gentics Mesh |
Answers the underlying REST and GraphQL requests with the permissions of the identity the request carries — see Mesh identity. |
The tool surface is identical for both transports. Tool names, input schemas and output formats are the same whether the server is started as a child process by a desktop client or reached over HTTP from a remote agent framework.
The server reads JSON-RPC from standard input and writes to standard output. This is the natural transport for local clients such as Claude Desktop, Cursor, Continue or Zed, which start the server as a child process and own it for the lifetime of the session. There is exactly one client per process and no authentication step: whoever is able to start the process is trusted.
stdio is the default. It is selected when MCP_TRANSPORT is not set, or explicitly with the --stdio command line flag.
The server binds an HTTP port and accepts JSON-RPC 2.0 requests on POST /mcp.
This is the transport for remote clients — agent frameworks, workflow tools and self-hosted LLM stacks that run somewhere else in the cluster.
Several clients can use the server at the same time, and each request is authenticated with a Bearer token.
HTTP is selected with MCP_TRANSPORT=http or the --http command line flag. A command line flag takes precedence over the environment variable.
The following JSON-RPC methods are handled: initialize, notifications/initialized, ping, tools/list and tools/call.
POST /mcp HTTP/1.1
Host: mcp.example.com
Authorization: Bearer <MCP_AUTH_TOKEN>
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "mesh_list_projects",
"arguments": {}
}
}
| The HTTP transport is request/response only. Server-initiated streams (SSE) for progress events are not implemented. All tools of this server answer in a single response, so nothing is lost by that. |
All six tools are read-only. A tool result is returned as a single MCP text content block that contains a JSON document.
| Tool | Purpose |
|---|---|
|
Lists the Gentics Mesh projects that are both allowlisted and readable for the current identity. |
|
Lists the schemas of a project together with their field definitions. |
|
Loads a single node by UUID and returns a normalised representation. |
|
Full-text search over the nodes of a project. Requires the Gentics Mesh Elasticsearch integration. |
|
Resolves a webroot path to the node behind it. |
|
Executes a read-only GraphQL query against a project. |
Takes no input.
{
"projects": [
{ "name": "demo", "uuid": "..." },
{ "name": "shop", "uuid": "..." }
]
}
Projects that are not on the MESH_ALLOWED_PROJECTS list are not returned, even if the identity could read them.
| Input | Required | Meaning |
|---|---|---|
|
yes |
Project name |
{
"project": "demo",
"schemas": [
{
"uuid": "...",
"name": "vehicle",
"fields": [
{ "name": "name", "type": "string", "required": true },
{ "name": "weight", "type": "number", "required": false }
]
}
]
}
| Input | Required | Meaning |
|---|---|---|
|
yes |
Project name |
|
yes |
Node UUID |
|
no |
Language code, default |
{
"uuid": "...",
"project": "demo",
"language": "de",
"schema": "vehicle",
"displayName": "DeLorean DMC-12",
"path": "/automobiles/delorean",
"published": true,
"version": "1.0",
"fields": { "name": "DeLorean DMC-12", "weight": 1230 },
"references": [
{ "field": "vehicleImage", "uuid": "..." }
]
}
Node references are listed by field name and UUID; the referenced nodes themselves are not resolved.
Follow-up calls to mesh_get_node load them on demand.
| Input | Required | Meaning |
|---|---|---|
|
yes |
Project name |
|
yes |
Search text |
|
no |
Restrict the search to one schema |
|
no |
Language code, default |
|
no |
Fields to search in, default |
|
no |
Number of results, |
|
no |
Only search published content, default |
{
"results": [
{
"uuid": "...",
"title": "DeLorean DMC-12",
"schema": "vehicle",
"language": "de",
"path": "/automobiles/delorean",
"snippet": "...",
"published": true
}
]
}
The search is executed through the Gentics Mesh search endpoint and therefore needs a configured and synchronised Elasticsearch — see Elasticsearch. Without it, the tool returns an upstream error.
| Input | Required | Meaning |
|---|---|---|
|
yes |
Project name |
|
yes |
Webroot path, for example |
|
no |
Language code, default |
The response has the same shape as mesh_get_node.
| Input | Required | Meaning |
|---|---|---|
|
yes |
Project name |
|
yes |
GraphQL query document |
|
no |
Query variables as a JSON object |
{
"data": { "...": "..." },
"meta": { "truncated": false, "timedOut": false }
}
Three guardrails apply before the query reaches Gentics Mesh, in this order:
A document that starts with the mutation keyword is rejected with forbidden.
A document longer than MCP_GRAPHQL_MAX_QUERY_LENGTH characters is rejected with validation_error.
A document whose brace nesting is deeper than MESH_GRAPHQL_MAX_DEPTH is rejected with validation_error.
The depth check counts braces; it does not parse the query. See GraphQL for the query language itself.
Every tool call passes through the policy layer before any request is sent to Gentics Mesh. The checks are deliberately simple and fail fast, so that a badly parameterised call from a language model is answered by the MCP Server and never reaches Gentics Mesh.
Project allowlist — MESH_ALLOWED_PROJECTS is mandatory. A tool call for any other project is rejected with forbidden.
Language allowlist — MESH_ALLOWED_LANGUAGES (default de,en). Tools that take a language argument reject other codes with forbidden.
Read-only — MESH_READ_ONLY defaults to true. There are currently no write tools; the setting exists so that a future write tool cannot be enabled by accident.
GraphQL guardrails — mutation block, length and depth limits, as described under mesh_run_graphql.
The policy layer is independent of Gentics Mesh permissions. It narrows what the MCP Server will ask for; what Gentics Mesh actually returns is still decided by the permissions of the identity the request runs under.
Two different questions are easy to confuse: whether a client may use the MCP Server, and which user Gentics Mesh sees when the server calls it. The answer to the second question depends only on the transport.
| Case | Setup | MCP authentication | Identity towards Gentics Mesh |
|---|---|---|---|
1 |
|
The server checks the Bearer token and rejects requests that do not carry it. |
anonymous. The token is a credential of the MCP Server, not of Gentics Mesh, and is never forwarded. |
2 |
|
none |
The client’s |
3 |
|
not applicable — the process that started the server is trusted |
A fixed identity from |
Case 1 means that Gentics Mesh serves anonymous requests. The anonymous role in Gentics Mesh needs read permission on every project listed in MESH_ALLOWED_PROJECTS; otherwise all tools return empty results without any error.
See Anonymous access for how to set that up.
In case 2 the forwarded token is attached to the individual request, not to the shared Gentics Mesh client, so concurrent calls cannot inherit each other’s identity.
MESH_API_KEY, MESH_USERNAME and MESH_PASSWORD are only valid for stdio. In HTTP mode the server refuses to start when any of them is set — they would have no effect there, and a silently ignored credential is the worse surprise.
At startup the server verifies the connection:
stdio — logs in and calls me(). If Gentics Mesh answers with the user anonymous, the credentials were not accepted and the server exits.
http — sends an unauthenticated ping to MESH_BASE_URL, so that a wrong URL shows up immediately rather than at the first tool call.
In HTTP mode every request to /mcp must carry Authorization: Bearer <token>, where the token equals MCP_AUTH_TOKEN.
The comparison is constant-time, so the token cannot be guessed through timing differences.
Requests without a valid token are answered with HTTP 401 and counted in mcp_http_auth_failed_total.
Authentication can be switched off with MCP_AUTH_DISABLED=true. That is only appropriate when a reverse proxy or ingress in front of the server already terminates authentication — with it disabled, everyone who can reach the port can call tools.
It also changes the Mesh identity, as described above.
Starting in HTTP mode without either MCP_AUTH_TOKEN or MCP_AUTH_DISABLED=true is a configuration error, and the server exits.
The AuthValidator interface in the source is the extension point for other schemes such as JWT introspection or OAuth 2.1; only the static Bearer token is shipped.
A language model in a retry loop, or an agent that is simply very curious, can produce a lot of requests in a short time. The protection layer keeps that away from Gentics Mesh. Three independent mechanisms sit between every tool call and the Gentics Mesh client, and all of them fail fast: nothing is queued, the client gets an error immediately.
A token bucket per Bearer token and a global one. Each tool consumes a fixed number of permits that roughly reflects the load it puts on Gentics Mesh:
| Tool | Permits |
|---|---|
|
1 |
|
2 |
|
3 |
|
5 |
Defaults are 20 permits per second per token and 50 per second globally.
The per-token limit does not apply to stdio, where there is only one client by design.
When a bucket is empty the call is rejected with rate_limited, HTTP 429 and Retry-After: 1.
At most MCP_MESH_MAX_CONCURRENT Gentics Mesh calls are in flight at the same time (default 10).
A single mesh_run_graphql can hold a connection for the full GraphQL timeout, which is a concurrency problem rather than a rate problem, and this is the cap for it.
Beyond the limit a call is rejected with busy, HTTP 429 and Retry-After: 1.
When Gentics Mesh starts failing, the server stops sending load instead of piling on.
The failure rate is measured over a sliding window of the last MCP_CIRCUIT_BREAKER_WINDOW_SIZE calls (default 20); at MCP_CIRCUIT_BREAKER_FAILURE_RATE_PERCENT (default 50 %) the breaker opens for MCP_CIRCUIT_BREAKER_OPEN_DURATION_MS (default 30 seconds) and then lets three probe calls through before closing again.
Client-side faults — validation_error, forbidden, not_found — do not count as failures; only problems on the Gentics Mesh side do.
While the breaker is open, calls are rejected with circuit_open, HTTP 503 and a Retry-After equal to the remaining open duration.
The breaker can be disabled with MCP_CIRCUIT_BREAKER_ENABLED=false; rate limit and bulkhead stay active.
On HTTP 429 or 503 a client should read Retry-After and back off exponentially.
Agent frameworks such as the OpenAI Agents SDK or LangChain do that on their own; custom integrations have to implement it.
The JSON-RPC envelope of a rejected call additionally carries result.isError = true and a structured error body, so the language model can put the situation into words for the user rather than retrying blindly.
Every tool error is returned as a JSON document with a stable code. Codes are never renamed; new ones are appended.
{
"error": {
"code": "forbidden",
"message": "Projekt 'staging' ist nicht in der Allowlist. Erlaubt: [demo, shop]"
}
}
| Code | HTTP status | Meaning |
|---|---|---|
|
200 |
Invalid tool input: a required field is missing, a value is out of range, or a GraphQL guardrail was hit. |
|
200 |
Policy violation: project or language not on the allowlist, GraphQL mutation, or Gentics Mesh answered 401/403. |
|
200 |
The node or webroot path does not exist in the project. |
|
200 |
The Gentics Mesh call failed: connection refused, 5xx, unexpected response. |
|
200 |
The GraphQL request exceeded |
|
200 |
Unexpected exception in the server. Worth a bug report. |
|
429 |
Token bucket empty, globally or for this Bearer token. |
|
429 |
Bulkhead full — too many concurrent Gentics Mesh calls. |
|
503 |
Gentics Mesh is unhealthy and the breaker is open. |
The HTTP status column applies to the HTTP transport. Tool-level errors are ordinary MCP results with isError = true and HTTP 200; only the protection layer changes the status, so that generic HTTP clients and proxies see 429 and 503.
Over stdio the same JSON is returned inside the MCP result.
All configuration is read from environment variables and validated once at startup. Validation collects every problem it finds and reports them together, so a misconfigured deployment fails on the first start with a complete list rather than one error per restart.
| Variable | Required | Default | Meaning |
|---|---|---|---|
|
yes |
– |
Base URL of the Gentics Mesh instance, without trailing slash |
|
stdio |
– |
Gentics Mesh API token (a long-lived JWT). Alternative to username and password. Rejected in HTTP mode. |
|
stdio |
– |
Service account user, together with |
|
stdio |
– |
Service account password. Never logged. Rejected in HTTP mode. |
|
yes |
– |
Comma-separated list of project names |
|
no |
(all) |
Comma-separated list of schema names. Accepted, but currently not enforced by any tool — see Current limitations. |
|
no |
|
Comma-separated ISO 639-1 language codes |
|
no |
|
Reserved for future write tools; keep at |
|
no |
|
Maximum GraphQL nesting depth, |
|
no |
|
GraphQL request timeout in milliseconds, at least |
|
no |
|
|
REST requests to Gentics Mesh use a fixed timeout of 10 seconds.
| Variable | Default | Meaning |
|---|---|---|
|
|
|
|
|
Bind address in HTTP mode |
|
|
Bind port in HTTP mode, |
|
– |
Bearer token. Required in HTTP mode unless |
|
|
|
|
– |
Comma-separated origin allowlist for browser clients; |
| Variable | Default | When to change it |
|---|---|---|
|
|
Global weighted permits per second. Raise when many tokens are expected to produce real load. |
|
|
Weighted permits per second per Bearer token. Lower to throttle individual agents harder. |
|
|
Bulkhead permits. Should mirror the Gentics Mesh capacity; one to two times the Gentics Mesh CPU cores is a reasonable start. |
|
|
|
|
|
Lower means a more sensitive breaker, |
|
|
Larger is more stable but reacts later. |
|
|
How long Gentics Mesh gets to recover. |
|
|
Maximum characters in a GraphQL query. Raise for power-user queries, lower when models build overly long ones. |
In HTTP mode the following endpoints are available without authentication. They are meant for the cluster — Kubernetes probes and a Prometheus scraper — not for clients.
| Endpoint | Meaning |
|---|---|
|
Liveness: |
|
Readiness: |
|
Prometheus text format. |
|
The MCP endpoint. Authentication required. |
mcp_uptime_seconds 3845.2
mcp_http_requests_total 1284
mcp_http_auth_failed_total 7
mcp_tool_calls_total{tool="mesh_get_node"} 612
mcp_tool_errors_total{tool="mesh_get_node"} 4
mcp_tool_duration_ms_total{tool="mesh_get_node"} 91240
mcp_rate_limit_rejected_total{scope="token",tool="mesh_run_graphql"} 42
mcp_rate_limit_rejected_total{scope="global",tool="mesh_search_nodes"} 5
mcp_concurrency_rejected_total{tool="mesh_get_node"} 11
mcp_circuit_breaker_rejected_total 8
mcp_circuit_breaker_state{state="CLOSED"} 1
mcp_circuit_breaker_state{state="OPEN"} 0
mcp_circuit_breaker_state{state="HALF_OPEN"} 0
mcp_circuit_breaker_state is a set of 0/1 gauges, one per state, in the same shape as the Resilience4j Prometheus binding, so alerts can be written on mcp_circuit_breaker_state{state="OPEN"} == 1.
Metrics are kept in memory and reset with the process.
The image is built by the Gentics Mesh Enterprise pipeline and tagged with the Gentics Mesh version:
docker.gentics.com/gentics/mesh-enterprise/mcp-server:<version>
The image runs as a non-root user on a JRE 17 base. In stdio mode no port is exposed and no init wrapper is used — that would break the stdio pipes.
docker run -i --rm \
-e MESH_BASE_URL=http://mesh:8080 \
-e MESH_USERNAME=admin \
-e MESH_PASSWORD=admin \
-e MESH_ALLOWED_PROJECTS=demo \
docker.gentics.com/gentics/mesh-enterprise/mcp-server:<version>
docker run --rm -p 3001:3001 \
-e MESH_BASE_URL=http://mesh:8080 \
-e MESH_ALLOWED_PROJECTS=demo \
-e MCP_TRANSPORT=http \
-e MCP_AUTH_TOKEN=$(openssl rand -base64 32) \
docker.gentics.com/gentics/mesh-enterprise/mcp-server:<version>
Run the HTTP transport as a Deployment with a Service, and point MESH_BASE_URL at the Gentics Mesh Service.
Use httpGet probes on /healthz and /readyz, put MCP_AUTH_TOKEN into a Secret, and scrape /metrics with Prometheus.
The MCP Server is stateless, so it can be scaled horizontally; note that rate limits and the circuit breaker are per instance.
The Gentics Mesh Helm chart does not include the MCP Server at the moment.
The server is a single executable JAR that needs Java 17:
export MESH_BASE_URL=http://localhost:8080
export MESH_ALLOWED_PROJECTS=demo
export MESH_USERNAME=admin
export MESH_PASSWORD=admin
java -jar mesh-mcp-server.jar # stdio
MCP_TRANSPORT=http MCP_AUTH_TOKEN=secret java -jar mesh-mcp-server.jar # HTTP
Both transports share one JAR; the transport is the only thing that differs. Remember that the Gentics Mesh credentials must be absent in HTTP mode.
{
"mcpServers": {
"gentics-mesh": {
"command": "java",
"args": ["-jar", "/path/to/mesh-mcp-server.jar"],
"env": {
"MESH_BASE_URL": "http://localhost:8080",
"MESH_USERNAME": "admin",
"MESH_PASSWORD": "admin",
"MESH_ALLOWED_PROJECTS": "demo"
}
}
}
}
The container image works the same way with "command": "docker" and "args": ["run", "-i", "--rm", …].
{
"mcpServers": {
"gentics-mesh": {
"url": "https://mcp.example.com/mcp",
"headers": { "Authorization": "Bearer <MCP_AUTH_TOKEN>" }
}
}
}
from openai import OpenAI
client = OpenAI()
resp = client.responses.create(
model="gpt-4.1",
tools=[{
"type": "mcp",
"server_label": "gentics-mesh",
"server_url": "https://mcp.example.com/mcp",
"headers": {"Authorization": "Bearer <MCP_AUTH_TOKEN>"},
}],
input="List the projects in Gentics Mesh.",
)
print(resp.output_text)
from langchain_mcp_adapters.client import MultiServerMCPClient
mcp = MultiServerMCPClient({
"gentics-mesh": {
"url": "https://mcp.example.com/mcp",
"transport": "streamable_http",
"headers": {"Authorization": "Bearer <MCP_AUTH_TOKEN>"},
}
})
tools = await mcp.get_tools()
Both take the endpoint URL https://mcp.example.com/mcp and an Authorization: Bearer … header in their MCP client settings. The tools are discovered automatically.
TOKEN=<MCP_AUTH_TOKEN>
curl -s http://localhost:3001/mcp \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq
curl -s http://localhost:3001/mcp \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"mesh_list_projects","arguments":{}}}' | jq
For stdio the MCP Inspector is the quickest way to try the tools interactively:
npx @modelcontextprotocol/inspector --command java --args -jar,mesh-mcp-server.jar
| Symptom | Likely cause |
|---|---|
Server exits at startup with |
A required variable is missing or a value is out of range. The message lists every problem at once. |
Server exits in HTTP mode because |
Gentics Mesh credentials are stdio-only. Remove them; in HTTP mode the identity comes from the request or is anonymous. |
stdio start fails with a message about the user |
The API key or username/password were not accepted by Gentics Mesh. |
All tools return empty results, no error (HTTP mode with |
Requests are anonymous, and the |
|
Not on |
|
Elasticsearch is not configured or not synchronised on the Gentics Mesh side. |
HTTP 429 with |
The client is faster than the configured limits. Back off, or raise the limits if Gentics Mesh has the capacity. |
HTTP 503 with |
Gentics Mesh was failing recently. Check Gentics Mesh; the breaker closes on its own once probe calls succeed. |
All tools are read-only; there are no write tools.
MESH_ALLOWED_SCHEMAS is parsed and validated but not enforced by any tool. The schema argument of mesh_search_nodes is passed to Elasticsearch as given.
The mesh_get_node input schema advertises an optional resolveReferencesDepth argument; it is currently ignored, and references are always returned as field and uuid only.
The HTTP transport is request/response only; server-initiated SSE streams are not supported.
Rate limits, bulkhead and circuit breaker are per instance and are not shared between replicas.
Only the static Bearer token is shipped as an authentication scheme.
Authentication — API tokens, anonymous access and how Gentics Mesh identifies a request
GraphQL — the query language behind mesh_run_graphql
Elasticsearch — the search integration behind mesh_search_nodes
Monitoring — Prometheus metrics of Gentics Mesh itself