Skip to main content
Back to Blog
Guide
2026-06-14

MCP Server Testing: The Complete Guide with MCP Inspector in 2026

Learn how to test MCP servers in 2026 with the official MCP Inspector and automated TypeScript tests using the @modelcontextprotocol/sdk client, covering tools, resources, prompts, transports, and CI.

MCP Server Testing: The Complete Guide with MCP Inspector in 2026

The Model Context Protocol (MCP) has rapidly become the standard way AI coding agents and assistants connect to external tools, data sources, and services. By 2026, thousands of MCP servers ship inside developer tools, internal platforms, and SaaS products -- each one exposing tools, resources, and prompts that an LLM can invoke autonomously. That power comes with a sharp edge: when an agent calls a tool with a malformed schema, a broken handler, or an unhandled error path, the failure surfaces as confusing model behavior rather than a clean stack trace. The only way to ship MCP servers with confidence is to test them like any other production API.

This guide is a complete, practical walkthrough of MCP server testing in 2026. You will learn why MCP servers need a dedicated testing strategy, how to do exploratory and manual testing with the official MCP Inspector (Anthropic's "Postman for MCP"), how to run the Inspector's CLI mode inside continuous integration, and how to write fully automated tests in TypeScript using the official @modelcontextprotocol/sdk client. We will cover asserting tool input and output schemas, validating error handling, testing tool side-effects, exercising every transport (stdio, SSE, and streamable HTTP), and wiring the whole suite into a CI pipeline. Every code sample is runnable and idiomatic for the current SDK. If you are building QA workflows around AI agents, this is foundational material -- and it pairs well with our MCP testing automation guide and the broader MCP for QA engineers guide.

Key Takeaways

  • MCP servers expose three primitives -- tools, resources, and prompts -- and each needs its own test coverage.
  • MCP Inspector is the official, now-stable Anthropic tool for manual and exploratory MCP testing; it is browser-based and connects over stdio, SSE, or streamable HTTP.
  • The Inspector ships a CLI mode (--cli) that is scriptable and perfect for smoke checks inside CI.
  • Automated tests use the official @modelcontextprotocol/sdk Client plus a transport such as StdioClientTransport to call listTools, callTool, listResources, and readResource.
  • Assert tool schemas, validate protocol-level error codes, verify side-effects, and test all transports separately.
  • A good MCP test suite runs headlessly in CI on every pull request, catching regressions before an agent ever sees them.

Why MCP Servers Need Dedicated Testing

An MCP server is not a normal REST API. It is a contract negotiated at runtime between your server and an LLM-driven client. The model reads your tool descriptions and JSON schemas, decides which tool to call, and constructs arguments on its own. If the schema is wrong, the description is ambiguous, or an error is returned in the wrong shape, the model misbehaves in ways that are hard to reproduce and harder to debug.

MCP servers expose three distinct primitives, and each fails differently:

  • Tools are functions the model can invoke. They have an input schema, an output, and side-effects. A tool that throws an unstructured exception, returns the wrong content type, or silently swallows an error will derail an agent.
  • Resources are read-only data the model can pull into context (files, database rows, API responses). They are addressed by URI and must return consistent content and MIME types.
  • Prompts are reusable, parameterized message templates the server offers to clients. Broken argument substitution or missing required arguments breaks prompt-driven workflows.

Beyond the primitives, the protocol itself has rules: capability negotiation during initialization, JSON-RPC error codes, pagination of large lists, and notification handling. A server can implement every tool correctly and still fail protocol validation. Testing has to cover both the business logic and the protocol envelope.

The table below maps each primitive to what can go wrong and what your tests should assert.

PrimitiveCommon failureWhat to test
ToolWrong/loose input schemaSchema shape, required fields, types
ToolUnhandled exceptionReturns structured error, not a crash
ToolIncorrect side-effectDatabase/file state after callTool
ResourceWrong MIME typemimeType and content on readResource
ResourceStale or missing URIURI listing matches readable resources
PromptMissing required argServer rejects with clear error
ProtocolBad error codeJSON-RPC error code matches spec
TransportConnection dropsReconnect / clean close behavior

Manual Testing with MCP Inspector

MCP Inspector is the official developer tool from Anthropic for inspecting and testing MCP servers. As of 2026 it is stable and widely adopted -- think of it as Postman for MCP. It is a browser-based application backed by a small Node proxy: a React UI runs in your browser, a Node process proxies protocol traffic to your server, and you get an interactive console to list and invoke every primitive your server exposes.

The Inspector connects over all three transports -- stdio (for local servers launched as a subprocess), SSE (Server-Sent Events), and streamable HTTP (the modern HTTP transport). You launch it with a single npx command and never install anything globally.

# Launch MCP Inspector against a local stdio server
npx @modelcontextprotocol/inspector node build/index.js

# Pass arguments and environment variables through to the server
npx @modelcontextprotocol/inspector -e API_KEY=test-key node build/index.js --verbose

# Inspect a Python-based MCP server
npx @modelcontextprotocol/inspector python -m my_mcp_server

When the Inspector starts it prints a local URL and opens the UI. From there you can:

  1. Connect and watch the initialization handshake (capability negotiation) succeed or fail.
  2. List tools and read each tool's name, description, and input schema exactly as the model would see them.
  3. Call a tool by filling in a generated form, then inspect the raw JSON-RPC request and response.
  4. List and read resources to confirm URIs, content, and MIME types.
  5. List prompts, supply arguments, and preview the rendered messages.
  6. Read the notifications pane to catch logging, progress, and error notifications.

Manual exploration in the Inspector is the fastest way to catch the obvious problems early: a tool whose description is confusing, a schema that allows the wrong types, or a resource that returns HTML when it should return JSON. Do this first, then automate what you discover. To connect over HTTP transports, point the Inspector at the URL instead of a command:

# Connect to a remote streamable HTTP server
npx @modelcontextprotocol/inspector --transport http --server-url http://localhost:3000/mcp

# Connect over SSE
npx @modelcontextprotocol/inspector --transport sse --server-url http://localhost:3000/sse

MCP Inspector CLI Mode for CI

The browser UI is great for exploration, but CI needs something headless and scriptable. MCP Inspector ships a CLI mode for exactly this. Adding --cli runs the Inspector as a command-line client that connects, issues a single method call, prints JSON to stdout, and exits with a meaningful status code. That makes it perfect for smoke tests in a pipeline.

# List all tools as JSON (great smoke test)
npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/list

# Call a specific tool with arguments
npx @modelcontextprotocol/inspector --cli node build/index.js \
  --method tools/call \
  --tool-name get_weather \
  --tool-arg city=London

# List resources and prompts
npx @modelcontextprotocol/inspector --cli node build/index.js --method resources/list
npx @modelcontextprotocol/inspector --cli node build/index.js --method prompts/list

Because the CLI emits structured JSON, you can pipe it into jq for quick assertions inside a shell-based CI job:

# Fail the build if the expected tool is missing
npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/list \
  | jq -e '.tools[] | select(.name == "get_weather")' > /dev/null \
  || { echo "get_weather tool is missing"; exit 1; }

The table below compares the two Inspector modes so you know when to reach for each.

AspectUI modeCLI mode (--cli)
Best forExploration, debuggingCI smoke tests, scripting
OutputInteractive React UIJSON on stdout
Human in loopYesNo
Exit codesn/aYes, scriptable
Transportsstdio / SSE / HTTPstdio / SSE / HTTP
Typical commandnpx ...inspector node build/index.jsnpx ...inspector --cli ... --method tools/list

CLI smoke tests are cheap and catch the most catastrophic regressions -- a server that fails to start, a tool that disappeared, a handshake that broke. But they are shallow. For real coverage you need automated tests that assert behavior, and that means driving the server from the official SDK client.


Writing Automated Tests with the MCP SDK Client

The most powerful way to test an MCP server is to connect to it from a real MCP client in your test process and assert on the results. The official @modelcontextprotocol/sdk package exports a Client class plus transport implementations. Pair it with any test runner -- here we use Vitest -- to build a full integration suite.

First, install the dependencies:

npm install -D vitest @modelcontextprotocol/sdk

The core pattern is to spawn your built server as a subprocess over stdio, connect a Client to it, and tear it down after the suite. A shared setup helper keeps every test clean:

import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';

export async function createTestClient() {
  const transport = new StdioClientTransport({
    command: 'node',
    args: ['build/index.js'],
    env: { ...process.env, NODE_ENV: 'test' },
  });

  const client = new Client(
    { name: 'mcp-test-client', version: '1.0.0' },
    { capabilities: {} },
  );

  await client.connect(transport);
  return { client, transport };
}

With the helper in place, a smoke test that verifies the server starts and advertises its tools is just a few lines:

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { createTestClient } from './setup';
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';

describe('MCP server: tools', () => {
  let client: Client;
  let close: () => Promise<void>;

  beforeAll(async () => {
    const ctx = await createTestClient();
    client = ctx.client;
    close = () => ctx.transport.close();
  });

  afterAll(async () => {
    await close();
  });

  it('lists the expected tools', async () => {
    const { tools } = await client.listTools();
    const names = tools.map((t) => t.name);
    expect(names).toContain('get_weather');
    expect(names).toContain('search_docs');
  });
});

This single test already protects you from a whole class of regressions. If a refactor accidentally unregisters a tool or renames it, the suite fails immediately -- long before an agent silently loses a capability in production. The same approach underpins our MCP testing automation guide, where we scale this into full suites.


Asserting Tool Schemas

A tool's input schema is the contract the model relies on. If it is too loose, the model passes garbage. If it is too strict or wrong, valid calls fail. Schema assertions catch drift before it reaches an agent. Every tool returned by listTools includes an inputSchema (JSON Schema), so you can assert on its required fields, property types, and structure.

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { createTestClient } from './setup';
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';

describe('tool schemas', () => {
  let client: Client;
  let transport: { close: () => Promise<void> };

  beforeAll(async () => {
    const ctx = await createTestClient();
    client = ctx.client;
    transport = ctx.transport;
  });
  afterAll(() => transport.close());

  it('get_weather has a correct input schema', async () => {
    const { tools } = await client.listTools();
    const tool = tools.find((t) => t.name === 'get_weather');

    expect(tool).toBeDefined();
    expect(tool?.description).toBeTruthy();

    const schema = tool?.inputSchema as {
      type: string;
      properties: Record<string, { type: string }>;
      required?: string[];
    };

    expect(schema.type).toBe('object');
    expect(schema.properties.city.type).toBe('string');
    expect(schema.required).toContain('city');
  });
});

For servers that declare structured outputs, also assert the outputSchema and verify that the structuredContent returned by a successful call validates against it. Keeping a snapshot of every tool's schema is another effective technique -- a snapshot diff makes any unintended schema change visible in code review.


Testing Error Handling and Protocol Validation

MCP defines two distinct error channels, and a robust server uses the right one in the right situation. Mixing them up is one of the most common MCP bugs.

  • Tool execution errors are returned as a normal successful result with isError: true and a human-readable message in content. The model sees the error and can recover -- for example, retrying with different arguments. Business-logic failures (a city not found, an invalid input value the tool chooses to handle) belong here.
  • Protocol errors are JSON-RPC errors thrown by the transport. Calling a tool that does not exist, sending malformed parameters, or hitting an internal server fault should surface as a thrown error with a standard JSON-RPC error code (such as -32602 for invalid params or -32601 for method not found).

Test both paths explicitly:

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { createTestClient } from './setup';
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';

describe('error handling', () => {
  let client: Client;
  let transport: { close: () => Promise<void> };

  beforeAll(async () => {
    const ctx = await createTestClient();
    client = ctx.client;
    transport = ctx.transport;
  });
  afterAll(() => transport.close());

  it('returns a tool-level error for bad business input', async () => {
    const result = await client.callTool({
      name: 'get_weather',
      arguments: { city: 'NotARealCity__xyz' },
    });

    expect(result.isError).toBe(true);
    const text = (result.content as Array<{ type: string; text: string }>)[0];
    expect(text.text.toLowerCase()).toContain('not found');
  });

  it('throws a protocol error for an unknown tool', async () => {
    await expect(
      client.callTool({ name: 'this_tool_does_not_exist', arguments: {} }),
    ).rejects.toThrow();
  });

  it('rejects malformed arguments', async () => {
    await expect(
      // city should be a string, not a number
      client.callTool({ name: 'get_weather', arguments: { city: 42 } }),
    ).rejects.toThrow();
  });
});

This split is the single most important behavior to get right. An agent that receives a thrown protocol error when it expected a recoverable tool error will abandon the task instead of retrying. Lock the contract down with tests.


Testing Tool Side-Effects and Resources

Many tools do more than compute -- they write to a database, create a file, send a request, or mutate external state. A pure return-value assertion misses whether the side-effect actually happened. Test the observable consequence, not just the response.

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { readFile } from 'node:fs/promises';
import { createTestClient } from './setup';
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';

describe('tool side-effects', () => {
  let client: Client;
  let transport: { close: () => Promise<void> };

  beforeAll(async () => {
    const ctx = await createTestClient();
    client = ctx.client;
    transport = ctx.transport;
  });
  afterAll(() => transport.close());

  it('save_note writes the note to disk', async () => {
    const result = await client.callTool({
      name: 'save_note',
      arguments: { id: 'test-1', body: 'hello from the test suite' },
    });

    expect(result.isError).toBeFalsy();

    // Assert the real side-effect, not just the response
    const written = await readFile('./data/notes/test-1.txt', 'utf-8');
    expect(written).toBe('hello from the test suite');
  });
});

Resources deserve their own coverage. List the available resources, then read each one and assert the URI, content, and MIME type:

it('exposes a config resource with the right MIME type', async () => {
  const { resources } = await client.listResources();
  const config = resources.find((r) => r.uri === 'config://app/settings');
  expect(config).toBeDefined();

  const read = await client.readResource({ uri: 'config://app/settings' });
  const item = read.contents[0] as { uri: string; mimeType: string; text: string };
  expect(item.mimeType).toBe('application/json');
  expect(() => JSON.parse(item.text)).not.toThrow();
});

For prompts, call listPrompts and getPrompt with arguments, and assert that required arguments are enforced and the rendered messages contain the substituted values.


Testing Transports

MCP servers can speak over multiple transports, and each has its own failure modes. A server that works perfectly over stdio may break over streamable HTTP because of session handling, headers, or keep-alive behavior. Test each transport your server supports.

The SDK provides a transport class per transport, so the test pattern is identical -- only the constructor changes. For an HTTP server, use StreamableHTTPClientTransport:

import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';

export async function createHttpClient(url: string) {
  const transport = new StreamableHTTPClientTransport(new URL(url));
  const client = new Client(
    { name: 'mcp-http-test', version: '1.0.0' },
    { capabilities: {} },
  );
  await client.connect(transport);
  return { client, transport };
}
import { describe, it, expect } from 'vitest';
import { createHttpClient } from './http-setup';

describe('streamable HTTP transport', () => {
  it('connects and lists tools over HTTP', async () => {
    const { client, transport } = await createHttpClient('http://localhost:3000/mcp');
    const { tools } = await client.listTools();
    expect(tools.length).toBeGreaterThan(0);
    await transport.close();
  });
});

The reference table below summarizes the three transports and when each is used.

TransportUse caseSDK transport classConnection target
stdioLocal subprocess serversStdioClientTransportcommand + args
Streamable HTTPRemote / hosted serversStreamableHTTPClientTransport/mcp URL
SSELegacy remote streamingSSEClientTransport/sse URL

Test the lifecycle too: connect, do work, close cleanly, and verify no dangling subprocess or open socket remains. For HTTP servers, assert that a fresh client gets a fresh session and that closing a session does not leak state into the next one.


Integrating MCP Tests into CI

The payoff for all of this is a suite that runs automatically on every pull request. Combine a fast CLI smoke test with the full SDK-driven integration suite. The smoke test fails the build instantly if the server cannot even start; the integration suite catches behavioral regressions.

name: MCP Server Tests

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci
      - run: npm run build

      # Fast smoke test with the Inspector CLI
      - name: Smoke test (tools/list)
        run: |
          npx @modelcontextprotocol/inspector --cli node build/index.js \
            --method tools/list | jq -e '.tools | length > 0'

      # Full automated integration suite
      - name: Run MCP integration tests
        run: npm test

A few CI best practices for MCP servers:

  • Build before you test. stdio tests spawn build/index.js, so a stale build hides real failures. Always run npm run build first.
  • Use a test environment. Pass NODE_ENV=test and point side-effecting tools at temporary directories or an ephemeral database so tests stay isolated and repeatable.
  • Clean up subprocesses. Always await transport.close() in afterAll. Leaked subprocesses make CI hang.
  • Keep the smoke test separate. A failing tools/list should fail fast and clearly, before the longer suite even runs.

If you are standing up MCP testing as part of a wider AI quality strategy, see our roundup of AI test automation tools for 2026 and the Playwright MCP browser automation guide for browser-driven agent testing.


Frequently Asked Questions

What is MCP Inspector and is it free?

MCP Inspector is the official open-source developer tool from Anthropic for inspecting and testing MCP servers. It is free, runs with a single npx @modelcontextprotocol/inspector command, and needs no global install. It provides a browser-based React UI backed by a Node proxy, plus a CLI mode for scripting, and connects over stdio, SSE, and streamable HTTP transports.

How do I test an MCP server without an AI model?

You do not need an LLM to test an MCP server. Use the official @modelcontextprotocol/sdk Client class in your test runner to connect directly to the server and call listTools, callTool, listResources, and readResource. This drives the exact same protocol an AI client uses, so you can assert on schemas, results, and errors deterministically without any model in the loop.

What is the difference between a tool error and a protocol error in MCP?

A tool error is a successful response with isError: true and a message the model can read and recover from -- use it for business-logic failures. A protocol error is a JSON-RPC error thrown by the transport, used for invalid params, unknown methods, or internal faults. Tests should assert that recoverable failures return tool errors while structural failures throw protocol errors with correct codes.

Can I run MCP tests in CI/CD?

Yes. Run a fast Inspector CLI smoke test (--cli --method tools/list) to confirm the server starts and exposes its tools, then run a full Vitest suite driven by the SDK client for behavioral coverage. Build the server first so stdio tests spawn fresh code, use a test environment with temporary state, and always close transports in teardown so CI does not hang on leaked subprocesses.

How do I test MCP resources and prompts?

For resources, call listResources to enumerate URIs, then readResource on each and assert the URI, content, and mimeType. For prompts, call listPrompts to confirm they are registered, then getPrompt with arguments and verify the rendered messages contain the substituted values. Also test that missing required prompt arguments are rejected with a clear error.

Which transports should I test for an MCP server?

Test every transport your server supports. stdio is for local subprocess servers and is tested with StdioClientTransport. Streamable HTTP is the modern remote transport, tested with StreamableHTTPClientTransport against a /mcp URL. SSE is the legacy streaming transport. Each transport has distinct session and lifecycle behavior, so a server that passes over stdio can still fail over HTTP.

How do I assert a tool's input schema is correct?

Call listTools and read the tool's inputSchema, which is a JSON Schema object. Assert that type is object, that each property has the expected type, and that required lists the mandatory fields. Snapshotting the schema is also effective -- a snapshot diff surfaces any accidental schema change during code review before it reaches an agent.

Do I need to rebuild before running stdio MCP tests?

Yes. stdio integration tests spawn your compiled server (for example node build/index.js), so they run against the last build, not your latest source. Always run your build step before the test step, both locally and in CI. Skipping the rebuild is the most common reason MCP tests pass on stale code and then fail mysteriously after deploy.


Conclusion

MCP servers are the connective tissue between AI agents and the real world, and an untested server is a liability that fails in the most confusing way possible -- as bad model behavior. A complete strategy combines exploratory testing in MCP Inspector, fast CLI smoke checks in CI, and a deep automated suite built on the official @modelcontextprotocol/sdk client. Assert your tool schemas, split tool errors from protocol errors, verify real side-effects, cover every transport, and run it all on every pull request. Do that and your server will behave predictably no matter which agent calls it.

Ready to level up your team's MCP and AI testing skills? Browse the full library of QA skills for AI coding agents on QASkills.sh and install ready-made testing skills directly into Claude Code, Cursor, and 30+ other agents. Then keep going with our MCP for QA engineers guide and MCP testing automation guide.

MCP Server Testing: The Complete Guide with MCP Inspector in 2026 | QASkills.sh