Dev ToolsΒ·
advanced
Β·15 min readΒ·Apr 4, 2026

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.

custom serverTypeScriptPythonSDKdevelopmenttoolsarchitecture
Interactive Tool
1-Click Export

Generate & Validate Multi-Client MCP Config

One-click export with environment variables & path locators for Claude Desktop, Cursor, Windsurf, and OpenAI Codex CLI.

Open in Generator

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

bash
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 --init

Update your tsconfig.json to target modern ESM:

json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true
  }
}

Step 2: Server Code (src/index.ts)

typescript
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 description field in ListToolsRequestSchema is 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 StdioServerTransport and you run console.log("Starting server..."), you will permanently break the connection. stdout is reserved strictly for JSON-RPC payloads. Use console.error() for all debugging output.
  • β–ΈSchema Validation Error: Ensure your inputSchema strictly 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:

bash
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)

Ready to Deploy?

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.

Customize in Generator

Related Guides