Architecture
Evolve's MCP implementation follows a layered architecture where MCP servers sit
between AI clients and the GraphQL Federation gateway. The @evolve-packages/mcp-core
package provides all the building blocks, while individual services like
mcp-customer compose them into a running server.
System overview
Transport layer
MCP servers support two transport mechanisms:
HTTP Streamable (SSE) for web-based and remote clients. The server exposes
a single /mcp endpoint that handles:
POST /mcpfor client-to-server JSON-RPC messagesGET /mcpfor server-to-client event streams (SSE)DELETE /mcpfor session termination
stdio for local clients like Claude Desktop. Uses standard input/output streams with the same JSON-RPC protocol.
Both transports are interchangeable. The same tools and plugins work regardless of which transport is used.
Session management
Each client connection creates a session with isolated state:
- Session ID: unique identifier for the connection
- Authentication context: access token, refresh token, and data token
- Store context: locale, currency, store key, and customer group
- Client tracking: which client connected and when
Sessions are managed in-memory with atomic operations. The session manager handles lifecycle events (creation, updates, termination) and emits telemetry.
Request flow
When an AI client invokes a tool, the following happens:
- The client sends a
tools/callJSON-RPC request with the tool name and arguments. - The server checks rate limits and resolves the session context.
- If a plugin intercepts the tool (e.g., for authentication), the plugin handles execution with custom logic.
- Otherwise, the tool's GraphQL operation is sent to the gateway using persisted document IDs.
- Response tokens (access, refresh, data) are extracted and stored in the session.
- The tool result is returned to the client.
Plugin system
Plugins can intercept specific tool calls to implement custom logic. This is used for flows that need more than a simple GraphQL query, such as authentication.
A plugin declares which tools it intercepts and provides a handler:
const plugin: MCPPlugin = {
name: "my-plugin",
version: "1.0.0",
interceptTools: ["tool_name"],
async handleTool(toolName, args, context, setSessionAuth) {
// Custom logic here
return { content: [{ type: "text", text: "result" }] };
},
};
The built-in customer-auth plugin intercepts customer_login and
customer_logout to manage session transitions between guest and authenticated
states.
Authentication & tokens
MCP servers manage a multi-token authentication system:
- Access token: short-lived JWT for API authorization
- Refresh token: used to obtain new access tokens
- Data token: optional token carrying additional claims
Token refresh happens automatically when a token is within a configurable threshold of expiry (default: 5 minutes). The server pre-validates JWTs (structure, expiry, issuer, audience) before forwarding to the gateway, providing defense-in-depth.
Query security & persisted documents
MCP tools execute GraphQL operations against the same gateway as the storefront. To prevent AI clients from executing arbitrary queries, the system uses persisted documents as an allowlist.
How it works
The codegen pipeline generates persisted-documents.json alongside
the tool definitions. This file maps document IDs (SHA-256 hashes of
the query string) to the actual GraphQL operations:
{
"f8567d7e...": "query GetProducts($filters: ...) { productSearch(...) { ... } }",
"8a7edd50...": "mutation AddToCart($cartId: ID!, ...) { cartAddLineItems(...) { ... } }"
}
These document IDs are registered with
GraphQL Hive's persisted
documents feature at deploy time. At runtime, the MCP server sends
only the documentId instead of the full query string:
// The GraphQL executor sends the persisted document ID
body: JSON.stringify({ documentId, variables })
The gateway rejects any request with an unregistered document ID. This
means the AI can only execute the exact queries that were defined in
operations.graphql and approved at build time. It cannot construct
arbitrary queries, access fields not included in the selection sets, or
bypass the operation boundaries you've defined.
Same security model as the storefront
This is not MCP-specific. The storefront frontend uses the same persisted documents mechanism. Both the AI and the web client operate under identical constraints: only pre-registered queries execute, and the gateway rejects everything else.
Schema changes that break an MCP tool's query are caught in CI when persisted documents are validated against the current schema. If a field is removed or a type changes, the build fails before it reaches production.
Selective field exposure
Beyond the allowlist, you control what data the AI sees through the
GraphQL selection sets in operations.graphql. A GetProducts tool
for the AI doesn't need to include internal pricing tiers, margin
data, or supplier information: just don't select those fields. The
operation itself is the contract with the AI.
The exclude flag
Some operations need to exist in the codebase (for token refresh or session management) but should not be exposed as tools to the AI:
mutation RefreshToken
@mcpTool(description: "Internal token refresh", exclude: true) {
refreshToken { accessToken }
}
Operations with exclude: true are included in the persisted
documents (so they can be executed at runtime by the plugin system)
but are omitted from the generated tool definitions. The AI never sees
them.
Observability
MCP servers emit OpenTelemetry metrics for monitoring:
| Metric | Type | Description |
|---|---|---|
mcp.tool.invocations | Counter | Tool calls by name and status |
mcp.tool.duration | Histogram | Tool execution time in ms |
mcp.sessions.active | UpDownCounter | Currently active sessions |
mcp.sessions.created | Counter | Total sessions created |
mcp.sessions.closed | Counter | Sessions closed by reason |
Structured logging via pino provides per-session context for debugging.