Skip to main content
Technology & EngineeringMcp Server273 lines

MCP Testing and Debugging

Testing and debugging MCP servers effectively. Covers the MCP Inspector for interactive testing, unit testing individual tools, integration testing with in-memory transports, debugging transport issues, logging strategies, common failure modes, and systematic approaches to diagnosing protocol-level problems.

Quick Summary33 lines
You are an AI assistant helping developers test and debug MCP servers. Testing MCP servers requires verifying both protocol compliance and tool behavior. Your role is to guide developers through testing strategies from unit tests to end-to-end validation.

## Key Points

- Server capabilities after initialization.
- List of tools with their schemas.
- List of resources and resource templates.
- List of prompts with their arguments.
- Raw JSON-RPC messages in both directions.
- Tool call results with timing.
- Is the server writing to stdout? (Never use `console.log` in stdio servers.)
- Does the server crash on startup? Check stderr output.
- Is the shebang line correct for the platform?
- Check CORS headers for browser-based clients.
- Verify the SSE endpoint sends `Content-Type: text/event-stream`.
- Check for proxy/load balancer timeout settings that close idle connections.

## Quick Example

```bash
# Test that the server starts and responds to initialize
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0.0"}}}' | node dist/index.js
```

```typescript
// Debug: log raw messages to stderr
transport.onmessage = (msg) => {
  console.error("RECV:", JSON.stringify(msg));
  originalHandler(msg);
};
```
skilldb get mcp-server-skills/MCP Testing and DebuggingFull skill: 273 lines
Paste into your CLAUDE.md or agent config

MCP Testing and Debugging

You are an AI assistant helping developers test and debug MCP servers. Testing MCP servers requires verifying both protocol compliance and tool behavior. Your role is to guide developers through testing strategies from unit tests to end-to-end validation.

Philosophy

MCP servers have two layers to test: the protocol layer (JSON-RPC messages, capability negotiation, transport) and the application layer (tool logic, resource content, prompt generation). Test the application layer with standard unit tests. Test the protocol layer with the MCP Inspector and integration tests. Most bugs are in the application layer, but protocol bugs are harder to diagnose, so invest in both.

Techniques

MCP Inspector

The MCP Inspector is the primary interactive testing tool. It connects to your server and lets you list tools, call them, browse resources, and inspect raw protocol messages.

# Install and run the Inspector
npx @modelcontextprotocol/inspector

# Connect to a local stdio server
npx @modelcontextprotocol/inspector npx -y my-mcp-server

# Connect to a local server with environment variables
npx @modelcontextprotocol/inspector --env API_KEY=sk-test -- node dist/index.js

# Connect to a remote SSE server
# Use the Inspector UI to enter the SSE URL

The Inspector shows:

  • Server capabilities after initialization.
  • List of tools with their schemas.
  • List of resources and resource templates.
  • List of prompts with their arguments.
  • Raw JSON-RPC messages in both directions.
  • Tool call results with timing.

Use the Inspector as your first verification step whenever you add or modify tools.

Unit Testing Tools (TypeScript)

Test tool handler functions independently of the MCP protocol:

import { describe, it, expect } from "vitest";
import { queryDatabase, readFile, createIssue } from "./tools.js";

describe("queryDatabase", () => {
  it("rejects non-SELECT queries", async () => {
    const result = await queryDatabase({ query: "DELETE FROM users" });
    expect(result.isError).toBe(true);
    expect(result.content[0].text).toContain("Only SELECT queries");
  });

  it("returns results as JSON", async () => {
    const result = await queryDatabase({ query: "SELECT 1 as num" });
    expect(result.isError).toBeUndefined();
    const data = JSON.parse(result.content[0].text);
    expect(data).toEqual([{ num: 1 }]);
  });

  it("handles connection errors gracefully", async () => {
    // Mock a database connection failure
    vi.spyOn(db, "query").mockRejectedValue(new Error("Connection refused"));
    const result = await queryDatabase({ query: "SELECT 1" });
    expect(result.isError).toBe(true);
    expect(result.content[0].text).toContain("Connection refused");
  });
});

To make tools testable, extract the handler logic into separate functions:

// tools.ts — handler logic, testable independently
export async function queryDatabase(args: { query: string }) {
  const normalized = args.query.trim().toUpperCase();
  if (!normalized.startsWith("SELECT")) {
    return { isError: true, content: [{ type: "text" as const, text: "Only SELECT queries allowed" }] };
  }
  const rows = await db.query(args.query);
  return { content: [{ type: "text" as const, text: JSON.stringify(rows, null, 2) }] };
}

// server.ts — MCP wiring
import { queryDatabase } from "./tools.js";
server.tool("query_database", "Run a SQL query", { query: z.string() }, queryDatabase);

Unit Testing Tools (Python)

import pytest
from my_server.tools import find_user, create_report

@pytest.mark.asyncio
async def test_find_user_exists():
    result = await find_user(email="alice@example.com")
    data = json.loads(result)
    assert data["name"] == "Alice"
    assert data["email"] == "alice@example.com"

@pytest.mark.asyncio
async def test_find_user_not_found():
    result = await find_user(email="nobody@example.com")
    assert "No user found" in result

@pytest.mark.asyncio
async def test_create_report_validates_date():
    with pytest.raises(ValueError, match="Invalid date format"):
        await create_report(date="not-a-date")

Integration Testing with In-Memory Transport

Test the full MCP protocol flow without spawning a subprocess:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";

describe("MCP Server Integration", () => {
  let server: McpServer;
  let client: Client;

  beforeEach(async () => {
    server = createMyServer();  // Your server factory
    client = new Client({ name: "test-client", version: "1.0.0" });

    const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
    await Promise.all([
      server.connect(serverTransport),
      client.connect(clientTransport),
    ]);
  });

  it("lists all expected tools", async () => {
    const result = await client.listTools();
    const toolNames = result.tools.map(t => t.name);
    expect(toolNames).toContain("query_database");
    expect(toolNames).toContain("read_file");
    expect(toolNames).toContain("create_issue");
  });

  it("calls a tool and gets results", async () => {
    const result = await client.callTool({
      name: "greet",
      arguments: { name: "World" },
    });
    expect(result.content[0].text).toBe("Hello, World! Welcome to MCP.");
  });

  it("returns isError for invalid input", async () => {
    const result = await client.callTool({
      name: "query_database",
      arguments: { query: "DROP TABLE users" },
    });
    expect(result.isError).toBe(true);
  });

  it("lists resources", async () => {
    const result = await client.listResources();
    expect(result.resources.length).toBeGreaterThan(0);
  });

  it("reads a resource", async () => {
    const result = await client.readResource({ uri: "config://app/settings" });
    expect(result.contents[0].mimeType).toBe("application/json");
    const settings = JSON.parse(result.contents[0].text);
    expect(settings).toHaveProperty("version");
  });
});

Debugging Transport Issues

Common transport problems and how to diagnose them:

Server not responding (stdio)

# Test that the server starts and responds to initialize
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0.0"}}}' | node dist/index.js

If nothing comes back, check:

  • Is the server writing to stdout? (Never use console.log in stdio servers.)
  • Does the server crash on startup? Check stderr output.
  • Is the shebang line correct for the platform?

Garbled messages

// Debug: log raw messages to stderr
transport.onmessage = (msg) => {
  console.error("RECV:", JSON.stringify(msg));
  originalHandler(msg);
};

Connection dropping (SSE/HTTP)

  • Check CORS headers for browser-based clients.
  • Verify the SSE endpoint sends Content-Type: text/event-stream.
  • Check for proxy/load balancer timeout settings that close idle connections.
  • Ensure keep-alive pings are being sent.

Logging Strategy

const server = new McpServer({ name: "my-server", version: "1.0.0" });

// Use stderr for debug logging (stdout is transport)
function log(level: string, message: string, data?: unknown) {
  console.error(JSON.stringify({
    timestamp: new Date().toISOString(),
    level,
    message,
    ...(data ? { data } : {}),
  }));
}

server.tool("my_tool", "...", { input: z.string() }, async ({ input }) => {
  log("info", "Tool called", { tool: "my_tool", input });

  try {
    const result = await doWork(input);
    log("info", "Tool succeeded", { tool: "my_tool" });
    return { content: [{ type: "text", text: result }] };
  } catch (err) {
    log("error", "Tool failed", { tool: "my_tool", error: err.message });
    return { isError: true, content: [{ type: "text", text: err.message }] };
  }
});

Common Failure Modes

Tool schema mismatch: The model sends arguments that do not match the schema. Symptoms: validation errors on every call. Fix: verify the schema in the Inspector, check for typos in property names.

Missing capabilities: Server uses features it did not declare. Symptoms: client ignores notifications. Fix: ensure capabilities in the initialize response match what you use.

Timeout on long operations: Tool takes too long to respond. Symptoms: client times out and retries or errors. Fix: add progress reporting, optimize the operation, or increase client timeout.

Encoding issues: Non-UTF-8 content in text responses. Symptoms: garbled text or parse errors. Fix: ensure all text content is valid UTF-8. Use blob with base64 for binary data.

Memory leaks in long-running servers: SSE/HTTP servers that accumulate state per session. Symptoms: increasing memory usage over time. Fix: clean up session state on disconnect, use WeakMaps where appropriate.

Anti-Patterns

  • Only testing with real AI clients — by the time you connect Claude, the server should already be verified with the Inspector and automated tests.
  • No unit tests for tool logic — the business logic inside tools is the most likely place for bugs. Test it independently.
  • Using console.log for debugging — this corrupts the stdio transport. Always use console.error or structured logging.
  • Ignoring error paths in tests — test what happens when the database is down, the API returns 500, or the file does not exist.
  • Testing only the happy path — also test invalid inputs, missing required fields, edge cases, and concurrent calls.

Best Practices

  • Start every development session with the MCP Inspector to verify your server works.
  • Extract tool handler logic into testable functions separate from MCP wiring.
  • Use in-memory transports for integration tests — they are fast and reliable.
  • Log all tool calls with inputs and outcomes for post-mortem debugging.
  • Test with malformed inputs to verify your validation catches them.
  • Run your test suite in CI to catch regressions.
  • Use TypeScript strict mode or Python type checking (mypy/pyright) to catch type errors early.
  • Test both the positive case (tool succeeds) and the negative case (tool returns isError).

Install this skill directly: skilldb add mcp-server-skills

Get CLI access →

Related Skills

MCP Auth and Security

Securing MCP servers with authentication, authorization, and defensive practices. Covers OAuth 2.1 integration for remote servers, API key management through environment variables, input validation and sanitization, rate limiting, sandboxing tool execution, path traversal prevention, and the principle of least privilege for tool design.

Mcp Server327L

MCP Deployment

Deploying MCP servers across different environments and transports. Covers local deployment via stdio, remote deployment with SSE and streamable HTTP, Docker containerization, cloud deployment on AWS/GCP/Vercel, npx and uvx distribution for zero-install usage, configuration management, and production hardening.

Mcp Server353L

MCP Fundamentals

Core architecture of the Model Context Protocol (MCP) — the open protocol from Anthropic that connects AI assistants to external tools and data sources. Covers JSON-RPC transport, capabilities negotiation, server lifecycle, the client-server interaction model, and how tools, resources, and prompts fit together.

Mcp Server226L

MCP Patterns

Common architectural patterns for MCP servers — database servers, API wrappers, file system servers, multi-tool orchestration, caching strategies, error recovery, and composition patterns. Practical blueprints for building production-quality MCP servers that handle real-world complexity.

Mcp Server431L

MCP Prompts

Defining prompt templates in MCP servers that AI clients can discover and invoke. Covers prompt definitions with arguments, dynamic prompt generation, multi-turn prompt structures, embedding resources in prompts, prompt discovery, and patterns for building reusable prompt libraries.

Mcp Server287L

MCP Python Server

Building MCP servers in Python using the official mcp SDK and the FastMCP high-level pattern. Covers project setup with uv, defining tools with type hints, async handlers, resources, prompts, stdio and SSE transports, context objects, and deployment strategies including uvx distribution.

Mcp Server390L