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/node

Add "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.ts

Inspector 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" to package.json or compile with tsc before running.
  • Inspector shows no tools. The server connected but exited before registration completed. Wrap server.connect in a try/catch and log errors to stderr.
  • 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.json is wrong or the command binary 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.