Skip to main content

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 /mcp for client-to-server JSON-RPC messages
  • GET /mcp for server-to-client event streams (SSE)
  • DELETE /mcp for 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:

  1. The client sends a tools/call JSON-RPC request with the tool name and arguments.
  2. The server checks rate limits and resolves the session context.
  3. If a plugin intercepts the tool (e.g., for authentication), the plugin handles execution with custom logic.
  4. Otherwise, the tool's GraphQL operation is sent to the gateway using persisted document IDs.
  5. Response tokens (access, refresh, data) are extracted and stored in the session.
  6. 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:

MetricTypeDescription
mcp.tool.invocationsCounterTool calls by name and status
mcp.tool.durationHistogramTool execution time in ms
mcp.sessions.activeUpDownCounterCurrently active sessions
mcp.sessions.createdCounterTotal sessions created
mcp.sessions.closedCounterSessions closed by reason

Structured logging via pino provides per-session context for debugging.