Chapter 20

Test, Debug, and Release the MCP Server

Testing an MCP server is made of four layers, and skipping any of them leaves a hole. First - pure domain tests, where the business logic is checked without the protocol. Then protocol contract tests. Then a real transport - an actually launched process, not in-memory calls. And finally security and failure tests, where you deliberately break the system. The Inspector helps explore the server by hand, but it doesn't replace an automatic suite.

An integration test must actually bring up the stdio server - otherwise you're not testing framing and shutdown.

TypeScript
import assert from "node:assert/strict";
import test from "node:test";

import { runMcpDemo } from "../src/mcp/client.js";

test("MCP client and server agree on the modern revision", async () => {
  const result = await runMcpDemo();
  assert.equal(result.era, "modern");
});

test("MCP discovery returns tools, resources, and prompts", async () => {
  const result = await runMcpDemo();
  assert.deepEqual(result.tools, ["search_runbooks", "review_release"]);
  assert.ok(result.resources.includes("runbook://release-policy"));
  assert.ok(result.prompts.includes("prepare-release-review"));
});

test("MCP tool returns validated structured output", async () => {
  const result = await runMcpDemo();
  assert.equal(result.decision, "ready");
  assert.equal(result.policyTitle, "Release policy");
});

A local review before release is a few commands, including manual inspection.

Bash
npm run typecheck
npm test
npm run demo:mcp

# Manual protocol inspection
npx @modelcontextprotocol/inspector <server command>

The main value is in the negative tests: what matters is not that the happy path works, but that it breaks predictably. The table shows what's worth breaking deliberately and what proof you expect.

Layer · What to break deliberately · Expected proof

  • Schema - An empty service, a bad semver; Invalid params before the domain
  • Authorization - A foreign tenant/object; The same safe deny
  • Transport - Garbage on stdout, an abort, a half-close; A clean finish with no hung process
  • Retry - A timeout after a side effect; Idempotency prevents a duplicate
  • Cache - A token change/list_changed; Private data isn't reused
  • Compatibility - Modern and a supported legacy peer; The negotiated era matches the expectation

Behind each row is a concrete class of bugs. An empty service or a bad semver must be filtered out before the domain. A foreign tenant - give the same safe deny. Garbage on stdout, an abort, or a half-close - finish cleanly, with no hung process. A timeout after a side effect - not lead to a duplicate, thanks to idempotency.

The release is worth accompanying with a release manifest: protocol version, the SDK lockfile, the server name/version, capability schemas, authorization scopes, golden requests, timeout limits, and a changelog. Publishing to a registry improves discovery but is not a certificate of trust.

The Inspector shows capability, not security. A successful manual tools/call doesn't prove object authorization, behavior under load, correct cancellation, or the absence of a secret leak. Only automatic tests prove that.

With this the MCP side is assembled in full - from the boundary to the release. Next we move to the system's second boundary, where the work is delegated to an independent agent. This is A2A.

Links