Skip to main content
Hey — this lesson walks through practical ways to test a local MCP (Model Context Protocol) server. You’ll find quick manual techniques for one-off checks, interactive UIs for exploring tools, and automated patterns for CI/CD-friendly tests that work well with STDIO MCP servers.
A presentation slide titled "Testing a Local MCP Server" with a large stylized "Demo" on a dark curved background to the right. The slide includes a small "© Copyright KodeKloud" note in the bottom-left.
Callouts
Use interactive tools for quick debugging (Postman, MCP Inspector), and automated tests for CI/CD (MCP Tester or language-specific test frameworks).
Why test locally?
  • Validate tool discovery (tools/list) and invocation (tools/call) before deploying.
  • Catch protocol/JSON-RPC issues early.
  • Automate checks to prevent regressions in CI/CD.
Summary of approaches
MethodBest forQuick example
Manual STDIO via PostmanFast interactive spot checks of STDIO MCP serversUse Postman transport type STDIO and run node /full/path/to/server.js
MCP Inspector (web UI)Interactive exploration and structured debuggingnpx @modelcontextprotocol/inspector node server.js
Automated Node tests (MCP Tester)Repeatable CI checks that spawn processes and assert resultsCreate a test script that spawns node server.js and communicates via STDIO
Shell piping (echo)Very quick one-off checks or shell scriptsSee section 4 for an echo example (echo a JSON-RPC request into node)
Important: For any table cells or inline examples containing curly braces or angle brackets we use code formatting so MDX doesn’t try to parse them.

1) Quick manual testing with Postman (STDIO)

Postman supports launching a process and communicating over STDIO. This is ideal for ad-hoc verification of JSON-RPC requests/responses. Steps:
  1. Open Postman → New → MCP request.
  2. Select transport type: STDIO.
  3. Set command to node and arguments to the full path of your server.js (e.g., /full/path/to/server.js).
  4. Postman will spawn node /full/path/to/server.js and communicate via STDIO.
  5. Use tools/list to discover tools, and tools/call to invoke them.
Example JSON-RPC request to call the add-integers tool:
{
  "method": "tools/call",
  "params": {
    "name": "add-integers",
    "arguments": {
      "a": 0,
      "b": 0
    }
  }
}
Minimal server.js example that exposes a single tool (add-integers):
#!/usr/bin/env node

import { Server } from "@modelcontextprotocol/sdk/server";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/transport";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types";

const server = new Server(
  {
    name: "basic-mcp-server",
    version: "1.0.0",
  },
  {
    capabilities: {
      tools: {},
    },
    transport: new StdioServerTransport(),
  }
);

// List tools handler
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "add-integers",
        description: "Add two integers and return the result",
        inputSchema: {
          type: "object",
          properties: {
            a: { type: "integer", description: "First integer to add" },
            b: { type: "integer", description: "Second integer to add" }
          },
          required: ["a", "b"]
        }
      }
    ]
  };
});

// Call tool handler (example)
server.setRequestHandler(CallToolRequestSchema, async ({ params }) => {
  if (params.name === "add-integers") {
    const { a, b } = params.arguments;
    return {
      content: [
        {
          type: "text",
          text: String(a + b)
        }
      ]
    };
  }
  throw new Error("Tool not found");
});

server.listen();
Once connected in Postman, call the tool with a=3 and b=4 and verify the output ("7").

2) Using MCP Inspector (interactive web UI)

MCP Inspector gives a web UI tailored to MCP servers. It’s excellent for exploring tool metadata, trying inputs, and seeing raw JSON-RPC traffic. Quick start:
npx @modelcontextprotocol/inspector node server.js
What to expect:
  • The inspector prints a local URL and a session token — open that in your browser.
  • Configure transport: STDIO, command: node, arguments: server.js.
  • You can call tools/list, tools/call, fill input fields, and view a history of JSON-RPC messages and raw stdin/stdout.
Inspector highlights:
  • Transport configuration (STDIO)
  • Tool discovery and input fields (e.g., add-integers)
  • JSON-RPC request/response history
  • Convenient manual testing similar to Postman but MCP-focused

3) Automated tests with a Node “MCP Tester”

Automated tests are essential for CI. The pattern below spawns your server, writes JSON-RPC messages to STDIN, captures STDOUT, and asserts correct behavior. Example test helper (compact):
#!/usr/bin/env node

import { spawn } from "child_process";
import { strict as assert } from "assert";

class MCPTester {
  constructor(entry = "server.js") {
    this.entry = entry;
    this.server = null;
    this._buffer = "";
  }

  spawnServer() {
    this.server = spawn("node", [this.entry], { stdio: ["pipe", "pipe", "inherit"] });

    // Capture stdout chunks and buffer them for JSON parsing
    this.server.stdout.on("data", (chunk) => {
      this._buffer += chunk.toString();
    });

    // Wait a short time to allow server startup logs; in real tests use a readiness probe
    return new Promise((resolve) => setTimeout(resolve, 200));
  }

  async sendMessage(message) {
    if (!this.server) throw new Error("Server not started");
    const payload = JSON.stringify(message) + "\n";

    // Clear buffer before sending
    this._buffer = "";

    this.server.stdin.write(payload);

    // Wait for a matching response by ID
    return new Promise((resolve, reject) => {
      const cleanup = () => clearInterval(interval);

      const wrappedResolve = (value) => {
        clearTimeout(timeout);
        cleanup();
        resolve(value);
      };
      const wrappedReject = (err) => {
        clearTimeout(timeout);
        cleanup();
        reject(err);
      };

      const timeout = setTimeout(() => wrappedReject(new Error("Response timeout")), 2000);

      const onData = () => {
        // Try to find first JSON object in buffer
        try {
          // Attempt multiple parses in case startup logs precede JSON
          const start = this._buffer.indexOf("{");
          const end = this._buffer.lastIndexOf("}");
          if (start !== -1 && end !== -1 && end > start) {
            const candidate = this._buffer.slice(start, end + 1);
            const parsed = JSON.parse(candidate);
            if (parsed && parsed.id === message.id) {
              wrappedResolve(parsed);
            }
          }
        } catch (err) {
          // ignore parse errors until buffer contains full JSON
        }
      };

      // Poll buffer periodically for responses (simple approach)
      const interval = setInterval(() => {
        try {
          onData();
        } catch (e) {
          // Ignore
        }
      }, 50);
    });
  }

  killServer() {
    if (this.server) {
      this.server.kill();
      this.server = null;
    }
  }

  async testListTools() {
    await this.spawnServer();
    try {
      const message = {
        jsonrpc: "2.0",
        id: 1,
        method: "tools/list",
        params: {}
      };
      const response = await this.sendMessage(message);

      assert(response.jsonrpc === "2.0", "Response should have jsonrpc 2.0");
      assert(response.id === 1, "Response should have matching id");
      assert(response.result, "Response should have result");
      assert(Array.isArray(response.result.tools), "Result should have tools array");
      // This test expects exactly one tool (change as appropriate)
      assert(response.result.tools.length === 1, "Should have exactly one tool");

      const tool = response.result.tools[0];
      assert(tool.name === "add-integers", "Tool name should be add-integers");
      assert(tool.description, "Tool should have description");
      assert(tool.inputSchema, "Tool should have inputSchema");
      assert(tool.inputSchema.properties.a, "Tool should require parameter 'a'");
      assert(tool.inputSchema.properties.b, "Tool should require parameter 'b'");
    } finally {
      this.killServer();
    }
  }

  async testAddIntegersValid() {
    await this.spawnServer();
    try {
      const message = {
        jsonrpc: "2.0",
        id: 2,
        method: "tools/call",
        params: {
          name: "add-integers",
          arguments: {
            a: 5,
            b: 3
          }
        }
      };

      const response = await this.sendMessage(message);

      assert(response.jsonrpc === "2.0", "Response should have jsonrpc 2.0");
      assert(response.id === 2, "Response should have matching id");
      assert(response.result, "Response should have result");
      assert(Array.isArray(response.result.content), "Result should have content array");
      assert(response.result.content.length === 1, "Should have one content item");
      assert(response.result.content[0].type === "text", "Content should be text type");
      assert(response.result.content[0].text === "8", "5 + 3 should equal 8");
    } finally {
      this.killServer();
    }
  }
}

// Example runner
(async () => {
  const tester = new MCPTester("server.js");
  try {
    await tester.testListTools();
    console.log("testListTools PASSED");
    await tester.testAddIntegersValid();
    console.log("testAddIntegersValid PASSED");
  } catch (err) {
    console.error("Test failed:", err);
    process.exit(1);
  }
})();
Notes on the example:
  • sendMessage uses a simple polling approach to extract JSON from a mixed stdout buffer. For production, implement a robust framing strategy (newline-delimited JSON, explicit frames, or length-prefix).
  • Add a proper readiness probe (e.g., wait for a specific log line or implement a ready JSON-RPC method) rather than fixed sleep timers.
  • Integrate these tests as part of npm test or your CI job so failures block merges.

4) Crude but fast: echoing JSON into the server (shell)

For one-off checks you can pipe a JSON-RPC request into the server process:
echo '{"jsonrpc":"2.0","method":"tools/list","id":1}' | node server.js
Example console output (startup logs followed by the JSON-RPC response):
Basic MCP Server running on stdio
{"result":{"tools":[{"name":"add-integers","description":"Add two integers and return the result","inputSchema":{"type":"object","properties":{"a":{"type":"integer","description":"First integer to add"},"b":{"type":"integer","description":"Second integer to add"}},"required":["a","b"]}}]},"jsonrpc":"2.0","id":1}
Use this approach for quick smoke tests or to write simple shell-based checks and automation.

Recommendations and next steps

  • Use Postman or MCP Inspector for interactive debugging and rapid exploration.
  • Add automated tests (like the MCP Tester example) to CI so regressions are caught early.
  • If your server is implemented in other languages (Python, Java, etc.), implement equivalent tests using that language’s test frameworks:
    • Python: pytest, unittest
    • Java: JUnit
    • Node: mocha, jest
  • For reliable automation:
    • Use a clear STDIO framing protocol (e.g., newline-delimited JSON).
    • Add explicit readiness checks before sending requests.
    • Capture and assert on both structured JSON-RPC responses and important logs.
Automated tests are essential. Manual checks are helpful for debugging, but CI/CD tests prevent regressions and give confidence when deploying changes.
If you want, I can adapt the automated test to use a line-delimited JSON framing strategy or convert the tester into a Jest/Mocha test suite ready for CI.

Watch Video

Practice Lab