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.
System overview
Section titled “System overview”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"]
Transport layer
Section titled “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) — declined with405in JSON-response mode, see belowDELETE /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.
JSON-response mode and trusted-proxy auth
Section titled “JSON-response mode and trusted-proxy auth”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.
Session management
Section titled “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
Section titled “Request flow”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
- 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
Section titled “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
Section titled “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
Section titled “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
Section titled “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 IDbody: 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
Section titled “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
Section titled “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
Section titled “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
Section titled “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.

