Overview
An MCP server exposes named tools that Claude Code (or any MCP client) can call during a session. Tools wrap shell commands, database queries, API calls, or any deterministic operation. The mcp-protocol defines the wire format; this guide focuses on the implementation path in TypeScript using the official SDK. The tool design rules live in mcp-tool-design.
Prerequisites
- Node 22 on the path.
- An Anthropic API key or Claude Code installed, to test after wiring.
- MCP Inspector installed:
npm install -g @modelcontextprotocol/inspector. - A target integration: a REST API, a database, or a local command you want to expose.
Steps
1. Choose a stack
Use the TypeScript SDK (@modelcontextprotocol/sdk) for servers that will be distributed as npm packages or run in Node. Use the Python SDK (mcp) for data tools that live inside Python environments. This guide uses TypeScript; the Python API mirrors it closely.
mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript tsx @types/nodeAdd "type": "module" to package.json and a tsconfig.json targeting ES2022.
2. Define tools with JSON Schema
Each tool has a name, a description the model reads to decide when to call it, and an inputSchema in JSON Schema format. Keep descriptions crisp; the model reads them at inference time.
// src/index.ts
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-mcp-server",
version: "0.1.0",
});
server.tool(
"get_weather",
"Return current weather for a city. Use when the user asks about weather.",
{
city: z.string().describe("City name, e.g. 'London'"),
units: z.enum(["metric", "imperial"]).default("metric"),
},
async ({ city, units }) => {
// Replace with a real API call.
const data = await fetchWeather(city, units);
return { content: [{ type: "text", text: JSON.stringify(data) }] };
}
);Zod schemas convert to JSON Schema automatically. Pass z.object({...}) or individual field schemas as the third argument. See mcp-tool-design for naming and description rules.
3. Wire the transport
MCP servers communicate over stdio (for local Claude Code use) or HTTP/SSE (for networked clients). Start with stdio; switching later requires only swapping the transport object. See mcp-transports for when to use each.
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP server running on stdio");
}
main().catch(console.error);Add a start script to package.json:
"scripts": { "start": "tsx src/index.ts" }4. Test with MCP Inspector
MCP Inspector is a GUI that connects to the server over stdio and lets you call tools manually before wiring to Claude Code.
npx @modelcontextprotocol/inspector node --loader tsx src/index.tsInspector opens in the browser. Select the get_weather tool, fill in city, and click Run. Confirm the response matches expectations.
5. Register the server in Claude Code
Add an entry to .claude/settings.json in the project that will consume the server. See run-claude-code-with-mcp for the full configuration reference.
{
"mcpServers": {
"my-mcp-server": {
"command": "node",
"args": ["--loader", "tsx", "/path/to/my-mcp-server/src/index.ts"]
}
}
}For a published npm package, replace the node command with npx -y my-mcp-server.
Verify it worked
# Inside a claude session:
/mcp
# Expected: my-mcp-server: connected (N tools available)
# Confirm the tool is callable:
# Ask: "What is the weather in Berlin?"
# Claude calls get_weather; the result appears in the response.Validate the JSON Schema the SDK generates by running server.listTools() from a test script and checking each tool’s inputSchema.
Common errors
SyntaxError: Cannot use import statement. Add"type": "module"topackage.jsonor compile withtscbefore running.- Inspector shows no tools. The server connected but exited before registration completed. Wrap
server.connectin atry/catchand log errors tostderr. - Zod schema mismatch. Required fields with no default throw if the caller omits them. Add
.optional()or.default(value)as needed. - Claude Code shows “disconnected”. The server path in
settings.jsonis wrong or thecommandbinary is not on the PATH used by Claude Code. Use an absolute path for reliability. - Tool description is too long. MCP clients include descriptions in the context window. Keep each description under 100 characters; move rationale to the tool result instead.