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:
- Connect to internal APIs (JIRA, Confluence, Salesforce)
- Query private databases
- Wrap CLI tools as Claude-callable functions
- Build domain-specific toolkits for your team
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
- Node.js 20+ and npm/bun
- Claude Code installed and running
- Basic TypeScript knowledge
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:
- Input validation: Use Zod schemas for all tool inputs — never trust raw args
- Auth: Pass API keys via environment variables, never hardcode
- Logging: Log tool calls and errors to a file for debugging
- Rate limiting: Wrap external API calls with retry logic
- Timeout: Add a global timeout per tool call (e.g., 30 seconds)
- Version pinning: Pin
@modelcontextprotocol/sdkversion inpackage.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
- Claude Code Complete Guide — Full Claude Code reference including MCP setup
- Claude Agent SDK Guide — Agentic patterns and tool definitions
- Claude API Cost Monitoring Guide — Monitor spend from MCP tool calls