How to Build a Custom MCP Server from Scratch
Comprehensive guide to building custom Model Context Protocol (MCP) servers using the official TypeScript and Python SDKs. Includes Zod schemas, inspector testing, and error handling.
Generate & Validate Multi-Client MCP Config
One-click export with environment variables & path locators for Claude Desktop, Cursor, Windsurf, and OpenAI Codex CLI.
Build a Custom MCP Server from Scratch
While the open-source community maintains dozens of pre-built Model Context Protocol (MCP) servers for databases, version control, and SaaS platforms, proprietary internal APIs and domain-specific workflows require building custom MCP servers.
This guide walks through building a production-grade custom MCP server from scratch. You will learn how to initialize an MCP server using the official @modelcontextprotocol/sdk, define strictly typed tools with Zod, expose cacheable read-only resources, test with the interactive MCP Inspector, and deploy across Claude Desktop, Cursor, and the OpenAI Codex CLI.
Architectural Principles of MCP Server Design
An MCP server is an isolated process that handles JSON-RPC 2.0 messages sent by an MCP client. When designing custom servers, adhere to four foundational principles:
- ▸Protocol Stream Isolation: In standard input/output (
stdio) mode,stdoutis exclusively reserved for framed JSON-RPC messages. Any application logging directed tostdoutcorrupts the protocol stream and immediately crashes the client. All logging, telemetry, and debugging must go tostderr. - ▸Schema Declarations as Prompts: The
descriptionand parameter properties in your tool definition are not just documentation—they act as the system prompt guiding the LLM's tool selection logic. Clear, unambiguous descriptions prevent hallucinated invocations. - ▸Strict Parameter Validation: LLMs occasionally pass stringified numbers or omit optional attributes. Always validate incoming parameters against a strict runtime schema validator (such as Zod in TypeScript or Pydantic in Python).
- ▸Idempotency and Error Isolation: Tools should fail gracefully by returning an error block (
isError: true) with descriptive text rather than throwing unhandled process exceptions that terminate the server process.
Part 1: TypeScript Implementation with the Official SDK
1. Project Initialization
Create a dedicated directory and configure modern Node.js ESM packaging:
mkdir mcp-custom-analytics
cd mcp-custom-analytics
npm init -yInstall the official protocol SDK and schema validation library:
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsxGenerate a tsconfig.json configured for NodeNext ESM module resolution:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}Update your package.json to enable ES modules and expose an executable binary:
{
"name": "mcp-custom-analytics",
"version": "1.0.0",
"type": "module",
"bin": {
"mcp-custom-analytics": "./dist/index.js"
},
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx src/index.ts"
}
}2. Complete Server Implementation (src/index.ts)
Create src/index.ts. This server exposes:
- ▸A Tool (
calculate_mortgage): Computes monthly loan amortization. - ▸A Resource (
analytics://system/health): Returns live CPU, memory, and runtime metrics.
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
ErrorCode,
McpError
} from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
import os from 'os';
// 1. Initialize server metadata and capability manifest
const server = new Server(
{
name: 'custom-analytics-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
resources: {},
},
}
);
// 2. Define Parameter Validation Schemas
const MortgageParamsSchema = z.object({
principal: z.number().positive({ message: "Principal must be greater than 0" }),
annualRatePct: z.number().min(0).max(100, { message: "Rate must be between 0 and 100" }),
termYears: z.number().int().positive({ message: "Term must be a positive integer" }),
});
// 3. Declare Available Tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'calculate_mortgage',
description: 'Calculate monthly mortgage payment, total interest, and full amortization cost based on principal, annual interest rate percentage, and loan term in years.',
inputSchema: {
type: 'object',
properties: {
principal: {
type: 'number',
description: 'The total loan amount in USD (e.g. 350000)',
},
annualRatePct: {
type: 'number',
description: 'Annual interest rate as a percentage (e.g. 6.5 for 6.5%)',
},
termYears: {
type: 'number',
description: 'Duration of the loan in years (e.g. 15 or 30)',
},
},
required: ['principal', 'annualRatePct', 'termYears'],
},
},
],
};
});
// 4. Handle Tool Execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === 'calculate_mortgage') {
try {
// Validate inputs using Zod
const parsed = MortgageParamsSchema.safeParse(args);
if (!parsed.success) {
return {
content: [
{
type: 'text',
text: `Invalid parameters: ${parsed.error.issues.map((i) => i.message).join(', ')}`,
},
],
isError: true,
};
}
const { principal, annualRatePct, termYears } = parsed.data;
const monthlyRate = annualRatePct / 100 / 12;
const totalPayments = termYears * 12;
let monthlyPayment: number;
if (monthlyRate === 0) {
monthlyPayment = principal / totalPayments;
} else {
monthlyPayment =
(principal * (monthlyRate * Math.pow(1 + monthlyRate, totalPayments))) /
(Math.pow(1 + monthlyRate, totalPayments) - 1);
}
const totalCost = monthlyPayment * totalPayments;
const totalInterest = totalCost - principal;
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
principal,
annualRatePct: `${annualRatePct}%`,
termYears,
monthlyPayment: Number(monthlyPayment.toFixed(2)),
totalInterest: Number(totalInterest.toFixed(2)),
totalCost: Number(totalCost.toFixed(2)),
},
null,
2
),
},
],
};
} catch (err: any) {
return {
content: [{ type: 'text', text: `Calculation error: ${err.message}` }],
isError: true,
};
}
}
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
});
// 5. Declare Available Resources
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: 'analytics://system/health',
name: 'Host System Health Metrics',
description: 'Real-time CPU, RAM, and platform telemetry of the host execution environment.',
mimeType: 'application/json',
},
],
};
});
// 6. Handle Resource Reads
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
if (request.params.uri === 'analytics://system/health') {
const memoryUsage = process.memoryUsage();
const metrics = {
platform: os.platform(),
arch: os.arch(),
totalMemoryMB: Math.round(os.totalmem() / (1024 * 1024)),
freeMemoryMB: Math.round(os.freemem() / (1024 * 1024)),
processRssMB: Math.round(memoryUsage.rss / (1024 * 1024)),
uptimeSeconds: Math.round(process.uptime()),
timestamp: new Date().toISOString(),
};
return {
contents: [
{
uri: 'analytics://system/health',
mimeType: 'application/json',
text: JSON.stringify(metrics, null, 2),
},
],
};
}
throw new McpError(ErrorCode.InvalidRequest, `Resource not found: ${request.params.uri}`);
});
// 7. Connect via Stdio Transport
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
// ALWAYS log to stderr — stdout is reserved for JSON-RPC 2.0 framing
console.error('Custom Analytics MCP Server running on stdio transport.');
}
main().catch((error) => {
console.error('Fatal server startup error:', error);
process.exit(1);
});Part 2: Interactive Testing with the MCP Inspector
Before registering your server in Claude Desktop or Cursor, test it with the official MCP Inspector tool. The inspector provides a visual browser interface to trigger initialization, list tools, and inspect JSON-RPC message payloads:
npx @modelcontextprotocol/inspector npx tsx src/index.ts- ▸Open the inspector URL in your browser (
or port shown in terminal). - ▸Click Connect to trigger the initialization handshake.
- ▸Switch to the Tools tab and verify
calculate_mortgageappears. - ▸Input test parameters (
principal: 300000,annualRatePct: 6.5,termYears: 30) and click Run Tool. - ▸Verify the returned JSON payload and check the Raw Protocol Messages tab to see exact JSON-RPC request/response frames.
Part 3: Python Alternative with FastMCP
If your team builds AI infrastructure in Python, use the mcp Python package with the high-level FastMCP interface:
pip install "mcp[cli]"Create server.py:
from mcp.server.fastmcp import FastMCP
import sys
mcp = FastMCP("Python Analytics Server")
@mcp.tool()
def calculate_growth(principal: float, rate_pct: float, years: int) -> dict:
"""Calculate compound interest growth over time."""
total = principal * ((1 + (rate_pct / 100)) ** years)
return {
"initial": principal,
"rate": f"{rate_pct}%",
"years": years,
"projected_total": round(total, 2),
"interest_earned": round(total - principal, 2)
}
if __name__ == "__main__":
# Runs on standard stdio
mcp.run(transport="stdio")Step 4: Registering in AI Clients
Claude Desktop Configuration
Add the absolute path to your compiled server in claude_desktop_config.json:
{
"mcpServers": {
"analytics": {
"command": "node",
"args": ["/absolute/path/to/mcp-custom-analytics/dist/index.js"]
}
}
}OpenAI Codex CLI Registration
Register using codex mcp add:
codex mcp add --name custom-analytics --command "node /absolute/path/to/mcp-custom-analytics/dist/index.js"Summary & Next Steps
Building custom MCP servers unlocks private databases, internal REST microservices, and specialized domain logic for AI agents. By enforcing strict parameter schemas, redirecting logs to stderr, and verifying with the MCP Inspector, you create reliable tools ready for production agent workflows.
Weekly Protocol Diffs & Verified Drops
New server guides, benchmarks, and security audits. No spam. Unsubscribe anytime.
Zero marketing spam. 1-click unsubscribe anytime.
Build your full agent toolstack in the Visual Generator
Combine How to Build a Custom with databases, search APIs, and memory graphs in a single configuration file.
Did this setup guide work with your AI host?
Help test this configuration across Claude, Cursor, and Codex CLI. Be the first to report:
How to Build a Custom MCP Server from Scratch FAQ
What is the How to Build a Custom MCP Server from Scratch?
Comprehensive guide to building custom Model Context Protocol (MCP) servers using the official TypeScript and Python SDKs. Includes Zod schemas, inspector testing, and error handling.
How do I configure How to Build a Custom MCP Server from Scratch in Claude Desktop or Cursor?
You can copy the configuration JSON from our guide or launch the interactive MCP Codex Config Generator at https://mcp-codex.com/generator to export valid configs in 1 click.
Can I use How to Build a Custom MCP Server from Scratch with the OpenAI Codex CLI?
Yes, OpenAI Codex CLI supports Model Context Protocol. You can add it directly to ~/.codex/config.toml or pass arguments to codex mcp add.
Specializing in Model Context Protocol (MCP) integrations, autonomous AI agent orchestration, and distributed developer toolchains. Researches and benchmarks production MCP client-server architectures across OpenAI Codex, Claude, and Cursor.
Related Guides
OpenCode Free AI Agent Guide
Guide to running OpenCode coding agent completely free without API keys using Zen free tier, local LLMs, and OAuth accounts.
Dev ToolsSelf-Correcting Verification Loops in Agents
Build robust self-correcting /loop verification harnesses for AI agents using Test-Driven Agent Development, dual-agent critics, and MCP test sandboxes.
Dev ToolsAutomated Doc Verification Loops with Claude CLI
Build automated documentation verification loops with Claude CLI and MCP. Validate code snippets, test tool schemas, and sync SDK docs in CI/CD. Verified.