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 .mcp.json at the root of the project that will consume the server. .mcp.json is the project-scoped, shared MCP registry; .claude/settings.json holds permissions and hooks, not server definitions. 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. You can also register the server without hand-editing JSON: claude mcp add --transport stdio my-mcp-server -- node --loader tsx /path/to/my-mcp-server/src/index.ts.
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.