Building a Custom MCP Server for Claude Code (2026)

When the existing MCP servers do not cover your use case — an internal API, a proprietary database, or a custom tool — you can build your own. The MCP TypeScript SDK handles all the protocol details, leaving you to implement just the tools and resources your server exposes. This guide covers the full process: understanding the architecture, scaffolding a project, implementing tools, testing locally, and connecting to Claude Code. Use the MCP Config Generator to generate the final configuration.

MCP Server Architecture

Every MCP server follows the same pattern:

Claude Code  ←→  MCP Protocol (JSON-RPC over stdio)  ←→  Your Server  ←→  Your Data/API

Your server declares:

  • Tools — Actions Claude can invoke (e.g., “create_ticket”, “run_query”)
  • Resources — Data Claude can read (e.g., “project_config”, “user_list”)

The MCP SDK handles serialization, transport, and protocol negotiation. You write the handler functions.

Project Setup

Initialize a new TypeScript project for your MCP server:

mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
# Create tsconfig
cat > tsconfig.json << 'EOF'
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true
  },
  "include": ["src/**/*"]
}
EOF
mkdir src

Implementing a Working Server

Here is a complete MCP server that exposes a weather lookup tool. This demonstrates the core patterns you will reuse for any custom server:

// 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: "weather-server",
  version: "1.0.0",
});
// Define a tool that Claude can call
server.tool(
  "get_weather",
  "Get current weather for a city",
  {
    city: z.string().describe("City name"),
    units: z.enum(["celsius", "fahrenheit"]).default("celsius"),
  },
  async ({ city, units }) => {
    // Replace with your actual API call
    const response = await fetch(
      `https://api.weatherapi.com/v1/current.json?key=${process.env.WEATHER_API_KEY}&q=${city}`
    );
    const data = await response.json();
    const temp = units === "celsius"
      ? data.current.temp_c
      : data.current.temp_f;
    return {
      content: [
        {
          type: "text" as const,
          text: `Weather in ${city}: ${temp}° ${units}, ${data.current.condition.text}`,
        },
      ],
    };
  }
);
// Define a resource that Claude can read
server.resource(
  "supported-cities",
  "weather://cities",
  async (uri) => ({
    contents: [
      {
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify([
          "New York", "London", "Tokyo", "Sydney", "Berlin"
        ]),
      },
    ],
  })
);
// Start the server
async function main(): Promise<void> {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}
main().catch(console.error);

This server exposes one tool (get_weather) and one resource (supported-cities). Claude Code will discover both automatically when connected.

Adding Multiple Tools

Extend your server with additional tools by calling server.tool() for each one:

server.tool(
  "get_forecast",
  "Get 5-day weather forecast",
  {
    city: z.string(),
    days: z.number().min(1).max(5).default(3),
  },
  async ({ city, days }) => {
    const response = await fetch(
      `https://api.weatherapi.com/v1/forecast.json?key=${process.env.WEATHER_API_KEY}&q=${city}&days=${days}`
    );
    const data = await response.json();
    const forecast = data.forecast.forecastday.map(
      (day: { date: string; day: { avgtemp_c: number; condition: { text: string } } }) =>
        `${day.date}: ${day.day.avgtemp_c}°C, ${day.day.condition.text}`
    );
    return {
      content: [{ type: "text" as const, text: forecast.join("\n") }],
    };
  }
);

Testing Locally

Test your server before connecting it to Claude Code:

# Run the server directly (it communicates over stdio)
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0.0"}}}' | npx tsx src/index.ts
# Or use the MCP inspector tool
npx @modelcontextprotocol/inspector npx tsx src/index.ts

The MCP inspector provides a web UI where you can call tools, read resources, and see the raw JSON-RPC messages.

Connecting to Claude Code

Add your custom server to the MCP configuration:

{
  "mcpServers": {
    "weather": {
      "command": "npx",
      "args": ["tsx", "/absolute/path/to/my-mcp-server/src/index.ts"],
      "env": {
        "WEATHER_API_KEY": "your_api_key_here"
      }
    }
  }
}

Or build and run the compiled JavaScript:

npx tsc
{
  "mcpServers": {
    "weather": {
      "command": "node",
      "args": ["/absolute/path/to/my-mcp-server/dist/index.js"],
      "env": {
        "WEATHER_API_KEY": "your_api_key_here"
      }
    }
  }
}

Restart Claude Code and verify with /mcp. Your weather server should appear as connected.

Error Handling Best Practices

Return errors through the MCP protocol rather than throwing exceptions:

server.tool(
  "get_weather",
  "Get current weather",
  { city: z.string() },
  async ({ city }) => {
    try {
      const response = await fetch(`https://api.weatherapi.com/v1/current.json?key=${process.env.WEATHER_API_KEY}&q=${city}`);
      if (!response.ok) {
        return {
          content: [{ type: "text" as const, text: `Error: Could not fetch weather for "${city}" (HTTP ${response.status})` }],
          isError: true,
        };
      }
      const data = await response.json();
      return {
        content: [{ type: "text" as const, text: `${city}: ${data.current.temp_c}°C` }],
      };
    } catch (err) {
      return {
        content: [{ type: "text" as const, text: `Network error: ${(err as Error).message}` }],
        isError: true,
      };
    }
  }
);

Publishing Your Server

Package your server for others to use:

{
  "name": "@yourorg/mcp-server-weather",
  "version": "1.0.0",
  "bin": { "mcp-server-weather": "./dist/index.js" },
  "files": ["dist"],
  "scripts": {
    "build": "tsc",
    "prepublishOnly": "npm run build"
  }
}

Add a shebang to src/index.ts:

#!/usr/bin/env node

After publishing to npm, users can configure it with:

{
  "mcpServers": {
    "weather": {
      "command": "npx",
      "args": ["-y", "@yourorg/mcp-server-weather"],
      "env": { "WEATHER_API_KEY": "xxx" }
    }
  }
}

Try It Yourself

Use the MCP Config Generator to create configuration entries for your custom server. It handles the JSON structure so you can focus on building tools.

What language can I write MCP servers in? MCP servers can be written in any language. Official SDKs exist for TypeScript and Python. Community SDKs are available for Go, Rust, and Java. The server communicates over stdio using JSON-RPC, so any language that can read stdin and write stdout works.
How do I handle authentication in my MCP server? Pass credentials through environment variables in the MCP configuration. Your server reads them from process.env. Never hardcode secrets in your server code. For OAuth flows, implement token refresh logic inside your server.
Can my MCP server maintain state between calls? Yes. Your server runs as a long-lived process during the Claude Code session. You can maintain in-memory state, database connections, or caches between tool calls. The state is lost when the session ends.
What happens if my MCP server crashes? Claude Code detects the crash and marks the server as disconnected. You can restart Claude Code or use the /mcp command to reconnect. Design your server with proper error handling to prevent crashes from propagating.

Get started → Generate your project setup with our Project Starter.

Try it: Paste your error into our Error Diagnostic for an instant fix.