Skip to content
Tutorial · Part 2 of 4June 2026 · 16 min read

Build a Custom MCP Server from Scratch: TypeScript & Python SDK Tutorial

Learn how to build, test, validate, and distribute production-ready Model Context Protocol servers in TypeScript and Python to expose proprietary internal APIs and databases to Claude Desktop and Cursor.

1. Architecture & Core Primitives of a Custom MCP Server

When building AI-assisted engineering workflows, pre-packaged community servers or automated converters might not cover every proprietary edge case. You might need to query a private on-premise SOAP legacy service, calculate complex financial analytics in memory, or orchestrate multi-step transactions across internal microservices. Building a custom Model Context Protocol Server gives you complete architectural freedom.

An MCP server acts as an intelligent intermediary between an AI client host (like Claude Desktop, Cursor IDE, or Cline) and your backend infrastructure. Communication is strictly standardized over JSON-RPC 2.0 messages transmitted via standard input/output streams (stdio) or Server-Sent Events (SSE).

The Model Context Protocol establishes a bidirectional connection lifecycle where the client and server negotiate capabilities on startup. During initialization, the client sends an initialize request detailing supported protocol versions, and the server responds with its declared capabilities, tool definitions, and resource schemas.

Every custom MCP server is built on three foundational protocol primitives that define how the AI discovers, reads, and executes capabilities in your domain:

  • Tools: Executable functions that perform side effects or dynamic data retrieval based on model intent. Every tool exposes a strict JSON Schema defining parameter names, types, and descriptions.
  • Resources: Static or streaming contextual data (such as database schemas, application logs, or OpenAPI contracts) that clients can read into memory via URI templates (e.g. schema://internal-crm).
  • Prompts: Pre-packaged prompt templates with argument slots that guide the language model on how to interact with your specific domain tools efficiently.

2. Building with the TypeScript SDK (@modelcontextprotocol/sdk)

The official TypeScript SDK provides a type-safe, declarative API for creating production-grade MCP servers with full runtime schema validation powered by Zod.

To get started, initialize a new Node.js project and install the core SDK packages alongside TypeScript build dependencies:

mkdir custom-mcp-server && cd custom-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx

Create your main server file (src/index.ts). In the example below, we define a ticket management server with structured input validation. Notice how each tool specifies an input schema using Zod. When the LLM decides to invoke your tool, the SDK automatically validates incoming parameters before executing your business logic:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

// 1. Initialize server instance with metadata
const server = new McpServer({
  name: "enterprise-ticket-mcp",
  version: "1.0.0"
});

// 2. Register tool with Zod schema validation
server.tool(
  "query_support_tickets",
  {
    status: z.enum(["open", "in_progress", "resolved"]).default("open"),
    limit: z.number().min(1).max(50).default(10),
    priority: z.enum(["low", "medium", "high", "critical"]).optional()
  },
  async ({ status, limit, priority }) => {
    // Perform internal database lookup or API call
    const tickets = [
      { id: "TICK-101", title: "Gateway timeout on checkout", priority: "critical", status },
      { id: "TICK-102", title: "Update OAuth redirect URI", priority: "medium", status }
    ].slice(0, limit);

    return {
      content: [
        {
          type: "text",
          text: JSON.stringify({ count: tickets.length, tickets }, null, 2)
        }
      ]
    };
  }
);

// 3. Connect to Standard I/O transport
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Enterprise Ticket MCP Server running on stdio");
}

main().catch((err) => {
  console.error("Fatal error in MCP server:", err);
  process.exit(1);
});

The SDK takes care of registering JSON-RPC 2.0 handlers for tools/list and tools/call automatically, returning formatted responses compliant with the protocol specification.

You can define multiple tools, register dynamic resources using URI patterns, and declare custom prompt workflows within a single server instance.

The TypeScript SDK also includes built-in support for cancellation tokens, allowing long-running operations (such as batch file processing or external network calls) to be aborted immediately if the user stops the generation in their IDE.

3. Building with the Python SDK (FastMCP)

For Python developers and data science teams, Anthropic provides the high-level FastMCP interface. FastMCP uses native Python type annotations and Pydantic models to construct tool schemas dynamically without manual JSON boilerplate.

Install the Python SDK using pip or uv:

pip install "mcp[cli]"

Write your Python server (server.py) using clean function decorators:

from mcp.server.fastmcp import FastMCP
from typing import Literal, Optional
import json

# Initialize FastMCP application
mcp = FastMCP("DataScienceMCP")

@mcp.tool()
def compute_dataset_stats(
    dataset_name: str,
    metric: Literal["mean", "median", "variance"] = "mean",
    confidence_interval: Optional[float] = 0.95
) -> str:
    """Calculates summary statistics for a given internal dataset."""
    results = {
        "dataset": dataset_name,
        "metric": metric,
        "value": 142.85,
        "ci": confidence_interval,
        "sample_size": 10000
    }
    return json.dumps(results, indent=2)

@mcp.resource("schema://datasets")
def list_available_datasets() -> str:
    """Exposes static metadata about available internal warehouse tables."""
    return "customers, transactions, warehouse_inventory, telemetry_events"

if __name__ == "__main__":
    mcp.run()

When executed with python server.py, FastMCP binds to stdio streams and parses docstrings into human-readable tool descriptions for model prompt context.

FastMCP also supports asynchronous tool handlers (async def) with full asyncio concurrency for non-blocking I/O operations against external web APIs and data lakes.

4. The #1 Stdio Bug: Stdout Corruption & Diagnostic Logging

The single most common bug when developing stdio MCP servers is using standard print statements (console.log() in JavaScript or print() in Python) for debug logging.

Under stdio transport, standard output (stdout) is strictly reserved for JSON-RPC 2.0 message framing. If your application or a third-party library writes any unformatted plain text to stdout, the client JSON parser immediately fails with a syntax error and disconnects the server.

To prevent protocol framing corruption, always redirect diagnostic messages to standard error (process.stderr.write() in Node.js or sys.stderr.write() / logging.getLogger() in Python). Clients safely stream stderr to their local diagnostic logs without affecting protocol framing.

The Golden Rule of MCP Logging

Never write plain text to stdout. Use stderr exclusively for debug logs and telemetry. Any stdout pollution will crash the client JSON parser instantly.

5. Interactive Live Testing with MCP Inspector

Rather than restarting Claude Desktop every time you make a code change, use the official MCP Inspector to test tool schemas, inspect prompts, and view raw JSON-RPC traffic in a local web interface:

# For TypeScript servers
npx @modelcontextprotocol/inspector npx tsx src/index.ts

# For Python servers
mcp dev server.py

The inspector opens an interactive developer interface at http://localhost:5173 where you can manually invoke tools, supply mock parameters, verify schema validation errors, and observe latency benchmarks in real time.

You can also inspect the raw JSON-RPC payloads sent between the host and your server, confirming that errors are properly formatted with standard JSON-RPC error codes (such as -32602 for invalid parameters or -32603 for internal errors).

6. Connecting to Claude Desktop & Cursor IDE

Once your server is tested and compiled to JavaScript (dist/index.js), register it in your local client configuration file (claude_desktop_config.json or .cursor/mcp.json):

{
  "mcpServers": {
    "my-custom-server": {
      "command": "node",
      "args": ["/absolute/path/to/custom-mcp-server/dist/index.js"],
      "env": {
        "API_SECRET_KEY": "sk_live_internal_secret_9481"
      }
    }
  }
}

After restarting your client, the hammer icon in Claude Desktop or Cursor Settings will display your newly registered tools ready for conversational invocation.