Testing an MCP Server Against a Real Agent Client
Unit tests on handlers are not enough. MCP bugs often live in transport framing, initialize negotiation, or host-specific config.
You need a layered test strategy: protocol client harness, host install check, and optional full agent loop.
Summary
Build an automated stdio (or HTTP) MCP client that initializes, lists tools, and calls golden paths; then install the same server in a real host (VS Code, Claude Desktop, or an Agents SDK MCP connector) and run a short scripted task that must invoke your tools correctly.
Recipe
- Extract pure business logic and unit-test it without MCP.
- Add an integration harness using the official client SDK over stdio (or HTTP).
- Assert initialize succeeds, tool names match a freeze list, and schemas include required fields.
- Call each critical tool with valid and invalid args; assert content and error behavior.
- Fail CI on stdout leakage for stdio servers (subprocess capture should stay JSON-RPC-clean).
- Install the server in one real host with the exact production command line.
- Run a human or scripted chat: "use the echo tool with pong" and confirm the host shows a tool call.
- Optionally drive an agent framework with MCP tools enabled and assert the final answer depends on tool output.
- Record fixtures for flaky upstreams; keep network off in default CI when possible.
Working Example
# tests/test_mcp_smoke.py - verify client APIs at build
import asyncio
import sys
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
SERVER = [
sys.executable,
"/ABSOLUTE/PATH/TO/server.py",
]
async def smoke() -> None:
params = StdioServerParameters(command=SERVER[0], args=SERVER[1:])
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
listed = await session.list_tools()
names = sorted(t.name for t in listed.tools)
assert "echo" in names, names
ok = await session.call_tool("echo", {"message": "pong"})
texts = [c.text for c in ok.content if hasattr(c, "text")]
assert any("pong" in t for t in texts), texts
bad = await session.call_tool("echo", {}) # missing args
# Prefer structured isError when available; at least ensure no crash
assert bad is not None
def test_mcp_smoke() -> None:
asyncio.run(smoke())pytest tests/test_mcp_smoke.py -qReal host checklist (manual or documented runbook):
- Config entry uses the same absolute command as CI.
- Host restart after config change.
- Tools panel lists expected names.
- One forced tool invocation returns expected text.
- stderr logs visible in host log viewer if something fails.
Deep Dive
Three layers of truth
| Layer | What it proves | What it misses |
|---|---|---|
| Unit tests | Business logic | Protocol, schemas, host UX |
| MCP client harness | Wire protocol + handlers | Host UI, OAuth browser, model routing |
| Real host / agent | End-to-end usability | Full matrix of every host |
Ship blockers usually need the middle layer green. Product confidence needs the third.
Schema freezes
Snapshot tool names and required JSON Schema properties in CI.
Unexpected renames break agents that hardcode tool names in prompts or evals.
Invalid arguments
Models will send wrong types. Your server should return a controlled error content path, not segfault.
Harness tests with empty objects, wrong types, and boundary values.
Timing and flakes
Real upstream APIs need retries and recorded fixtures.
Default CI should use fakes so MCP protocol tests stay deterministic.
HTTP servers
Add harness cases for:
- Missing
Mcp-Session-Idafter initialize - Wrong
MCP-Protocol-Versionheader - 401 without bearer token (when auth is enabled)
Agent-level tests
When using OpenAI Agents SDK, Claude Agent SDK, or similar with native MCP, write one eval case: user goal only solvable via your tool.
Assert the transcript contains a tool call to your server, not just a plausible hallucination.
Gotchas
- Testing only handlers - never catches stdout corruption.
- Different commands in CI vs host - "works in pytest, fails in Desktop."
- No absolute paths - cwd-dependent failures.
- Assuming list order - sort names before compare.
- Ignoring
isError- success HTTP/RPC with error payload. - Live credentials in CI - use stubs; secret scans will fail your repo.
- One-host myopia - VS Code behavior is not Claude Desktop behavior.
- No timeout - hung tools freeze the suite; set client timeouts.
Alternatives
| Approach | When it wins | When it loses |
|---|---|---|
| Client harness (this recipe) | Fast, automatable protocol truth | Weak UX signal |
| Host-only manual QA | Early prototype | Regressions return |
| Contract tests from OpenAPI | REST backends | Skips MCP framing |
| Full multi-agent eval suite | Production agents | Expensive for tiny servers |
FAQs
Is the MCP Inspector enough?
Interactive inspectors are great for debugging. Keep a headless harness for CI so merges stay honest.
Should I mock the transport?
Mock business dependencies, not the transport, for smoke tests. You want real JSON-RPC bytes.
How do I test resources and prompts?
Mirror the tool smoke: list, then read/get with fixtures. Assert mime types and template text.
What about auth-heavy remote servers?
Split tests: unauthenticated endpoints return 401 with WWW-Authenticate; authenticated path uses a test token from a local IdP or stub verifier.
Can I run the server once for the whole suite?
Yes for HTTP. For stdio, per-test processes are slower but isolate state. Prefer isolation unless startup is heavy.
How do I detect stdout leaks?
In the harness, ensure the client still parses messages; optionally wrap the server to fail if non-JSON lines appear on stdout.
Do golden files for tool dumps help?
Yes for stable formatters. Avoid golden files for timestamps and UUIDs without scrubbers.
When is an agent eval mandatory?
When tool choice is non-obvious or descriptions are subtle. Protocol green does not prove the model will call you.
Related
Related: Building & Deploying MCP Basics
Related: Building Your First MCP Server in TypeScript or Python
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.