Architecting Agent Experience (AX): Deterministic Local Testing for MCP Servers and AI Agents
Architecting Agent Experience (AX): Deterministic Local Testing for MCP Servers and AI Agents
For the past decade, software architecture focused heavily on Developer Experience (DX). We optimized hot module reloading, typed public APIs, and tuned local dev environments. But in 2026, a new stakeholder consumes your codebase before any human developer ever touches it: the AI coding agent.
Designing APIs, documentation, and tooling specifically for LLMs is now recognized as Agent Experience (AX). Whether you are extending GitHub Copilot, building Model Context Protocol (MCP) tools, or wiring custom autonomous agents into your platform, your job is no longer just making code maintainable—it is making code discoverable and executable by LLMs.
However, testing AX introduces a massive architectural bottleneck. Every time you tweak a skill definition or updated an MCP server schema and trigger an evaluation suite, your agent:
- Calls live external services or mutations on test databases.
- Incurs high API token costs across multi-turn reasoning loops.
- Yields non-deterministic test failures due to upstream network latency or model drift.
In this deep dive, we will architect a zero-latency, zero-cost local evaluation suite that intercepts MCP JSON-RPC calls and REST endpoints, allowing you to run robust AX evals completely offline.
The Flakiness Problem in Agent Evals
When evaluating human-facing code, tests are deterministic: Input X produces Output Y. When evaluating an agent skill, tests evaluate intent and tool selection accuracy:
[User Prompt] -> [Agent Planner] -> [Tool Selection] -> [API Invocation] -> [Response Parsing] -> [Final Output]
If your eval suite hits live APIs during the [API Invocation] phase, two problems occur:
- Data Mutation Side-Effects: An agent testing a
delete_userordeploy_previewskill actually executes those actions in staging. - Cost & Speed Creep: A full evaluation matrix running 50 prompts with 4 tool turns each can easily burn 200 API calls and minutes of runtime per CI pull request.
To buildAX evals that actually scale, we must decouple agent reasoning evaluation from external side effects.
Architecture of a Local AX Mocking Proxy
To achieve true deterministic testing, we insert a lightweight local interceptor between the agent harness and your MCP server or backend endpoints.
+--------------------+ +------------------------+ +------------------------+
| | | Local AX Proxy | | Mock Fixture Store |
| AI Agent Harness | ----> | (Schema Interceptor) | ----> | |
| (Copilot/Claude) | | | | - Stored Schemas |
+--------------------+ +------------------------+ | - Response Determinism|
+------------------------+
The proxy performs three critical jobs:
- Schema Validation: Verifies that the tool call produced by the agent matches the strict JSON schema expected by your MCP server.
- Payload Interception: Intercepts outgoing JSON-RPC 2.0 requests (for MCP) or HTTP requests (for REST tools) and matches them against pre-recorded fixture states.
- Eval Assertion Verification: Asserts whether the agent supplied required context fields without executing the real underlying business logic.
Building the Mock Proxy Layer in TypeScript
Let us implement a practical, local interception transport for an agent tool skill using Node.js and standard JSON schema validation. This setup runs inside your CI pipeline prior to merging changes to your MCP tool interfaces.
1. Defining the Agent Skill Contract
Suppose we have an MCP tool called provision_preview_environment:
// src/schemas/environment.ts
import { z } from "zod";
export const ProvisionEnvSchema = z.object({
branch: z.string().describe("The Git branch to deploy from"),
ttlHours: z.number().min(1).max(72).default(24).describe("Lifetime of the preview env"),
region: z.enum(["us-east-1", "eu-west-1", "ap-southeast-1"]).describe("Target cloud region"),
variables: z.record(z.string(), z.string()).optional().describe("Environment variable overrides")
});
export type ProvisionEnvInput = z.infer<typeof ProvisionEnvSchema>;
2. Creating the Interceptor Harness
Instead of exposing the live provisioning service during tests, we create an AX Mock Interceptor that records invocations and returns simulated operational outcomes:
// src/testing/axMockInterceptor.ts
import { ProvisionEnvSchema, ProvisionEnvInput } from "../schemas/environment";
export interface InterceptedCall {
timestamp: number;
toolName: string;
rawArgs: unknown;
isValid: boolean;
validationErrors?: string[];
}
export class AXMockServer {
private calls: InterceptedCall[] = [];
// Intercept incoming MCP tool calls without executing real infra code
public async handleToolCall(toolName: string, args: unknown): Promise<{ success: boolean; data?: any; error?: string }> {
if (toolName !== "provision_preview_environment") {
return { success: false, error: `Unknown tool call: ${toolName}` };
}
const parseResult = ProvisionEnvSchema.safeParse(args);
if (!parseResult.success) {
const errors = parseResult.error.errors.map(e => ``e.path.join("."):`{e.message}`);
this.calls.push({
timestamp: Date.now(),
toolName,
rawArgs: args,
isValid: false,
validationErrors: errors
});
return {
success: false,
error: `Invalid tool parameters: ${errors.join(", ")}`
};
}
// Record successful interception
this.calls.push({
timestamp: Date.now(),
toolName,
rawArgs: parseResult.data,
isValid: true
});
// Return deterministic mock payload
return {
success: true,
data: {
environmentId: "env-mock-99823",
status: "ready",
url: `https://mock-${parseResult.data.branch}.internal.dev`,
allocatedRegion: parseResult.data.region
}
};
}
public getHistory(): ReadonlyArray<InterceptedCall> {
return this.calls;
}
public reset(): void {
this.calls = [];
}
}
3. Writing the AX Evaluation Test
Now, we run our agent prompt against the mock server to evaluate if the agent chose the correct parameters based on user context:
// tests/ax/provisionEnv.eval.ts
import { AXMockServer } from "../../src/testing/axMockInterceptor";
import { assert } from "chai";
describe("AX Eval: Environment Provisioning Skill", () => {
let mockServer: AXMockServer;
beforeEach(() => {
mockServer = new AXMockServer();
});
it("correctly extracts branch and region from user prompt", async () => {
// Simulated prompt: 'Deploy a preview for feature/auth-fix in London for 12 hours'
// The agent framework converts this prompt to a tool call:
const simulatedAgentOutput = {
tool: "provision_preview_environment",
args: {
branch: "feature/auth-fix",
region: "eu-west-1",
ttlHours: 12
}
};
const response = await mockServer.handleToolCall(
simulatedAgentOutput.tool,
simulatedAgentOutput.args
);
// Assertion 1: Tool execution should succeed schema validation
assert.isTrue(response.success, "Agent parameters failed schema validation");
// Assertion 2: Verify recorded calls for exact arguments
const history = mockServer.getHistory();
assert.lengthOf(history, 1);
assert.isTrue(history[0].isValid);
assert.strictEqual((history[0].rawArgs as any).region, "eu-west-1");
});
});
Measuring the Impact: Live vs Mocked Evals
By moving from live network interactions to local schema proxy testing during CI runs, your eval suite performance shifts dramatically:
| Metric | Live API / External Skill Runs | Local AX Interceptor Layer |
| :--- | :--- | :--- |
| Latency per Test | 1,200ms - 4,500ms | 3ms - 8ms |
| Cost per 1,000 Runs | 15.00 -45.00 | $0.00 |
| Deterministic Pass Rate | ~82% (network/rate limits) | 100% |
| Side Effect Risk | High (Staging DB pollution) | Zero |
Advanced AX Strategies: Catching Tool Misunderstandings
When agents fail in production, it is rarely because the LLM failed to output JSON. It is usually because the API schema was ambiguous. Here are three architectural rules for optimizing your tools for agents:
- Eliminate Overloaded Field Types: Avoid fields that accept
string | string[]. Force distinct types or separate tools. Agents frequently fail on union types in tool parameters. - Use Explicit Enum Descriptions: Instead of
region: string, use an enum with descriptions for each valid value. The LLM relies on schema descriptions to perform zero-shot map matching. - Fail Fast with Meaningful Schema Messages: When your mock server catches a validation error, pass the schema error back to the agent in the eval test to see if the agent can self-correct in a multi-turn retry loop.
Key Takeaways
- Agent Experience (AX) is a First-Class Concern: Designing schemas, documentation, and tools for AI agents requires dedicated testing pipelines just like DX.
- Never Test Skills Against Live Infra in CI: Directing agent evals at real APIs leads to rate limits, unintended data mutations, high costs, and test flakiness.
- Intercept early at the Transport Layer: Build local proxies for MCP JSON-RPC endpoints to validate schema conformance and arguments deterministically in milliseconds.
- Evaluate Intent and Parameter Accuracy: Measure if the agent extracted the right variables from the prompt rather than waiting for downstream infrastructure side-effects.
What You Should Do Today
- Audit Your Tool Schemas: Review your current API parameter schemas. Add explicit description strings to all fields used by coding agents or custom extensions.
- Isolate Your MCP Evals: Wrap your MCP tool handlers in a local mock server using tools like
mswor a custom proxy harness before pushing updates to production. - Add AX Schema Validation to Pre-commit Hooks: Prevent ambiguous or non-conforming schema changes from reaching main branches by validating tool definitions against sample agent payloads locally.
More TechSheets
Architecting for Agent Experience (AX): Deterministic Testing and API Mocking for AI Skills
Learn how to build deterministic evaluation harnesses and transparent API mocks for AI agent skills without burning API credits or mutating production data.
Don't Rewrite Your CLI for AI Agents: Tool Design in the AX Era
Why forcing single JSON payloads onto CLI tools breaks AI coding agents, and how to design robust Agent Experience (AX) interfaces that actually work.
Mastering Next.js 15 and React Server Components: Architecture, Performance, and Best Practices
A deep-dive guide for Senior Developers on Next.js 15 features, React Server Components (RSC) patterns, and strategies for building high-performance web applications.