How to Build an MCP Server for Claude Code: Step-by-Step Guide
Build an MCP server using the @modelcontextprotocol/sdk package in Node.js or the mcp Python library. Define tools with Zod schemas, implement their handlers, and connect via stdio transport. The whole skeleton is under 50 lines.
Building an MCP server lets you give Claude Code any capability you can write a function for. The protocol is simple: your server declares tools with name, description, and input schema; Claude decides when to call them; your server executes the logic and returns a result.
The Node.js SDK (@modelcontextprotocol/sdk) is the most mature option. Create a new Node project, install the SDK, define your tools using Zod for input validation, connect a StdioServerTransport, and your server is ready. The whole pattern takes about 30 minutes to get right the first time.
Tool descriptions are critical — Claude uses them to decide when to call your tool. Write the description as if explaining to a new developer: what the tool does, when to use it, what the inputs mean, and what format the output is in. Vague descriptions lead to missed calls or wrong arguments.
For production MCP servers, add proper error handling (throw McpError with appropriate codes), keep responses concise (large payloads waste context), and version your server so users can pin to a known-good release. Publish to npm if it is generally useful — the ecosystem rewards good servers with adoption.
Examples
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "my-tools",
version: "1.0.0",
});
// Define a tool
server.tool(
"get_weather",
"Fetches current weather for a city. Returns temperature in Celsius, conditions, and humidity.",
{
city: z.string().describe("City name, e.g. 'London' or 'New York'"),
units: z.enum(["celsius", "fahrenheit"]).default("celsius"),
},
async ({ city, units }) => {
const url = `https://api.open-meteo.com/v1/forecast?city=${city}`;
const res = await fetch(url);
const data = await res.json();
return {
content: [{
type: "text",
text: JSON.stringify(data, null, 2),
}],
};
}
);
// Start server on stdio
const transport = new StdioServerTransport();
await server.connect(transport);{
"name": "my-mcp-server",
"version": "1.0.0",
"type": "module",
"bin": {
"my-mcp-server": "./dist/index.js"
},
"scripts": {
"build": "tsc",
"dev": "tsx src/index.ts"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"zod": "^3.22.0"
},
"devDependencies": {
"typescript": "^5.0.0",
"tsx": "^4.0.0"
}
}# Build the server
npm run build
# Register with Claude Code
claude mcp add --scope project my-tools node /path/to/my-mcp-server/dist/index.js
# Or use npx during development
claude mcp add --scope project my-tools npx tsx /path/to/src/index.ts
# Verify it connected
claude mcp list
# Test in Claude — just ask
# "What is the weather in London?"Tips
- →Write tool descriptions in plain English explaining when Claude should use them — this is more important than the code itself.
- →Return structured JSON in your tool responses rather than prose — Claude can reason over structured data more reliably.
- →Use Zod .describe() on every input field so Claude knows exactly what each parameter expects.
- →Add a 'resources' endpoint alongside tools if you want to expose read-only data (like config files or documentation) that Claude can browse.
- →Test your server with the MCP Inspector CLI: 'npx @modelcontextprotocol/inspector node dist/index.js' gives you a REPL to call tools manually.
FAQ
Should I use TypeScript or Python for my MCP server?+
TypeScript is the better choice for most use cases — the official SDK is most mature there, async/await is clean, and packaging is simple. Use Python if your tool needs heavy data science libraries (numpy, pandas) or if your team is already in a Python codebase.
How do I handle authentication in an MCP server?+
Pass secrets as environment variables configured in settings.json's env block. Your server reads them at startup with process.env. Never accept credentials as tool arguments — they would appear in Claude's context window.
Can my MCP server maintain state between tool calls?+
Yes. Since the server is a long-running process (for stdio transport), you can keep state in module-level variables. Use this for connection pooling (database clients, HTTP clients) and session caching. Just be careful about memory leaks in long-running sessions.
What is the difference between tools and resources in MCP?+
Tools are callable functions with side effects — they take inputs and do work. Resources are read-only data endpoints that Claude can list and fetch, like documentation pages, config files, or API schemas. Use tools for actions, resources for reference data.