AI Agent Assurance for Your Agent Deployments

Agent Assurance Tests How Your Agent Actually Behaves across Workflows, Tools, and Actions to Catch Failures and Vulnerabilities.

npm install -g @testmuai/rook

AITesting

MCP Testing: How to Test MCP Servers in 4 Layers

MCP testing in 4 layers for the 2026-07-28 spec: unit tests, Inspector CLI protocol checks, schema conformance, agent evals, security tests and CI/CD gates.

Last Updated on:

MCP testing verifies that an MCP server's tools, schemas, transport, and error handling behave correctly when an AI agent calls them. The Model Context Protocol specification defines two transports, stdio and Streamable HTTP, and requires every HTTP server to validate the Origin header on incoming connections to block DNS rebinding attacks.[1]

Revision 2026-07-28 also made MCP a stateless protocol: a server must not rely on earlier requests for context, so each request has to state its protocol version and client capabilities itself.[7] The checks in this guide target that revision and note what still applies to servers that keep serving 2025-11-25 clients.

Overview

MCP testing verifies that a Model Context Protocol server's tools, schemas, transport and error handling behave correctly when an AI agent calls them. Test an MCP server in four layers: unit tests, protocol tests, conformance checks and evaluation runs; only the evaluation layer needs a model. Use MCP Inspector for the protocol layer, then test what an agent actually does through your server with Agent Assurance, which runs from the Rook CLI.

How to Test an MCP Server in Four Layers

  • Unit tests: MCP unit tests run the handler function behind each tool with its dependencies mocked, which catches bad branch logic, unhandled nulls and wrong upstream error mapping. Needs a model: No (deterministic).
  • Protocol tests: MCP protocol tests drive server/discover, tools/list and tools/call through a real client, which catches capability mismatches, registration gaps and transport faults. Needs a model: No (a plain client is enough).
  • Conformance checks: MCP conformance checks confirm that each tool's inputSchema is valid, that structured results match the declared outputSchema and that errors follow the error rules of MCP revision 2026-07-28, catching schema drift and breaking field renames. Needs a model: No (deterministic).
  • Evaluation runs: MCP evaluation runs repeat each prompt and measure how often a model picks the correct tool and completes the task, which exposes ambiguous descriptions and overlapping tools. Needs a model: Yes. Gate the suite on a pass rate set in advance, and run it nightly and on tool changes.

MCP Server Failures That Unit Tests Miss

  • Tool execution errors: An MCP tool that fails on an API error, bad input or a business rule still returns a successful JSON-RPC result, with isError set to true. A test suite that watches only for protocol errors therefore reports green while the tool is broken.
  • Per-request _meta: Every MCP request in revision 2026-07-28 carries its protocol version and client capabilities in _meta. An MCP server must reject a request missing either field with error -32602 (Invalid params). Initialize handshake: No (revisions 2025-11-25 and earlier only).
  • Logging to stdout: A stdio MCP server must write only valid MCP messages to stdout and may send logs to stderr. One stray print statement corrupts the message stream, so a server that passes its own tests can fail the moment a client launches it.
  • Schema drift: When an MCP tool's inputSchema marks a parameter optional but the handler requires it, the tool passes discovery and fails at call time. Catch the mismatch by asserting on the real call as well as the schema, and fail the build when a tools/list snapshot changes without review.

Tools That Run MCP Tests

  • MCP Inspector CLI: The MCP Inspector CLI connects in the legacy protocol era by default, so CI runs add --protocol-era modern. With tools/list, --strict turns an error-severity tool-schema finding into exit code 6. Installation required: No (it runs through npx on Node 22.19.0 or newer).
  • MCP Python SDK Client: The official MCP Python SDK's Client(mcp) connects a pytest test straight to the server object in memory, so in-process protocol tests are fast enough to run on every commit. Subprocess or port required: No.
  • Agent Assurance: TestMu AI's Agent Assurance runs an agent you own that calls your MCP server on staging and checks each criterion against observed tool calls, files and read-only checks. What it could not observe is reported as Unable to Verify, never as a pass or a failure. Status: self-serve, with a 14-day free trial; it installs through the Rook CLI.

What Is MCP Testing?

Testing an MCP server means validating that it correctly implements the Model Context Protocol and returns dependable results to the AI agents that call it. It covers four things: the handler logic behind each tool, the protocol messages the server exchanges, the schemas it publishes, and the behavior it produces once a model is choosing tools on its own.[2]

The distinction that matters is the caller: a REST API is called by code a developer wrote, so the inputs are known in advance. An MCP server is called by a language model reading a text description of each tool, which means the input distribution is decided at runtime by a system nobody controls precisely. That difference, and where the two layers meet, is covered in MCP vs API.

That single difference splits the work into a deterministic half and a probabilistic half. The deterministic half is ordinary software testing and belongs in your existing framework. The probabilistic half asks whether the model understands the tools you exposed, and it needs a pass-rate threshold instead of a boolean assertion.

What Does an MCP Server Expose That You Have to Test?

An MCP server exposes four testable surfaces: version negotiation with its per-request metadata, discovery (its declared capabilities and the tools and resources it lists), the execution path that runs a tool, and the message transport. A suite that only exercises tool execution leaves three of the four untested.

  • Version negotiation - each request names its protocol version, and a server that does not implement it answers with UnsupportedProtocolVersionError (-32022), listing the versions it supports.[3] Servers on revision 2025-11-25 and earlier negotiate once through an initialize handshake instead. Moving a server to the stateless revision is a regression testing job of its own, covered in stateless MCP migration testing.
  • Request metadata - every request's _meta must carry io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities. A request missing either is rejected with -32602 (Invalid params), and one that needs a client capability the client did not declare gets MissingRequiredClientCapabilityError (-32021).[7]
  • Capability declaration - a server offering tools must declare the tools capability, and a mismatch between what it declares and what it implements is invisible until a client asks for the missing feature. Every server must also implement server/discover, which returns its supported versions and capabilities.[8]
  • Discovery - the tools/list method returns each tool's name, description and inputSchema, the fields a model mainly reads before it chooses a tool.
  • Execution - the tools/call method runs the tool and returns content, and a tool that declares an outputSchema must also return structuredContent that conforms to it.[2]
  • Transport - stdio runs the server as a subprocess over standard input and output, while Streamable HTTP takes each message as a POST to one MCP endpoint, with the protocol version, method and, for tools/call, resources/read and prompts/get, the target name mirrored into headers; a missing or mismatched header gets 400 and HeaderMismatch (-32020).[1]

The transport surface carries the least obvious rule in the specification. A stdio server must not write anything to stdout that is not a valid MCP message, and it may write logs to stderr instead.[4] A debug print left in a handler is therefore not a cosmetic problem: it adds a non-JSON line to the message stream and breaks the connection.

Diagram of an MCP client connected to an MCP server over stdio or Streamable HTTP, labelling the four testable surfaces: version negotiation through the protocol version in the _meta field, discovery through tools/list returning name, description and inputSchema, execution through tools/call returning content and structuredContent, and the message transport where stdout carries MCP messages only and logs go to stderr

How Do You Test an MCP Server With MCP Inspector?

Run MCP Inspector with npx and point it at the command that starts your server. MCP Inspector is the reference developer tool for testing and debugging MCP servers, and it ships three clients behind one binary: a web UI for exploration, a CLI for pipelines, and a terminal UI.[5] Pointing its CLI at a Cursor config is covered in Cursor MCP.

# Web UI: explore a local stdio server by hand
npx @modelcontextprotocol/inspector \
  node path/to/server/index.js

# CLI: list the tools and exit, for scripts and CI
npx @modelcontextprotocol/inspector --cli \
  node path/to/server/index.js --protocol-era modern \
  --method tools/list

# CLI against a deployed HTTP server
npx @modelcontextprotocol/inspector --cli \
  https://api.example.com/mcp --transport http \
  --protocol-era modern --method tools/call \
  --tool-name get_weather --tool-arg city=Boston

MCP Inspector requires Node 22.19.0 or newer and needs no installation, and the web client prints a URL carrying a per-launch API token that it opens in your browser.[5] Both CLI commands pass --protocol-era modern because the CLI otherwise connects in the legacy era, a plain initialize with no server/discover probe, which exercises only a server's legacy path.[9] A server that still serves 2025-11-25 clients needs each check once per era.

An earlier run of the plain tools/list command, with no era flag, against the reference server @modelcontextprotocol/server-everything 2026.7.4, using MCP Inspector 2.1.0 on Node 24.13.1, returned 14 tools. Each entry carries the name, description, and inputSchema a model reads before it picks anything, which is the payload a conformance assertion runs against. The first tool from that run is below, shown on its own with the other 13 omitted for length.

{
  "name": "echo",
  "title": "Echo Tool",
  "description": "Echoes back the input string",
  "inputSchema": {
    "type": "object",
    "properties": {
      "message": {
        "type": "string",
        "description": "Message to echo"
      }
    },
    "required": [
      "message"
    ],
    "$schema": "http://json-schema.org/draft-07/schema#"
  },
  "annotations": {
    "readOnlyHint": true,
    "destructiveHint": false,
    "idempotentHint": true,
    "openWorldHint": false
  },
  "execution": {
    "taskSupport": "forbidden"
  }
}

Current releases wrap the same list in a result object under --format json, which is the shape the jq filters later in this guide read.[10] The required array names message, so a call that omits it should come back as a tool execution error, and the annotations block is server-supplied metadata a client treats as untrusted rather than as a permission grant.[2]

The execution block is a revision 2025-11-25 field: that revision's tools specification defines it for task support, and the 2026-07-28 Tool definition no longer includes it, so the fields a listing shows depend on the era of the connection.

The practical limit is what the web UI can prove. Clicking a tool and seeing a result confirms the path works once, on one machine, with one set of arguments, so it is a smoke test. The --cli flag converts the same connection into an assertion a pipeline can run unattended, which is why the CLI client belongs in your build and the web client belongs on your desk.

To check the same server inside Codex, Codex MCP covers codex mcp list, OAuth sign-in and approval settings.

How to Gate CI on the MCP Inspector CLI

Pin an exact release in CI, because npx otherwise resolves the latest Inspector each time it runs. The Inspector repository's smoke-testing guide adds that a range such as @2 is not a pin and that --yes stops the first-run install prompt from hanging the job; the latest release on npm is 2.11.0, published 14 October 2026. Each command below fails the job with its own exit code:

# Exit 6 on an error-severity tool-schema finding
npx --yes @modelcontextprotocol/inspector@2.11.0 --cli \
  node build/index.js --protocol-era modern \
  --method tools/list --strict > /dev/null

# Assert on tool names in the JSON output
npx --yes @modelcontextprotocol/inspector@2.11.0 --cli \
  node build/index.js --protocol-era modern \
  --method tools/list --format json \
  | jq -e '[.result.tools[].name] | index("get_weather")'

# Exit 5 if a known-good call returns isError: true
npx --yes @modelcontextprotocol/inspector@2.11.0 --cli \
  node build/index.js --protocol-era modern \
  --method tools/call --tool-name get_weather \
  --tool-arg city=Boston > /dev/null

# HTTP in CI: exit 3 at once instead of a browser login
npx --yes @modelcontextprotocol/inspector@2.11.0 --cli \
  --transport http --server-url "$MCP_URL" \
  --protocol-era modern --stored-auth-only \
  --method tools/list --format json > /dev/null

On any non-zero exit the CLI also writes one JSON line to stderr with a stable code such as auth_required, unreachable, tool_is_error or schema_unportable, so a job can report why it failed; exit 4 means the server was unreachable. Under --format json, the --strict findings are folded into the output as schemaFindings.[10] The MCP Inspector learning hub guide covers launching the web UI, its panes and the basic CLI calls.

What Are the Four Layers of MCP Testing?

The four layers are unit tests for handler logic, protocol tests for the JSON-RPC exchange, conformance tests for the declared schemas, and evaluation tests for model tool selection. Each layer catches a class of defect the layer below it cannot see, and only the fourth needs a model.

LayerWhat it assertsWhat it catches
UnitThe function behind a tool returns the right value with dependencies mocked.Bad branch logic, unhandled nulls, wrong upstream error mapping.
ProtocolA real client completes server/discover, tools/list, and tools/call.Capability mismatches, registration gaps, transport faults.
ConformanceEach inputSchema is valid, structured results match the declared outputSchema, and errors follow the 2026-07-28 rules.Schema drift, breaking field renames, undeclared response shapes.
EvaluationA model picks the correct tool and completes the task across repeated runs.Ambiguous descriptions, overlapping tools, missing parameter hints.
Stacked diagram of the four layers of MCP testing, from unit tests on the handler function with dependencies mocked, up through protocol tests over server/discover, tools/list and tools/call, then conformance tests checking that each inputSchema is valid and that structured results match the declared outputSchema, to evaluation tests measuring model tool selection across repeated runs, with the first three layers marked deterministic and needing no model and the fourth marked probabilistic and gated on a pass rate

Keep the layers separate rather than merging them into one end-to-end suite. A merged suite reports a single red result that could mean a broken handler, a renamed field, or a model that picked the wrong tool, and the triage cost lands on every failure.

How Do You Run MCP Protocol Tests Without Spawning a Subprocess?

Spawning the server as a subprocess for every test makes the protocol layer slow and adds a process boundary that breaks for reasons outside the code under test. The official MCP Python SDK avoids both: its Client takes the server object directly, with no subprocess and no port, and the connection is era-neutral, probing the server and picking the protocol path.[11] This example targets the v2 SDK (mcp 2.3.0 on PyPI, released 2 October 2026) and a tool that raises ToolError for an unknown SKU:

# test_inventory.py (MCP Python SDK v2, pytest)
import pytest
from mcp import Client
from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError

# Defined inline here; import your own server instead
mcp = MCPServer("Inventory")
STOCK = {"SKU-1": 12}

@mcp.tool()
def stock_level(sku: str) -> int:
    """Return units in stock for a SKU."""
    if sku not in STOCK:
        raise ToolError(f"Unknown SKU {sku}")
    return STOCK[sku]

@pytest.fixture
def anyio_backend():
    return "asyncio"

@pytest.fixture
async def client():
    async with Client(mcp, raise_exceptions=True) as c:
        yield c

@pytest.mark.anyio
async def test_known_sku(client):
    result = await client.call_tool(
        "stock_level", {"sku": "SKU-1"})
    assert result.is_error is False
    assert result.structured_content == {"result": 12}

@pytest.mark.anyio
async def test_unknown_sku_is_a_tool_error(client):
    result = await client.call_tool(
        "stock_level", {"sku": "SKU-9"})
    assert result.is_error is True
    assert "Unknown SKU" in result.content[0].text

Both tests behave the same with or without raise_exceptions: an exception raised inside a tool comes back as a result with is_error set to True rather than as a protocol failure, and a ToolError's message is what the model reads. The flag matters for failures outside a tool body, which the server would otherwise sanitize into a generic error, so the SDK's testing guide says to leave it on in tests. For legacy-only behavior such as sampling or elicitation push, pin mode="legacy" and drop raise_exceptions=True there.

The TypeScript SDK splits the same work by era: InMemoryTransport.createLinkedPair() connects 2025-era instances only, and its testing guide passes handler.fetch as the fetch option of StreamableHTTPClientTransport for 2026-07-28 coverage.[12] The standalone FastMCP framework documents an in-process pytest fixture too, and says it enables a tight development loop by letting you avoid a separate tool like MCP Inspector during development.[6]

import pytest
from fastmcp import Client
from server import mcp   # your FastMCP server object

@pytest.fixture
async def mcp_client():
    async with Client(transport=mcp) as client:
        yield client

async def test_tools_are_registered(mcp_client):
    tools = await mcp_client.list_tools()
    names = {t.name for t in tools}
    assert names == {"get_weather", "search_files"}

MCP Conformance Checks to Automate

These checks turn the 2026-07-28 rules into assertions for every commit, run through an in-process client or the Inspector CLI with --protocol-era modern. Items marked SHOULD are recommendations, so decide whether each one fails the build or only warns:

  • server/discover returns supportedVersions and capabilities, including tools when the server exposes tools; serverInfo in the result's _meta is a SHOULD.[8]
  • An unsupported version gets UnsupportedProtocolVersionError (-32022) with the supported versions in data.supported and HTTP 400 on Streamable HTTP, and a modern-only server should name those versions in any error it returns to initialize.[3][1]
  • A request missing protocolVersion or clientCapabilities in _meta gets -32602, and one needing an undeclared client capability gets -32021 with data.requiredCapabilities, both with HTTP 400.[7]
  • Every inputSchema is a JSON Schema object, never null, read as 2020-12 when it has no $schema field; a tool without parameters should use {"type": "object", "additionalProperties": false}.[2]
  • Tool names are 1 to 128 characters of ASCII letters, digits, underscore, hyphen and dot, unique within the server (SHOULD rules).[2]
  • Repeated tools/list calls return the same order while the tool set is unchanged (a SHOULD), which keeps client and prompt caches stable.[2]
  • On Streamable HTTP, a missing or mismatched MCP-Protocol-Version, Mcp-Method or Mcp-Name header gets 400 with -32020, a foreign Origin gets 403, an unknown method gets 404 with -32601, and a modern-only server answers GET or DELETE on the endpoint with 405 (a SHOULD).[1]
  • An unknown tool fails as a protocol error, while input validation and business-logic failures return results with isError set to true.[2]

The official @modelcontextprotocol/conformance framework on npm tests client and server implementations against the specification; the stateless MCP migration guide runs it at both revisions.

Assertions to Run Against Every MCP Tool

Conformance proves the server speaks the protocol. Each tool still needs behavior checks that a schema cannot express:

  • Input validation - servers must validate all tool inputs, so send a wrong type, an out-of-range value and a missing required argument, and expect each to return isError set to true.[2] The guide to agent tools explains how such an error result returns to the model so it can fix its arguments or stop.
  • Structured output - a declared outputSchema means structuredContent must conform, and the same JSON should also appear in a TextContent block for older clients.[2]
  • Error text - the specification's tool execution error example states the rule that was broken ("must be in the future"), so assert that your messages name the field to fix.[2]
  • Retries - a model can retry after a tool execution error, so call each write tool twice with the same arguments and check the stored state for a duplicate write.[2]
  • State handles - a call with an expired or unknown handle should return a tool execution error that says so, and an authenticated server should check the caller against the handle on every call.[2]
  • Scope-filtered lists - tools/list may vary by the authorization on the request but must not vary per connection, so compare what a narrow token and a broad one each see.[2]
  • Rate limits - servers must rate limit tool invocations, so send a burst above your limit and assert the excess calls are refused.[2]

How Do You Catch Tool Schema Drift?

Snapshot the tools/list response and fail the build when it changes without review. Schema drift happens when the schema a server advertises stops matching what its handler actually accepts, and it survives unit testing because the handler and the schema are tested separately.

The common shape is a parameter that the inputSchema leaves out of the required array while the handler rejects the call without it. Discovery looks correct, the model omits the field because the schema said it was optional, and the call fails at runtime with an error the model cannot act on.

The reverse case is harder to notice and more damaging. When a tool declares an outputSchema, the specification requires the server to return structured results that conform to it, and clients should validate against it.[2] A field renamed in the handler but not in the schema breaks every agent already parsing that response, and nothing in the server itself errors.

  • Required-array diff - fail the build when a parameter enters or leaves the required array, because both directions break callers already in production.
  • Round-trip validation - validate each tools/call response against the tool's own outputSchema in the test, rather than trusting that the handler and the schema agree.
  • Description diff - treat a changed tool description as a behavior change, since a reworded description can change which tool the model picks.
  • Enum narrowing - flag any removed enum value, because an agent that learned the old value will keep sending it.

A snapshot only works if two runs against an unchanged server produce the same file, so request JSON, pin the era, sort the tools by name and keep the fields an agent depends on. The CLI documentation lists no cursor option for tools/list, so fail the step when the response carries nextCursor rather than committing part of the list:[10]

npx --yes @modelcontextprotocol/inspector@2.11.0 --cli \
  node build/index.js --protocol-era modern \
  --method tools/list --format json > tools.json

# Stop if the server paginated the list
jq -e '.result.nextCursor == null' tools.json

jq -S '.result.tools | sort_by(.name)
  | map({name, description, inputSchema, outputSchema})' \
  tools.json > tools.snapshot.json
git diff --exit-code -- tools.snapshot.json

When the diff touches a tool, rerun the agent scenarios that call it before the change merges, since a changed description reaches the model the next time the client lists tools. The guide to end to end agent testing shows how to build those scenarios so each checks every tool call and the change the run leaves in the system.

How Do You Test Whether the Agent Calls the Right Tool?

Run each prompt several times and measure how often the model selects the correct tool, then gate on a pass rate rather than a single result. Tool selection is driven by the name, description, and schema in the tools/list response, so this layer tests the wording of your tool definitions more than your code.[2] Track these rates per prompt:

  • Selection rate - how often the correct tool is chosen for a prompt.
  • Completion rate - how often the task finishes successfully once the right tool is called.
  • Extra-call rate - invocations the task did not need, including calls made after the goal was met, which is where latency and token cost accumulate without any visible failure.

A single failing run tells you almost nothing here, because the same prompt can succeed on the next sample. Set the threshold before you run the suite, keep the prompt set in version control, and re-baseline whenever you change a tool description or switch models. Eval-driven development takes that further: the eval and its target are written before a tool description changes, and the change ships only when it passes.

Past tool selection, these agent-level failures show up in the calls a run made and are easy to miss in the final reply:

  • Overlapping descriptions - when two tools' descriptions both fit a request, the model's choice splits between them; say in each description when to use it over its neighbor, then rerun the prompts.
  • Wrong arguments - the right tool called with the wrong argument, such as an identifier copied from an earlier turn, passes a selection metric and still does the wrong thing.
  • Handle carry-forward - the specification's shopping-cart example works only if the model carries basket_id from create_basket into add_item, and a run that drops the handle may open a second cart.[2]
  • Recovery after isError - clients should pass tool execution errors to the model so it can correct itself; check that the next call changes the argument the error named instead of repeating the call.[2]
  • Name collisions across servers - if your server and another connected server both expose search, the host should disambiguate, for example by prefixing tool names with a server identifier rather than relying on serverInfo names, which need not be unique.[2]

Grade these runs on effects: which tools ran, with which arguments, in what order, and what changed in the system. When a run leaves no evidence for a criterion, count it as unverified, apart from passes and failures, instead of accepting the agent's summary. Scoring agents whose behavior varies between runs is covered in more depth in AI agent testing. What the agent tells a customer when a tool call times out, drifts, or disappears is a separate set of checks, covered in MCP failure modes.

How Do You Run Security Tests on an MCP Server?

Test the three places an MCP server accepts untrusted input: the transport, the tool arguments a model generates, and the text the server itself returns. The specification requires servers to validate all tool inputs, implement access controls, rate limit invocations, and sanitize outputs.[2]

Each row below pairs a test with the result the specification or its security best practices require.[13] The token, consent, scope and handle rows apply to HTTP servers that implement authorization; a stdio server should take its credentials from the environment instead.[7]

RiskTest to runExpected result
DNS rebindingSend a request with a foreign Origin header.HTTP 403 Forbidden.[1]
Network exposureCheck which interface a local HTTP server binds to.127.0.0.1, not 0.0.0.0 (a SHOULD);[1] stdio limits access to the client that launched the server.[13]
Token passthroughCall a tool with a valid token issued for a different resource.Rejected: a server must not accept a token that was not issued for it.[13]
Confused deputyOn a proxy with a static client ID, authorize a newly registered client while an earlier consent cookie exists.The proxy shows its own consent page for that client first and accepts only a redirect_uri that exactly matches the registered one.[13]
Over-broad scopesRead scopes_supported and the challenge returned by the first privileged call.A minimal starting scope, a targeted step-up through a WWW-Authenticate scope challenge, and no wildcard or omnibus scopes.[13]
State handle hijackingPresent user A's handle with user B's token.Refused, because possessing a handle is never authentication; handles are random, expire and are bound to the user.[13]
Schema abuseValidate a schema that holds a network $ref and deeply nested composition keywords with the validator your server uses.The network $ref is not fetched, and validation stops at your depth, size or time bounds.[7]
Hostile argumentsCall each tool with path traversal strings, SQL fragments and shell metacharacters.Input validation rejects or neutralizes each one, since a model can be talked into generating any of them.
Injection through tool outputSeed an upstream record with an instruction aimed at the model, then call the tool that returns it.The output is sanitized and marked as untrusted content, and an agent-level run shows the agent does not act on it; OWASP lists this as indirect prompt injection under LLM01:2025.[14]
Untrusted annotationsReturn readOnlyHint set to true from a tool that writes.Your client grants no extra trust, since the specification counts annotations from an untrusted server as untrusted input.[2]
Legacy sessionsReplay a stale Mcp-Session-Id against a server on revisions 2025-03-26 through 2025-11-25.HTTP 404 from a legacy server that assigned the session, per the 2025-11-25 transport rules, while a 2026-07-28 server ignores the header.[1]

The subtler risk is the tool description itself. A model reads those strings as instructions, so a server that returns attacker-controlled text into a description or a tool result can steer the agent that called it. Testing that path is the same discipline as prompt injection testing, applied to tool output rather than to a chat box.

The server-level instructions string a client receives from initialize or server/discover is a third path for attacker text, covered in testing MCP server instructions for prompt injection. The threat model these checks defend against, including what changes once the model rather than your code decides a tool fires, is set out in MCP security. For the wider field, from training data and models to agents and their infrastructure, see what AI security covers layer by layer.

How Do You Test the Agent That Calls Your MCP Server Before Release?

Server tests end at the protocol boundary, so they cannot show what an agent you own does with your tools across a multi-step task. TestMu AI's Agent Assurance invokes that agent for real before release, on staging or in CI, and grades each acceptance criterion against what the run changed. It is self-serve, with a 14-day free trial, and installs through the Rook CLI:

npm install -g @testmuai/rook@latest
rook --version

Installing through npm needs Node.js 20 or newer. Platform coverage spans macOS and Linux, and 64-bit Windows through npm or WSL. Without Node on macOS or Linux, use Homebrew or the shell installer from the Rook CLI installation guide.

  • Scenarios - derived from the agent's own code or its spec, functional and adversarial by default, with adversarial categories such as jailbreak, prompt injection, hijacking, data exfiltration and PII leakage mapped to MITRE ATLAS, the OWASP Top 10 for LLM Applications (2025) and the OWASP Top 10 for Agentic Applications (2026). A small agent may get no attacks unless you pass --class adversarial.
  • Discovery - it connects to each stdio MCP server you approve and records the tools that server reports instead of trusting the config file; the current release stores remote HTTP definitions without connecting to them.
  • Observed evidence - criteria are checked against the tool calls your profile's hooks report, held against the agent's declared tools, plus changed files, artifacts and read-only checks through an MCP tool you approve. What the agent says it did is never accepted as proof.
  • Verdicts - every criterion receives one of Pass, Fail or Unable to Verify; an unverifiable criterion keeps a separate count and is never reported as a pass.
  • Reruns - each rerun is diffed against the run before it, listing the pass-rate delta, the scenarios that newly fail and the scenarios now fixed.

The Agent Assurance docs publish GitHub Actions, Jenkins and Argo CD recipes that run a reviewed suite from a reviewed branch. A rook run that finishes exits 0 whatever its verdicts, so gate the job on the report totals in the run's JSON output, using the gate script from run Agent Assurance in CI/CD. The agent's writes are real, so point every run at staging; the steps to configure MCP servers in Agent Assurance cover approving repository-declared servers and adding read-only verifiers.

The guide on how to gate GitHub Actions on Rook CLI verdicts covers wiring the report-totals check into a workflow and publishing the verdicts to the job summary.

Start a 14-day free trial of Agent Assurance by TestMu AI, with 4,000 credits, to test how your AI agents behave across workflows, tools, and actions

What Breaks MCP Servers That Pass Their Unit Tests?

A handler-only unit suite misses the failures that occur at the server's boundary: version negotiation, the transport, the schema contract, and the timeout path. That is why a server that passes every unit test can still break on its first connection.

  • stdout pollution - a logging line written to stdout instead of stderr corrupts the JSON-RPC stream and the client drops the connection with no useful error.
  • Silent tool errors - a handler that catches an exception and returns a text message without setting isError reports success, so the model treats the failure as a valid answer.
  • Version rejection - a server that does not implement the version a request declares must answer with an UnsupportedProtocolVersionError listing what it does support, and one that answers anything else leaves the client with no recovery path.[3]
  • Missing timeouts - the specification tells clients to implement timeouts for tool calls, and a tool that waits on a slow upstream without one holds the request open indefinitely.[2]
  • Environment gaps - a server that reads credentials from the shell works locally and fails under a client that launches it as a subprocess with a different environment.

The environment gap deserves a dedicated test because it is invisible in development. Under stdio the client spawns the server as a child process, so the server inherits the client's environment rather than the shell where it was built. A regression test that launches the server with a deliberately empty environment shows every implicit dependency at once.

How Do You Run MCP Tests in CI/CD?

Run the three deterministic layers on every commit and the evaluation layer on a schedule or on tool changes. Unit, protocol, and conformance tests are fast and repeatable, so they gate merges. Evaluation runs cost model tokens and vary between runs, which makes them a poor merge gate.

  • Run unit and in-process protocol tests in your existing test job, since neither needs a network or a model.
  • Add an Inspector CLI step, pinned to an exact version, that runs tools/list with --strict and one known-good tools/call against the built server, so a schema finding (exit 6) or a failed call (exit 5) stops the job.[10]
  • Diff the tools/list output against the committed snapshot and fail the job when a schema changed without review.
  • Pin the versions of the server, the SDK, and the Inspector so an upstream release cannot change the result silently.
  • Run the evaluation suite nightly and on any commit that edits a tool name, description, or schema, against staging, since agent-level runs make real writes.
Flowchart of MCP tests in a CI/CD pipeline: on every commit the job runs unit and in-process protocol tests, an Inspector CLI tools/list step that fails on a non-zero exit code, a diff of tools/list against the committed snapshot, and pinned versions of the server, SDK and Inspector, and any failure blocks the merge; on a nightly schedule or a commit editing a tool name, description or schema, the evaluation suite runs and is gated on a pre-set pass rate

Keep credentials out of the deterministic job. Protocol and conformance tests should run against stubbed upstreams so the pipeline stays fast and a rate-limited third-party API cannot turn a code review into a red build. Teams comparing this wiring against a command-line workflow will find the trade-offs laid out in MCP vs CLI.

MCP Server Pre-Release Checklist

Run this list against a release candidate. Each item maps to a check earlier in this guide, so a failure points to the section with the fix.

  • server/discover lists your versions and declares every capability you implement.
  • Unsupported versions, missing _meta fields and undeclared capabilities get -32022, -32602 and -32021.
  • Every inputSchema is a non-null JSON Schema object, and tools/list order is stable.
  • Bad input returns isError set to true with a message naming the field to fix.
  • structuredContent conforms to each declared outputSchema, with the same JSON in a text block.
  • A stdio build writes only MCP messages to stdout and logs to stderr.
  • On HTTP, a foreign Origin gets 403, a header mismatch gets -32020, and a local server binds to 127.0.0.1.
  • Tokens issued for another resource fail, scopes start minimal, and state handles are bound to their user.
  • No network $ref is dereferenced, and schema validation is bounded.
  • The Inspector CLI gates pass at the pinned version with --protocol-era modern, and a dual-era server passes them in the legacy era as well.
  • The tools/list snapshot diff is reviewed, and agent scenarios for changed tools were rerun on staging.
  • Evaluation runs meet the tool-selection pass rate set before the run.

Conclusion

Start MCP testing with one in-process protocol test that asserts the exact tool names your server registers, then add the pinned Inspector CLI gate with --strict and the tools/list snapshot to your pipeline. Add the evaluation layer once those deterministic checks are green, because a model-in-the-loop failure is hard to triage while a schema mismatch is still live.

Where MCP connects an agent to its tools, the A2A protocol connects agents to each other, and agent-to-agent protocol testing covers the assertions that layer needs. For servers already built for QA work, see MCP servers for test automation, and for the architecture behind agent-driven workflows, see MCP and AI agents. The TestMu AI MCP server connects an AI client to TestMu AI testing tools, and its Automation MCP Tool documentation shows how to triage failed automation runs from your IDE.

Shipping agents that other agents use? Rook by TestMu AI does Agent Assurance: agents testing agents before release, with no human in the loop.

To run your first test, install Agent Assurance with npm install -g @testmuai/rook@latest command, or
for a full walkthrough of the product.

Author

...

Sai Krishna

Blogs: 23

  • Linkedin

Sai Krishna is Director of Engineering at TestMu AI, where he leads agentic AI for quality engineering, building AI agents that autonomously drive mobile and conversational test automation. His current focus is Agent Testing and Model Context Protocol (MCP) support for mobile. He is a core contributor and member of the Appium open-source project and the creator of AppiumTestDistribution and appium-device-farm. With over 14 years of experience including more than 9 years at Thoughtworks as a Principal Consultant, he holds a BSc in Electronics and speaks regularly at TestMu and Appium Conf on Appium, mobile automation, and agentic AI in testing.

Reviewer

...

Srinivasan Sekar

Reviewer

  • Linkedin

Srinivasan Sekar is Director of Engineering at TestMu AI, where he leads engineering and open-source initiatives behind the Selenium and Appium automation grid and owns TestMu AI's MCP Server. A committer to Appium and a contributor to Selenium, WebdriverIO, Taiko, and AppiumTestDistribution, he brings over 15 years of experience in quality engineering and open-source technologies. He is the author of the Apress book 'The MCP Standard: A Developer's Guide to Building Universal AI Tools with the Model Context Protocol,' a Certified Kubernetes and Cloud Native Associate, and an international conference speaker. Before TestMu AI he spent over eight years at Thoughtworks as a Principal Consultant and Quality Architect. Srinivasan holds a B.Tech in Information Technology from Anna University.

Add to Google preferred sources

Summarise with AI

Copied to Clipboard!
...

3000+ Browsers. One Platform.

See exactly how your site performs everywhere.

Try it free
...

Write Tests in Plain English with KaneAI

Create, debug, and evolve tests using natural language.

Try for free

MCP Server Testing FAQs

Did you find this page helpful?

More Related Blogs

TestMu AI forEnterprise

Get access to solutions built on Enterprise
grade security, privacy, & compliance

  • Advanced access controls
  • Advanced data retention rules
  • Advanced Local Testing
  • Premium Support options
  • Early access to beta features
  • Private Slack Channel
  • Unlimited Manual Accessibility DevTools Tests