DEV Community

gentic news
gentic news

Posted on • Originally published at gentic.news

Integration Testing Your MCP Server: The Pattern That Caught 3 Hidden Bugs

Integration test your MCP server by spinning it up with STDIO and sending real tools/call requests. This catches startup races, JSON-RPC framing errors, and state bugs that 90% unit test coverage missed.

Key Takeaways

  • Integration test your MCP server by spinning it up with STDIO and sending real tools/call requests.
  • This catches startup races, JSON-RPC framing errors, and state bugs that 90% unit test coverage missed.

The Problem: 90% Coverage, Still Breaking in Production

You've built an MCP server. It's your shared memory for AI agents—recording failures so one agent warns another before hitting the same bug. Unit tests pass. SQLite layer? Checked. MCP transport with mocks? Green. Retry logic? Solid.

But in production, things break. A tool call that works in isolation fails when session tracking hasn't initialized. A fix that passes with one MCP client crashes with another. The issue isn't the code—it's the interactions between components.

The Fix: Real MCP Server, Real STDIO, Real Tests

The pattern: spin up your actual MCP server via subprocess, send real JSON-RPC requests over STDIO, and assert on the responses. No mocks, no stubs, no fakes.

import subprocess
import json
import time
import sys

def call_tool(server, name, args=None):
    request = {
        "jsonrpc": "2.0",
        "id": int(time.time() * 1000),
        "method": "tools/call",
        "params": {"name": name, "arguments": args or {}}
    }
    server.stdin.write(json.dumps(request) + "\n")
    server.stdin.flush()
    line = server.stdout.readline()
    return json.loads(line)

server = subprocess.Popen(
    ["python", "-m", "mcp_failure_server", "--stdio"],
    stdin=subprocess.PIPE, stdout=subprocess.PIPE,
    stderr=subprocess.PIPE, text=True
)

record = call_tool(server, "record_failure", {
    "tool": "browser_navigate", "error": "Connection refused"
})
assert record.get("id")

query = call_tool(server, "query_similar", {
    "tool": "browser_navigate", "error_fragment": "refused"
})
assert len(query.get("matches", [])) > 0

server.terminate()
print("Integration tests passed")
Enter fullscreen mode Exit fullscreen mode

Why This Catches Bugs Unit Tests Miss

Manual Software Testing: Uncovering …

1. Startup order matters. MCP servers initialize in sequence: open database, load cache, register tools, listen on STDIO. This test caught a bug where the cache was queried before it finished loading—returning stale data. Unit tests never saw this because they called functions after manual setup.

2. JSON-RPC framing is not trivial. A 4KB failure report with URL-encoded characters caused readlines() to split mid-payload. Only a real STDIO conversation reveals this. Your mock client probably sends perfectly framed messages.

3. State accumulates. After 150 entries, deduplication used LIMIT 1 without ORDER BY, returning a random match. Unit tests with 3–5 entries never caught this. Integration tests with realistic data volumes did.

Gotchas to Avoid

  • Don't test in production. Use tempfile.mkstemp() for the database and clean up in finally.
  • Add timeouts. The server might wait for a signal that never comes on STDIO mode. Add a 3-second alarm.
  • Clean up processes. Always terminate() and .wait() in try/finally to avoid zombie processes.

What About Your MCP Server?

Claude Code users routinely build MCP servers for custom tools—database queries, API integrations, file operations. If you're only unit testing, you're missing the bugs that happen when components actually talk to each other.

Add one integration test today. It will catch things you didn't know were broken.


Source: dev.to

[Updated 30 Jul via gn_mcp_protocol]

Beyond testing for correctness, MCP servers also need security audits: a scan of 24 open-source MCP projects found one critical sandbox command injection (CVSS 9.8) in AgenticX (202⭐), where file operation methods used unsanitized f-strings in shell commands [per dev.to]. The same scanner found zero real vulnerabilities in top-tier projects like Cline (65k⭐) and OpenHands (82k⭐), suggesting that integration tests like the one described above can be extended to cover injection patterns.


Originally published on gentic.news

Top comments (0)