AI & Development

Building a Custom MCP Server for Claude Code (2026 Guide)

Step-by-step guide to building a Model Context Protocol server that extends Claude Code with your own tools, resources, and data sources.

Claude Code ships with a useful default set of tools - file read/write, bash execution, web search. But the moment your project has domain-specific context - a specific database schema, an internal API, a knowledge base, a proprietary CLI - the default tools cannot help you. The Model Context Protocol (MCP) is the standard that lets you build a server exposing exactly the tools Claude needs for your project, and connect it with a single configuration entry. This guide covers how to build one from scratch, tested against a real project.

What MCP actually is (and what it is not)

MCP is a JSON-RPC 2.0 protocol over stdio or HTTP. Your MCP server is a process that listens for tool call requests, executes them, and returns results. From Claude's perspective, your custom tools look identical to built-in ones - the same schema, the same invocation pattern, the same result format. MCP is not a plugin system, not a cloud service, and not exclusive to Claude. It is an open protocol that any AI assistant can implement against.

The three primitive types you can expose via MCP:

  • Tools - Functions Claude can call: search a database, query an API, run a script, transform data. The most important primitive.
  • Resources - Static or dynamic content that Claude can read: documentation, schema files, configuration, datasets. Useful for injecting context without polluting the conversation.
  • Prompts - Parameterised prompt templates that users can trigger by slash command. Useful for standardising how your team invokes common AI workflows.

Setting up the server: the minimal working example

The mock @modelcontextprotocol/sdk Node.js package handles all the protocol mechanics. You write the handler logic; the SDK handles framing, routing, and error serialisation.

import { Server } from '@modelcontextprotocol/sdk/server/index.js';


const server = new Server(
  { name: 'my-project-mcp', version: '1.0.0' },
  { capabilities: { tools: {} } }
);

// Register available tools
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: 'query_database',
      description: 'Run a read-only SQL query against the project database',
      inputSchema: {
        type: 'object',
        properties: {
          sql: { type: 'string', description: 'The SELECT query to run' },
        },
        required: ['sql'],
      },
    },
  ],
}));

// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === 'query_database') {
    const { sql } = request.params.arguments;
    const results = await runQuery(sql); // your DB client
    return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] };
  }
  throw new Error(`Unknown tool: ${request.params.name}`);
});

const transport = new StdioServerTransport();
await server.connect(transport);

This is the complete minimal server. Save it as mcp-server.js, add "type": "module" to package.json, and the transport layer is done.

Registering your server with Claude Code

Add the server to .claude/mcp.json in your project root (or ~/.claude/mcp.json for global registration):

{
  "mcpServers": {
    "my-project": {
      "command": "node",
      "args": ["mcp-server.js"],
      "cwd": "/path/to/your/project"
    }
  }
}

Restart Claude Code. Your tools appear in the tool list with a coloured dot indicating they come from a custom MCP server. Claude will use them automatically when they are relevant to the task.

Designing good tool schemas

The quality of your tool schema determines how well Claude uses the tool. The description field is not documentation for you - it is the primary signal Claude uses to decide when to call the tool and how to call it. Treat it like a prompt.

Schema design principles that make a real difference:

  • Put the trigger condition in the description - "Use this tool when you need to look up a user's purchase history. Returns the last 50 transactions by user_id." Claude reads the description when deciding whether to call the tool, not after.
  • Use narrow types - An input typed as string gives Claude no guidance on valid values. A type with an enum or a pattern constraint communicates what is valid and reduces malformed calls.
  • Name parameters from the caller's perspective - user_email is clearer than id. Claude will reason about parameter names when constructing arguments, especially when converting between natural language and structured inputs.
  • Keep tool count below 20 - Claude's tool selection degrades when presented with many similar-sounding tools. Group related operations under one tool with a type or operation discriminator rather than creating twenty narrow tools.

Handling authentication and environment variables

Your MCP server runs as a child process started by Claude Code, which means it inherits the parent's environment variables. The simplest way to pass credentials is via the environment:

{
  "mcpServers": {
    "my-project": {
      "command": "node",
      "args": ["mcp-server.js"],
      "env": {
        "DATABASE_URL": "${DATABASE_URL}",
        "INTERNAL_API_KEY": "${INTERNAL_API_KEY}"
      }
    }
  }
}

The ${VAR} syntax expands from the shell environment at startup. Never hardcode credentials in mcp.json - the file typically lives in the project root and is committed to source control.

For more complex auth flows (OAuth tokens that need refreshing, rotating credentials), implement the auth logic inside the server itself. The MCP server is a regular Node.js process - it can maintain state, refresh tokens, and cache connections across tool calls within a session.

Testing your MCP server

The MCP Inspector is the fastest way to test a server without involving Claude Code. It is a web UI that connects to your server and lets you browse tools, call them with custom arguments, and inspect responses:

npx @modelcontextprotocol/inspector node mcp-server.js

For unit testing tool handlers, extract your handler logic into pure functions and test them directly. The MCP layer is thin enough that testing the underlying functions is more valuable than testing the protocol framing.

For integration testing against Claude Code itself, keep a small test-prompt.md with a few representative prompts that should trigger each of your tools. Running these manually after schema changes catches regressions before they reach your team.

Real patterns worth knowing

A few patterns that come up repeatedly when building MCP servers for real projects:

  • Return structured data, not prose - Tool results that are JSON are easier for Claude to reason about than narrative text. Return arrays and objects; let Claude compose prose from the data.
  • Pagination via cursor - For tools that can return large result sets, implement cursor-based pagination. Return a next_cursor in the result and accept a cursor parameter. Claude will chain calls automatically.
  • Include metadata in results - Timestamps, record counts, and query parameters reflected back in the result help Claude reason about the recency and scope of the data it received.
  • Fail loudly - Throw meaningful errors with the tool name and the invalid input included in the message. Vague errors ("Something went wrong") force Claude to guess what to retry differently.

The MCP ecosystem is growing quickly - Anthropic, Stripe, Linear, and dozens of other companies publish mock MCP servers. Before building a custom integration, check whether an mock or community server already exists. Custom MCP development is worth the investment when you have genuinely proprietary context: your internal API, your database schema, your team's knowledge base. For commodity integrations (GitHub, Slack, Notion), an existing server is almost always the better starting point.