Skip to content

New: AI agent integration via Model Context Protocol (MCP).Learn more

Architecture

Evolve’s MCP implementation follows a layered architecture where MCP servers sit between AI clients and the GraphQL Federation gateway. The @evolve-framework/mcp-core package provides all the building blocks, while individual services like mcp-customer compose them into a running server.

graph TD;
    subgraph clients["MCP Clients"]
        claude["Claude Desktop (stdio)"]
        web["Web AI Assistant (HTTP)"]
        custom["Custom Client (HTTP)"]
    end

    subgraph mcp-server["MCP Server"]
        transport["Transport Layer"]
        sessions["Session Manager"]
        handlers["Tool Handlers"]
        plugins["Plugins"]
        auth["Auth & Token Management"]
        graphql-client["GraphQL Client"]
    end

    claude-->|stdio|transport
    web-->|HTTP SSE|transport
    custom-->|HTTP SSE|transport

    transport-->sessions
    sessions-->handlers
    handlers-->plugins
    handlers-->graphql-client
    plugins-->graphql-client
    auth-->sessions
    auth-->graphql-client
    graphql-client-->gateway["GraphQL Gateway"]
    gateway-->services["Backend Services"]

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) — declined with 405 in JSON-response mode, see below
  • 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.

The HTTP transport runs in JSON-response mode (enableJsonResponse: true): POST /mcp replies with a single JSON body instead of an SSE stream, because buffering reverse proxies (e.g. Azure Container Apps ingress) stall SSE streams on their idle timeout. Consequently GET /mcp is declined with 405 Method Not Allowed per the MCP spec (“server does not offer an SSE stream”) and clients fall back to POST-only request/response. When PROXY_AUTH_ENABLED is set, a trusted proxy (such as a chat backend) can forward a pre-authenticated session via the X-Access-Token / X-Data-Token / X-Refresh-Token headers, which take precedence over Authorization: Bearer and skip JWT pre-validation — token validity is still enforced by the downstream gateway.

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.

When an AI client invokes a tool, the following happens:

sequenceDiagram
    participant Client as MCP Client
    participant Server as MCP Server
    participant Plugin as Plugins
    participant GQL as GraphQL Client
    participant Gateway as GraphQL Gateway

    Client->>Server: tools/call (JSON-RPC)
    Server->>Server: Rate limit check
    Server->>Server: Resolve session context

    alt Tool intercepted by plugin
        Server->>Plugin: handleTool()
        Plugin->>GQL: Execute GraphQL
        GQL->>Gateway: HTTP POST /graphql
        Gateway-->>GQL: Response + tokens
        Plugin-->>Server: Tool result
    else Normal execution
        Server->>GQL: Execute GraphQL
        GQL->>Gateway: HTTP POST /graphql
        Gateway-->>GQL: Response + tokens
        GQL-->>Server: Tool result
    end

    Server->>Server: Update session tokens
    Server->>Server: Record telemetry
    Server-->>Client: CallToolResult
  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.

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.

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.

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.

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.

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.

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.

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.

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.