MCP server in TypeScript
Minimum-viable Model Context Protocol server with one tool, exposed over stdio for Claude Code / Desktop.
The Model Context Protocol (MCP) is the way LLM clients (Claude Desktop, Claude Code, Cursor, Cline) discover and invoke tools. Below is the smallest useful server: one tool, stdio transport, JSON Schema validation.
Install
npm i @modelcontextprotocol/sdk zod
Server
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
const server = new Server(
{ name: "weather-mcp", version: "0.1.0" },
{ capabilities: { tools: {} } }
);
const GetWeatherInput = z.object({
city: z.string().describe("City name, e.g. Bengaluru"),
});
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "get_weather",
description: "Return current weather for a city.",
inputSchema: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
},
],
}));
server.setRequestHandler(CallToolRequestSchema, async (req) => {
if (req.params.name !== "get_weather") {
throw new Error(`Unknown tool: ${req.params.name}`);
}
const { city } = GetWeatherInput.parse(req.params.arguments);
// Replace with a real API call
const tempC = 28;
return {
content: [{ type: "text", text: `${city}: ${tempC}°C, partly cloudy.` }],
};
});
await server.connect(new StdioServerTransport());
Wire it into Claude Code
{
"mcpServers": {
"weather": {
"command": "node",
"args": ["./dist/server.js"]
}
}
}
Tips from production
- Keep tool descriptions tight — descriptions are LLM-facing and a major lever for selection accuracy.
- Beyond ~10–15 tools per server, classification accuracy on small models (Haiku) drops. Split into multiple servers, or use a Proxy Aggregator.
- Always validate inputs with Zod and return structured errors via
isError: truecontent blocks — the LLM will retry productively. - Stream long-running tool output via
progressTokenupdates instead of one giant payload.
How it works
The server is built from three pieces of the official SDK. Server is the protocol object — I name it (weather-mcp) and declare a tools capability so clients know this server offers tools at all. StdioServerTransport is the wire: the process talks JSON-RPC over standard input and output, which is exactly what a desktop client expects when it spawns the server as a child process.
The two setRequestHandler calls are where the real work lives. The handler for ListToolsRequestSchema answers the "what can you do?" question — it returns one tool, get_weather, with a human-readable description and a JSON Schema for its input. The handler for CallToolRequestSchema answers the "do it" request: it checks the tool name, parses the arguments through the Zod schema GetWeatherInput, and returns a content array containing a text block. In this snippet the weather value is hard-coded; the comment marks where a real API call would go. Finally, await server.connect(...) opens the transport and the server begins listening. The .mcp.json file is the registration side — it tells Claude Code to launch the compiled server with node ./dist/server.js.
When to use it
Reach for an MCP server when you want a Claude client to take actions or pull live data rather than answer from its training. This stdio pattern is the right starting point for local, single-user tools — anything that can run as a child process on the same machine as the client. The JSON Schema in the list handler and the Zod parse in the call handler are doing two different jobs: the schema advertises the shape to the model, and the Zod parse enforces it at runtime, so keep them in sync.
A few pitfalls worth flagging. Because stdio is the transport, never write logs to stdout — that stream carries protocol messages and stray prints will corrupt them; log to stderr instead. Build the TypeScript to dist before pointing .mcp.json at it, or the spawn will fail silently. And as the tips above note, tool descriptions are model-facing text and the single biggest lever on whether the right tool gets picked, so write them as carefully as you would write a prompt.