How to Build a Custom MCP Server for Claude Code
13 min
read
Learn how to build a custom MCP server for Claude Code — connect your own tools, APIs, and workflows to extend Claude's capabilities.

Building a custom MCP server for Claude Code is how you give the agent access to anything the official servers do not cover: your internal APIs, proprietary data sources, legacy systems, and workflow tools with no public MCP implementation.
The TypeScript MCP SDK makes this approachable. Define the tools you want Claude Code to call, implement the handlers, and register the server. This guide walks through the full build with a working example.
Key Takeaways
- Any API becomes an MCP tool: If you can call it from Node.js, you can expose it to Claude Code as a named tool through a custom MCP server.
- TypeScript SDK is the standard path: Anthropic maintains
@modelcontextprotocol/sdk, which handles protocol communication so you write tool definitions and handlers, not transport code. - Zod schemas validate tool inputs: Each tool has a name, description, and Zod-based input schema. Claude Code uses the description to decide when to call the tool.
- The server runs as a local process: Your custom MCP server runs on the developer's machine, communicates over stdio, and makes any outbound network call your handler requires.
- Registration is one
.mcp.jsonentry: Point the entry at your compiled build output and Claude Code treats your custom tools exactly like any official MCP tool. - This guide builds an internal API server: A reusable pattern that wraps a REST API and exposes its endpoints as named tools Claude Code can call during development.
Why Would You Build a Custom MCP Server?
Build a custom MCP server when no official or community server exists for your data source, or when your workflow requires business logic that no existing tool can provide.
The official servers cover common external services: GitHub, Supabase, Postgres, Notion, Brave Search, Filesystem, and Puppeteer. They do not cover internal company APIs, proprietary data stores, or legacy systems.
- Check existing servers first: Review the official MCP servers available and the community registry before investing in a custom build.
- Understand the MCP vs Tool Use distinction: The MCP vs Tool Use comparison clarifies when to build an MCP server versus using the Claude API's Tool Use feature directly.
- Get the prerequisites first: If your team is new to MCP, the MCP setup fundamentals guide provides the context you need before starting a custom server build.
- Internal API access without going public: A custom MCP server exposes internal APIs to Claude Code without requiring those APIs to be publicly accessible.
- Business logic that cannot be composed: When your workflow requires logic specific to your organisation, a custom server is the correct abstraction layer.
The custom server path is the right choice when you have a specific internal service Claude Code needs to reach and no existing server can reach it.
What Does a Custom MCP Server Look Like Before You Build One?
A custom MCP server is a Node.js process that defines a set of named tools, handles tool call requests from Claude Code over stdio, and makes whatever outbound calls your handler code requires.
Understanding the components before writing code saves significant debugging time. The mental model is simple once the four parts are clear.
- The MCP server: A Node.js process running on the developer's machine. It declares tools and handles tool call requests from Claude Code.
- The transport layer: The TypeScript SDK handles this automatically. Claude Code spawns the server as a child process and communicates via stdio pipes.
- The tool definition: Each tool has a name (the identifier Claude Code uses to call it), a description (how Claude Code decides to use it), and an input schema.
- The tool handler: The function that runs when Claude Code calls the tool. It receives validated arguments and returns a structured text response.
This guide builds a three-tool server wrapping a hypothetical internal REST API: list_feature_flags, trigger_deployment, and get_service_logs. These three tools cover read, write, and parameterised-read patterns that appear in almost every custom server build.
How Do You Set Up the MCP Server Project?
Project setup requires Node.js 18 or later, the @modelcontextprotocol/sdk package, Zod for schema validation, and TypeScript configured for CommonJS or ESM output with a dist output directory.
This is the boilerplate that must exist before any tool code is written. Keep the structure minimal, it scales cleanly.
- Install dependencies: Run the commands below to initialise the project and install everything needed.
- Configure
tsconfig.json: Set"module": "commonjs"or"node16","outDir": "./dist", and"strict": true. - Add build scripts: Define
"build": "tsc"and"start": "node dist/index.js"inpackage.json. - Keep the source structure flat: All server code lives in
src/index.ts. Compiled output goes todist/index.js. - The
.mcp.jsonentry points todist: This is the path Claude Code uses to start the server process, so it must exist before registration.
mkdir internal-api-mcp && cd internal-api-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node ts-node
npx tsc --init
Project structure after setup:
internal-api-mcp/
├── src/
│ └── index.ts
├── dist/
├── package.json
└── tsconfig.json
How Do You Define Tools in an MCP Server?
Tool definitions go in the ListTools handler. Each tool has a name, a description written for Claude Code to read, and an input schema that defines the parameters Claude Code must provide when calling the tool.
The description field is the most important part of any tool definition. Claude Code uses it to decide which tool to call. Write it as an instruction, not documentation.
- Server initialisation first: Create the server instance with its name, version, and
toolscapability before defining any handlers. - ListTools handler declares all tools: Return the full array of tool objects from this handler. Claude Code reads this list at session start to know what tools exist.
- Description quality determines selection accuracy: A precise, action-oriented description ("Use this when the user wants to deploy code") produces more reliable tool selection than a vague one.
- Input schema drives argument construction: Tools with underspecified schemas produce ambiguous tool calls. Define every parameter with a type and description. See Claude Code agentic workflows for how tool precision affects session reliability.
- Required fields control validation: List every parameter the handler cannot function without in the
requiredarray. Optional parameters should have defaults documented in their description.
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
const server = new Server(
{ name: "internal-api-server", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "list_feature_flags",
description: "Returns all current feature flags and their enabled/disabled status. Use this when the user asks about which features are active.",
inputSchema: { type: "object", properties: {}, required: [] }
},
{
name: "trigger_deployment",
description: "Triggers a deployment for a specified service and environment. Use this when the user wants to deploy code to staging or production.",
inputSchema: {
type: "object",
properties: {
service: { type: "string", description: "The service name to deploy" },
environment: { type: "string", enum: ["staging", "production"], description: "Target environment" }
},
required: ["service", "environment"]
}
},
{
name: "get_service_logs",
description: "Retrieves recent logs for a specified service. Use this when debugging issues or checking service health.",
inputSchema: {
type: "object",
properties: {
service: { type: "string", description: "The service name" },
lines: { type: "number", description: "Number of log lines to retrieve (default: 50)" }
},
required: ["service"]
}
}
]
};
});
How Do You Implement Tool Handlers?
The CallTool handler routes incoming tool calls to individual handler functions by tool name. Each handler validates its inputs with Zod, calls the relevant API or service, and returns a structured text response.
Return errors in the content field rather than throwing unhandled exceptions. Claude Code recovers from returned error text more gracefully than from a server crash.
- Switch on tool name: The
CallToolRequestSchemahandler receives the tool name and arguments. Route each name to its dedicated handler function. - Validate inputs with Zod: Parse the
argsparameter with a Zod schema in every handler that accepts parameters. This catches type errors before your API call. - Use environment variables for credentials: Pass
Authorizationheaders usingprocess.env.INTERNAL_API_TOKEN, never a hardcoded value in the handler source. - Return format is always content array: Every handler must return
{ content: [{ type: "text", text: string }] }. UseJSON.stringifyfor structured data. - Wrap all handler logic in try/catch: Return the error message in the content field. An unhandled exception crashes the server process and breaks all subsequent tool calls.
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case "list_feature_flags":
return await handleListFeatureFlags();
case "trigger_deployment":
return await handleTriggerDeployment(args);
case "get_service_logs":
return await handleGetServiceLogs(args);
default:
throw new Error(`Unknown tool: ${name}`);
}
});
async function handleListFeatureFlags() {
const response = await fetch("https://internal-api.company.com/features", {
headers: { Authorization: `Bearer ${process.env.INTERNAL_API_TOKEN}` }
});
const data = await response.json();
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
}
const DeploymentSchema = z.object({
service: z.string(),
environment: z.enum(["staging", "production"])
});
async function handleTriggerDeployment(args: unknown) {
const { service, environment } = DeploymentSchema.parse(args);
const response = await fetch("https://internal-api.company.com/deployments", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.INTERNAL_API_TOKEN}`
},
body: JSON.stringify({ service, environment })
});
const data = await response.json();
return { content: [{ type: "text", text: `Deployment triggered: ${JSON.stringify(data)}` }] };
}
How Do You Register the Custom Server With Claude Code?
Registration requires building the TypeScript to dist/index.js, adding a single entry to .mcp.json with the absolute path to the compiled output, and setting the required environment variable.
Use an absolute path in the .mcp.json entry. Relative paths resolve inconsistently depending on how Claude Code starts the server process.
- Build first, then register: Run
npm run buildand confirmdist/index.jsexists before editing.mcp.json. - Use absolute path in args: Point the
argsarray to the full filesystem path ofdist/index.js, not a relative path. - Pass credentials as env references: Use the
"INTERNAL_API_TOKEN": "${INTERNAL_API_TOKEN}"pattern in theenvfield. Never hardcode the value. - Verify with a test prompt: Start a new Claude Code session and ask "List all feature flags." Claude Code should call
list_feature_flagsand return the response. - Debug by running the server directly: If the server does not load, run
node /path/to/dist/index.jsin your terminal. Startup errors appear there and are usually missing environment variables or import path issues.
{
"mcpServers": {
"internal-api": {
"command": "node",
"args": ["/absolute/path/to/internal-api-mcp/dist/index.js"],
"env": {
"INTERNAL_API_TOKEN": "${INTERNAL_API_TOKEN}"
}
}
}
}
If Claude Code does not call your tool, ask it: "What tools do you have available?" This lists all loaded MCP tools and confirms whether the server registered successfully.
What Are Real-World Custom MCP Server Use Cases?
Custom MCP servers are most valuable for internal tooling that your team queries repeatedly during development: feature flags, CI pipelines, internal docs, monitoring systems, and custom database tooling.
The three-tool pattern from this guide scales directly to each of these use cases. The only change is which API or data source the handlers call.
- Feature flag management: Expose LaunchDarkly, Unleash, or a custom flag service as MCP tools. Claude Code can read flag states, toggle flags, and create new flags during sessions.
- CI/CD pipeline control: Wrap Jenkins, CircleCI, or Buildkite APIs as MCP tools. Claude Code can trigger runs, check build status, and retrieve logs without leaving the terminal.
- Internal documentation search: Connect Claude Code to Confluence or a private wiki. Claude Code can search and retrieve internal docs as context during any coding session.
- Monitoring and alerting: Expose Datadog, Grafana, or PagerDuty APIs as MCP tools. Claude Code can query alert status, fetch metrics, or acknowledge incidents from the terminal.
- Custom migration tracking: For teams using custom migration frameworks, build an MCP server that reads your migration history table and applies pending migrations.
For teams running Claude Code across multiple engineers with shared internal API access, the enterprise Claude Code patterns guide covers governance and credential management for shared MCP server deployments.
Conclusion
Building a custom MCP server closes the gap between Claude Code's built-in capabilities and your team's specific internal tools.
The pattern is consistent regardless of what you are wrapping: define tools with precise descriptions, implement handlers with validated inputs, build the server, and register it. Once running, Claude Code treats your custom tools exactly like any official MCP server tool.
Start with one internal API endpoint your team queries repeatedly during development. One tool working correctly is more valuable than five tools half-implemented.
Need a Custom MCP Server Built for Your Internal Tools?
Most internal tools your team needs during development have no official MCP implementation. That gap either slows your developers down every session or gets solved once with a custom server build.
At LowCode Agency, we are a strategic product team, not a dev shop. We design and build custom MCP servers that connect Claude Code to your internal APIs, proprietary data sources, and legacy systems, so your development team has a capable agentic tool that fits your actual stack.
- Internal API integration: We wrap your internal REST APIs as fully typed MCP tools with Zod validation and error handling built in.
- Tool definition design: We write the tool descriptions and input schemas that produce reliable, accurate tool selection inside Claude Code sessions.
- Authentication setup: We configure secure credential handling using your existing secrets management infrastructure, not hardcoded values.
- Legacy system connectors: We build MCP handlers that bridge Claude Code to older internal systems that lack modern API surfaces.
- Team deployment: We structure the server for shared team use, with documentation, environment variable conventions, and a
.mcp.jsontemplate every developer can use. - Testing and validation: We run the full suite of tool call tests before handoff, including edge cases and error conditions your handlers will encounter in real sessions.
- Custom AI agent development: We go beyond MCP servers to build complete agentic workflows when your use case requires more than a single tool layer.
We have built 350+ products for clients including Coca-Cola, American Express, and Medtronic. We know the difference between an MCP server that works in a demo and one that holds up across a full development team.
If you want a custom MCP server that connects Claude Code to your internal tools, discuss your MCP build.
Last updated on
May 13, 2026
.









