← All guides

Build a Custom MCP Server for Claude Code: Complete Guide

Step-by-step guide to building a custom MCP server for Claude Code. Covers TypeScript setup, tool definitions, transport layers, and local testing in.

A custom MCP (Model Context Protocol) server lets Claude Code call your own tools, APIs, and data sources as naturally as built-in commands. To build one: scaffold a Node.js/TypeScript project, implement the Server class from @modelcontextprotocol/sdk, define tools with JSON Schema, and register the server in Claude Code's settings. A minimal working server takes under 60 minutes to build. This guide walks through every step with production-ready patterns.

What Is MCP and Why Build a Custom Server?

MCP is an open standard that defines how AI models communicate with external tools. Claude Code ships with built-in MCP servers (filesystem, browser, memory), but custom servers let you:

Benchmark: teams using custom MCP servers report 40–60% reduction in manual copy-paste steps per development session, based on internal surveys at early MCP adopters.

Prerequisites

Project Scaffold

mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
npx tsc --init

Update tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "strict": true
  }
}

Add to package.json:

{
  "scripts": {
    "dev": "tsx src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

Minimal MCP Server

Create src/index.ts:

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  ListToolsRequestSchema,
  CallToolRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";

const server = new Server(
  { name: "my-custom-server", version: "1.0.0" },
  { capabilities: { tools: {} } }
);

// Define available tools
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "get_weather",
      description: "Get current weather for a city",
      inputSchema: {
        type: "object",
        properties: {
          city: { type: "string", description: "City name" },
        },
        required: ["city"],
      },
    },
  ],
}));

// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  if (name === "get_weather") {
    const { city } = z.object({ city: z.string() }).parse(args);
    // Replace with your real API call
    const result = await fetchWeather(city);
    return {
      content: [{ type: "text", text: JSON.stringify(result) }],
    };
  }

  throw new Error(`Unknown tool: ${name}`);
});

async function fetchWeather(city: string) {
  // Example: call your weather API here
  return { city, temp: 22, condition: "sunny" };
}

// Start with stdio transport (for Claude Code)
const transport = new StdioServerTransport();
await server.connect(transport);

Get 30+ ready-to-use MCP server templates: Claude Power Prompts (P1, $29) — includes database connectors, REST API wrappers, and CI/CD MCP servers.


Register the Server in Claude Code

Add to ~/.claude/settings.json (or project .claude/settings.json):

{
  "mcpServers": {
    "my-custom-server": {
      "command": "node",
      "args": ["/absolute/path/to/my-mcp-server/dist/index.js"],
      "env": {
        "MY_API_KEY": "your-key-here"
      }
    }
  }
}

For development with hot reload:

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

Restart Claude Code after updating settings. Verify the server is loaded with /mcp in Claude Code's command palette.

Multi-Tool Server Pattern

Real servers expose multiple tools. Structure them cleanly:

// tools/definitions.ts
export const TOOLS = [
  {
    name: "create_jira_ticket",
    description: "Create a JIRA issue",
    inputSchema: {
      type: "object",
      properties: {
        summary: { type: "string" },
        description: { type: "string" },
        priority: { type: "string", enum: ["Low", "Medium", "High", "Critical"] },
      },
      required: ["summary"],
    },
  },
  {
    name: "search_confluence",
    description: "Search Confluence wiki pages",
    inputSchema: {
      type: "object",
      properties: {
        query: { type: "string" },
        space: { type: "string", description: "Space key (optional)" },
      },
      required: ["query"],
    },
  },
];

// tools/handlers.ts
export async function handleTool(name: string, args: unknown) {
  switch (name) {
    case "create_jira_ticket":
      return createJiraTicket(args);
    case "search_confluence":
      return searchConfluence(args);
    default:
      throw new Error(`Unknown tool: ${name}`);
  }
}

Error Handling Best Practices

MCP tool errors should be returned as structured content, not thrown exceptions (which crash the server):

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  try {
    const result = await handleTool(request.params.name, request.params.arguments);
    return { content: [{ type: "text", text: JSON.stringify(result) }] };
  } catch (error) {
    // Return error as content — don't throw
    return {
      content: [{
        type: "text",
        text: `Error: ${error instanceof Error ? error.message : String(error)}`,
      }],
      isError: true,
    };
  }
});

HTTP Transport for Remote Servers

The stdio transport works for local servers. For remote/team servers, use the HTTP SSE transport:

import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";

const app = express();
const transport = new SSEServerTransport("/mcp", app);
await server.connect(transport);
app.listen(3000, () => console.log("MCP server on :3000"));

Register in Claude Code settings:

{
  "mcpServers": {
    "remote-server": {
      "url": "http://your-server.internal:3000/mcp"
    }
  }
}

Testing Your MCP Server

Test without Claude Code using the MCP inspector:

npx @modelcontextprotocol/inspector node dist/index.js

This opens a browser UI where you can call tools directly, inspect schemas, and debug responses. Always test all tools in the inspector before registering with Claude Code.

For automated tests:

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";

const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
const client = new Client({ name: "test-client", version: "1.0.0" }, {});
await server.connect(serverTransport);
await client.connect(clientTransport);

const result = await client.callTool({ name: "get_weather", arguments: { city: "Seoul" } });
console.assert(result.content[0].text.includes("Seoul"));

Production Deployment Checklist

Before using a custom MCP server in team environments:

  1. Input validation: Use Zod schemas for all tool inputs — never trust raw args
  2. Auth: Pass API keys via environment variables, never hardcode
  3. Logging: Log tool calls and errors to a file for debugging
  4. Rate limiting: Wrap external API calls with retry logic
  5. Timeout: Add a global timeout per tool call (e.g., 30 seconds)
  6. Version pinning: Pin @modelcontextprotocol/sdk version in package.json

For more on tool patterns, see the Claude Agent SDK Guide and the broader Claude Code Complete Guide.


30+ production MCP templates: Claude Power Prompts (P1, $29) — JIRA, GitHub, Postgres, Slack, and more.


Frequently Asked Questions

What is MCP and how does it relate to Claude Code?

MCP (Model Context Protocol) is an open standard for connecting AI models to external tools and data. Claude Code has a built-in MCP client; by building a custom MCP server, you expose any function or API as a tool Claude can call during its agentic loop.

Do I need TypeScript to build an MCP server?

No. The MCP SDK is available for Python (mcp package) and TypeScript/JavaScript (@modelcontextprotocol/sdk). Python is popular for data and ML tooling; TypeScript is preferred for Node.js ecosystem integrations.

How do I pass environment variables to a custom MCP server?

In Claude Code's settings.json, add an "env" object inside the server config block. Claude Code will set those variables in the server process's environment before starting it.

Can multiple people use the same MCP server?

Yes. Deploy it as an HTTP SSE server (using SSEServerTransport) behind your internal network or VPN. Each Claude Code user points to the URL in their settings. Authentication is handled via bearer tokens in request headers.

How many tools can one MCP server expose?

There is no hard limit, but keep it practical. Claude includes all tool definitions in its context window, so 20–30 focused tools is a good upper bound. Group related tools into separate servers if the list grows large.

How do I debug a broken MCP server?

Run npx @modelcontextprotocol/inspector node dist/index.js to open the MCP inspector UI. It shows the exact JSON sent and received. Also check Claude Code's MCP logs: open the command palette → /mcp → select your server → view logs.

Is there a way to share MCP servers with my team?

Yes. Host the server as an HTTP service and share the URL, or commit the server repo and have teammates run it locally. For Claude Code, each user registers the server in their own settings.json. A shared project .claude/settings.json can automate this.

Related Guides

Tools and references