How to Build a Custom MCP Server from Scratch
Learn how to create your own MCP server using the TypeScript or Python SDK. Expose custom tools and resources for AI agents to use.
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
When existing MCP servers don't cover your use case, you can build your own. This guide walks you through building a TypeScript server that exposes a math utility tool (calculate_compound_interest) and a dynamic resource (finance://metrics/live).
Prerequisites
- βΈNode.js 18+ installed.
- βΈBasic knowledge of TypeScript and Zod.
- βΈFamiliarity with the JSON-RPC spec is helpful but not required.
Step 1: Project Setup
mkdir mcp-finance-server
cd mcp-finance-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
npx tsc --initUpdate your tsconfig.json to target modern ESM:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true
}
}Step 2: Server Code (src/index.ts)
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema
} from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
const server = new Server(
{ name: 'finance-analyzer', version: '1.0.0' },
{ capabilities: { tools: {}, resources: {} } }
);
// Define tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'calculate_compound_interest',
description: 'Calculates total compound interest return over a period of years.',
inputSchema: {
type: 'object',
properties: {
principal: { type: 'number', description: 'Initial deposit amount' },
rate: { type: 'number', description: 'Annual interest rate (e.g. 0.05)' },
years: { type: 'number', description: 'Investment duration' }
},
required: ['principal', 'rate', 'years']
}
}
]
};
});
// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === 'calculate_compound_interest') {
// Validate inputs with Zod
const principal = z.number().parse(args?.principal);
const rate = z.number().parse(args?.rate);
const years = z.number().parse(args?.years);
const total = principal * Math.pow(1 + rate, years);
const interestEarned = total - principal;
return {
content: [
{
type: 'text',
text: JSON.stringify({
principal,
rate,
years,
interestEarned: Number(interestEarned.toFixed(2)),
totalValue: Number(total.toFixed(2))
}, null, 2)
}
]
};
}
throw new Error(`Unknown tool: ${name}`);
});
// Define read-only resources
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: 'finance://metrics/live',
name: 'Live Financial Metrics Summary',
description: 'Static overview of market stats.',
mimeType: 'application/json'
}
]
};
});
// Handle resource reading
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
if (request.params.uri === 'finance://metrics/live') {
return {
contents: [
{
uri: 'finance://metrics/live',
mimeType: 'application/json',
text: JSON.stringify({
sp500_day_change: '+1.2%',
vix: '14.5',
timestamp: new Date().toISOString()
})
}
]
};
}
throw new Error(`Resource not found: ${request.params.uri}`);
});
const transport = new StdioServerTransport();
await server.connect(transport);
// STDERR is safe for logging, STDOUT is reserved for JSON-RPC
console.error("Finance MCP Server running on stdio transport.");Best Practices
- βΈValidate Arguments: Always use Zod or Pydantic to parse incoming
arguments. AI agents frequently hallucinate missing required parameters or invent string types when a number is expected. - βΈDetailed Descriptions: The
descriptionfield inListToolsRequestSchemais arguably the most important part of your code. It serves as the direct system prompt to the LLM deciding whether to invoke your tool.
Troubleshooting
- βΈServer Hangs on Startup: If you are using the
StdioServerTransportand you runconsole.log("Starting server..."), you will permanently break the connection.stdoutis reserved strictly for JSON-RPC payloads. Useconsole.error()for all debugging output. - βΈ
Schema Validation Error: Ensure yourinputSchemastrictly adheres to JSON Schema Draft 7 specifications. Complex deeply-nested one-of references are poorly supported by current LLMs. Keep schemas flat when possible.
Using with OpenAI Codex
To use this MCP server with the OpenAI Codex CLI, you can add it to your configuration using the codex mcp add command:
codex mcp add --name build-custom --command "npx @modelcontextprotocol/server-build-custom"(Note: Depending on the server, you may need to append arguments or use --env flags for environment variables as described in the configuration section above)
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.
Related Guides
How to Use the Puppeteer MCP Server for Web Scraping & Automation
Automate web browsers using the Puppeteer MCP server. Navigate websites, take screenshots, extract data, and automate web interactions through AI.
Dev ToolsHow to Use the Git MCP Server for Version Control Operations
Manage local Git repositories through AI agents. Clone repos, create branches, make commits, and view diffs using the Git MCP server.
Dev ToolsHow to Set Up the GitHub MCP Server for Repository Management
Connect your AI agent to GitHub repositories. Create issues, manage pull requests, search code, and automate repository workflows through MCP.