Skip to main content
Version: 2026.1

MCP Integration

The Model Context Protocol (MCP) is how the agent reaches every Pimcore capability - data objects, assets, documents, tags - and any third-party service you wire up. The bundle splits MCP servers into two families:

  • internal (authored in PHP, served by Pimcore) and
  • external (third-party).

They are authenticated and authorized differently.

Internal MCP servers

Internal MCP servers are PHP endpoints at /pimcore-mcp/agent/*, running inside the Pimcore application. They expose data-object CRUD, asset management, document editing, tag operations, and anything else a bundle contributes via the pimcore.mcp_tool service tag.

Tools are organised into tool groups by domain and access level (data-objects, assets, tags, documents, …). The McpToolRegistryPass compiler pass collects every tagged service at container compilation and wires up one MCP endpoint per group automatically. To add your own tools, see Extending → Custom MCP Tools.

The documents tool group has its own architectural layer because document editables are determined by Twig templates rather than class definitions - see Document Schema for how the agent learns the editable surface of a document or areabrick (AST analysis + co-located example fixtures).

Two ways to expose internal tools to an agent

An agent's YAML controls which internal tool groups it sees, and how the LLM sees them:

Config keyPresentationWhen to use
pimcoreMcpServersFull tool schemas loaded into context on every turn. LLM can call any tool in one roundtrip.An agent's core tools - the ones it uses frequently.
pimcoreMetaGroupsTools wrapped behind a meta-tool server. LLM first discovers, then describe_tools, then executes.Supplementary tools the agent needs sometimes but shouldn't pay for every turn.

Both routes land on the same underlying PHP tools. The choice is about token budget and discoverability. An agent can use both: core tools in pimcoreMcpServers, the long tail in pimcoreMetaGroups.

The meta-tool server

The meta-tool server lives at /pimcore-mcp/agent/meta?groups=<group1>,<group2> and exposes exactly three tools regardless of how many PHP tools hide behind it:

ToolPurpose
discoverList available groups and their tool names.
describe_toolReturn the full parameter schema for a named tool.
executeRun a tool by name with JSON arguments.

Allowed groups are set server-side from the agent's pimcoreMetaGroups; the LLM cannot influence the list. The token overhead is constant at about 1,200 tokens regardless of group count - the main reason the bundle ships nearly every Pimcore capability through the meta-tool by default.

External MCP servers

Third-party servers run independently of Pimcore and are configured per agent under mcpServers. HTTP transport, typically bearer-token authenticated.

mcpServers:
pimcore-datahub:
type: http
url: ${MCP_PIMCORE_URL}
headers:
Authorization: "Bearer ${MCP_PIMCORE_TOKEN}"
tools: "*"

Environment variable interpolation is supported in url and headers. External servers do not participate in the tool-group system and never receive a user cookie.

Authentication forwarding

The bearer token (Authorization: Bearer pmcp_…) is the credential for all server-to-server PHP calls for a chat session - not just MCP. This covers both the internal MCP servers (/pimcore-mcp/agent/*) and the bundle API (/pimcore-studio/api/bundle/agent/*). The agent-server holds two request-scoped store instances:

  • PimcoreSessionStore.withBearer - used for all per-session operations on bundle/agent routes (proposal persistence, token refresh, etc.). Bound to the bearer at task start; survives cookie expiry.
  • PimcoreSessionStore.withCookie - used only for session-independent operations (create/list sessions, bulk delete, auth-validator, bootstrap ensureMcpToken).

McpAccessTokenAuthenticator in studio-backend resolves the bearer to the chat initiator's Pimcore User. Pimcore enforces the same workspace ACLs and user/role permissions that apply in Studio. The chat session id is bound to the token at issuance time and recovered server-side from the validated token - the previously-used X-Chat-Session-Id request header has been removed (it was forgeable). Neither the bearer nor the chat session id is controllable by the LLM.

External MCP servers (under mcpServers) never receive this bearer; they use whatever auth their YAML configures. See Authentication for the agent-server-facing flows.

Token lifecycle

The token format is pmcp_<64 hex>. It is stored hashed (SHA-256) at rest in bundle_studio_mcp_access_token; only the bearer the agent-server holds in memory is reversible.

Configurable TTL - pimcore_agent.chat_session_token.ttl (seconds, default 7200, minimum 60).

PhaseWhat happensReminted?
Issue (new chat)POST /pimcore-studio/api/bundle/agent/sessions returns { …, mcpToken: "pmcp_…" }. The agent-server bakes the bearer into the SDK session's MCP server headers.n/a (first mint)
Refresh (per turn, user-interactive)Before resuming an existing chat, the agent-server calls POST /sessions/{id}/mcp-token. If the live row's TTL is still in budget, PHP extends expires_at = now + ttl and returns { token: null, reminted: false } - the cached SDK session keeps its original bearer. This is the user-triggered path: it runs when a chat turn starts.false (sliding-window extend)
Refresh (server-driven, autonomous)While a task is running, TaskRunner calls refreshToken on a setInterval of max(60s, tokenTtlSeconds/2). The timer is cleared on every terminal status transition. This keeps long-running or overnight turns alive independently of any user interaction or cookie validity. Configured via AGENT_SERVER_MCP_TOKEN_TTL (see below).false (sliding-window extend)
Re-mint (token gone)If the row was GC'd or revoked, POST /sessions/{id}/mcp-token with { forceRemint: true } mints a fresh token under the same reference (chat session id), atomically deleting any prior row first, and returns { token: "pmcp_…", reminted: true }. The agent-server uses forceRemint on cold-resume after a restart, when no usable bearer is held in memory.true
Revoke (chat deleted)Single or bulk delete in the chat sidebar revokes the token row before the session record is removed.n/a
Cascade (user deleted)bundle_studio_mcp_access_token.user_id has ON DELETE CASCADE on users.id - deleting a Pimcore user atomically clears their tokens.n/a
GCThe studio_mcp_access_token_gc Pimcore maintenance task removes any row whose expires_at < now().n/a

The agent-server applies the result via the obtainMcpToken helper in routes/sse-helpers.ts. When reminted is true the chat route passes tokenReminted: true to resumeSession, which forces the SDK session to be destroyed and recreated - see SDK session rebuilds below.

SDK session rebuilds

The Copilot SDK caches an active session per chat with the MCP Authorization header baked into the attached servers at construction time. The agent-server has three reasons to destroy and recreate that SDK session (forceRecreate = isAgentSwitch || mcpResetPending || tokenReminted, see adapters/copilot/copilot-adapter.ts:426):

  1. Agent switch - the user picked a different agent definition.
  2. MCP transport reset pending - see Transport-session recovery below.
  3. Token reminted - the cached bearer is stale; the new one must be re-baked.

The hot path (no switch, no reset, no re-mint) early-returns the cached session. Conversation context is restored from the persistent session store via priorConversationContext on the first turn after a rebuild - the user never sees the gap, only a small recovery delay before the next response.

Transport-session recovery

The Pimcore-internal MCP servers, like any HTTP MCP transport, have their own session lifetime. The PHP MCP SDK garbage-collects idle sessions independently of the access token's TTL - short-lived in-memory transport sessions can die even while the bearer is still valid. When the upstream session is gone, the next MCP call returns 404 / 401 and the SDK surfaces the failure as a tool.execution_complete event with success: false - but the SDK's onPostToolUse hook never fires for transport-level errors, so a "look at the result and recover" approach in the application layer is not viable.

The agent-server addresses this by inspecting the raw SDK event stream (McpFailureInspector in agent-server/src/adapters/copilot/mcp-failure-detector.ts):

  1. Maps tool.execution_start events to toolCallId → toolName so it knows which server is in play.
  2. On tool.execution_complete with success: false, matches the error message against a small set of patterns (Session not found, 404, 401, Unauthorized, …) - false positives only cost one extra destroy-and-recreate cycle, so the matcher is deliberately liberal.
  3. Also reacts to session.mcp_server_status_changed events whose status is failed or needs-auth.
  4. When triggered, marks the app session for SDK reset (mcpResetPending).

On the next user turn, the Copilot adapter destroys the existing SDK session via deleteSession and re-creates it with a fresh mcp-session-id for every attached MCP server. Conversation history is carried over via the persistent session store (the same mechanism used for agent switching), so the user does not see a gap - only a small recovery delay before the next response.

If the failure was a 401 caused by a stale bearer (after a re-mint that for any reason didn't reach the cached SDK session - e.g. the recovery path), the chat route's retry callback forces tokenReminted: true so the rebuild always picks up the freshest token.

Things to know

  • Bearer never appears in browser network traffic. The token is server-side state in the agent-server process. The browser only sees Cookie (PHP session) on /agent-server/api/* and /pimcore-studio/api/*. If you see pmcp_ in Chrome DevTools, that's only the one-shot response body when a chat is created or re-minted - never an upstream request header.
  • Logs are redacted. Pino's redact config in agent-server/src/server.ts lists both req.headers.authorization and req.headers.cookie, with a custom serializers.req that emits headers (without it, redaction would be a silent no-op). The PHP side must do the same - see Installation → Logging.
  • _mcp_token_reference request attribute. On a token-authenticated request, studio-backend stashes the chat session id on request->attributes under that key. The HasChatSession trait used by chat-scoped MCP tools reads it from there. The previously-supported X-Chat-Session-Id request header has been removed
    • it was forgeable from the browser and is no longer trusted.
  • HTTPS in production. pmcp_… is a credential. Serve Studio (and the agent-server upstream link) over TLS - over plain HTTP these tokens are sniffable.
  • No effective-user override yet. EffectiveUserResolver currently returns the chat initiator. Agent configs cannot impersonate other users.