MP-201a · Module 2
Stdio Server Implementation
4 min read
The stdio transport is the simplest way to run an MCP server. The client launches your server as a child process, sends JSON-RPC messages to its stdin, and reads responses from its stdout. No ports, no networking, no CORS — just a process reading and writing text. This makes stdio servers trivial to distribute (a single script), easy to debug (pipe in test messages), and inherently secure (the server has no network surface area beyond what it explicitly creates).
The MCP SDK handles the JSON-RPC framing, so you never parse raw messages. You create a Server instance, register your tool definitions with server.setRequestHandler for the ListToolsRequestSchema, register your tool execution logic with another handler for CallToolRequestSchema, and connect a StdioServerTransport. The SDK handles message routing, schema validation, and error formatting. Your code is pure business logic.
A critical gotcha: anything you write to stdout that is not a JSON-RPC message will corrupt the transport. All logging, debugging output, and diagnostic messages must go to stderr. Use console.error() for logging, never console.log(). This is the single most common bug in stdio MCP servers, and it manifests as mysterious "parse error" responses that seem unrelated to your code.
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
ListToolsRequestSchema,
CallToolRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
const server = new Server(
{ name: "weather-server", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
// Register tool definitions
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "get_weather",
description:
"Get current weather for a city. Returns temperature, " +
"conditions, and humidity. Use ISO country codes for " +
"disambiguation (e.g., 'Paris, FR' vs 'Paris, US').",
inputSchema: {
type: "object",
properties: {
city: {
type: "string",
description: "City name, optionally with country code",
},
units: {
type: "string",
enum: ["celsius", "fahrenheit"],
default: "celsius",
},
},
required: ["city"],
},
},
],
}));
// Register tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "get_weather") {
const { city, units = "celsius" } = request.params.arguments ?? {};
// stderr for logging — stdout is reserved for JSON-RPC
console.error(`[weather] Looking up: ${city} (${units})`);
const data = await fetchWeather(city, units);
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
throw new Error(`Unknown tool: ${request.params.name}`);
});
// Connect stdio transport and start
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("[weather] Server running on stdio");
- Scaffold the project Run `npm init -y && npm install @modelcontextprotocol/sdk`. Add `"type": "module"` to package.json. Create src/index.ts with the Server + StdioServerTransport boilerplate.
- Register tools Add ListToolsRequestSchema handler returning your tool definitions and CallToolRequestSchema handler with your execution logic. Route by request.params.name.
- Test manually Run `echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | node dist/index.js` to verify the server responds with your tool list. Check stderr for your log messages.