Developer Tutorial
18 min read
Updated March 2026

How to Build Custom MCP Servers in Python & TS [2026]

Learn how to build custom MCP servers using Python and TypeScript. Our definitive 2026 tutorial covers full Model Context Protocol development from concept to deployment.

Published on March 08, 2026 • Code examples updated for SDK v1.2+

Quick Answer

To build a custom MCP server, you implement the Model Context Protocol Server SDK (available via npm or pip). Define Tools (functions the AI can call) and Resources (data the AI can read). Test it locally by configuring Claude Desktop or Cursor to launch your script via stdio.

⏱️ TL;DR: Import SDK, define schema, wrap function, connect via standard I/O.

Before You Begin

Prerequisites Checklist

  • Basic knowledge of TypeScript or Python 3.10+
  • Node.js (v18+) or pip installed
  • Claude Desktop App or Cursor IDE installed for testing
  • Familiarity with standard JSON structures

Choosing Your Language: Python vs TypeScript

Anthropic essentially supports two primary first-party SDKs for MCP development. Here is how they break down in 2026.

TypeScript

Package:@modelcontextprotocol/sdk

Best for: Frontend devs, JavaScript shops, easy npm distribution

  • Strong typing
  • Zod integration
  • Express/Fastify adapters

Python

Package:mcp-server

Best for: Data scientists, ML engineers, backend heavy stacks

  • Pydantic validation
  • FastAPI adapters
  • Native sync/async

Building a TypeScript Server

Below is a minimal example of a TypeScript MCP server that exposes a weather forecasting tool using Zod for robust input validation.

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: "weather-mcp",
  version: "1.0.0",
});

// Define the arguments schema
const weatherSchema = z.object({
  city: z.string(),
  zipcode: z.string().optional()
});

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [{
    name: "get_weather",
    description: "Fetches current weather for a city",
    // Convert zod schema to JSON schema format
    inputSchema: zodToJsonSchema(weatherSchema) 
  }]
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === "get_weather") {
    const { city } = request.params.arguments;
    // ... Implement fetch logic ...
    return {
      content: [{ type: "text", text: `It's sunny in ${city}!` }]
    };
  }
  throw new Error("Tool not found");
});

const transport = new StdioServerTransport();
await server.connect(transport);

Building a Python Server

If you prefer Python, you can utilize Pydantic models to quickly establish input validation structures. Here is the same concept constructed utilizing the official Python SDK.

import asyncio
from mcp.server import Server, NotificationOptions
from mcp.server.models import InitializationOptions
import mcp.server.stdio
import mcp.types as types

app = Server("weather-server")

@app.list_tools()
async def list_tools() -> list[types.Tool]:
    return [
        types.Tool(
            name="get_weather",
            description="Fetches current weather for a city",
            inputSchema={
                "type": "object",
                "properties": {
                    "city": {"type": "string"}
                },
                "required": ["city"]
            }
        )
    ]

@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
    if name == "get_weather":
        city = arguments.get("city")
        return [types.TextContent(
            type="text",
            text=f"It's sunny in {city}!"
        )]

async def main():
    async with mcp.server.stdio.stdio_server() as (read, write):
        await app.run(read, write, InitializationOptions())

if __name__ == "__main__":
    asyncio.run(main())

✅ Do:

  • • Handle all internal errors gracefully so you do not crash the `stdio` pipe.
  • • Use Zod or Pydantic to strongly define what properties `inputSchema` expects.
  • • Test your server using the official MCP Inspector terminal tool.

❌ Don't:

  • • Don't use `console.log` or `print()` for debugging (it breaks the JSON-RPC stdio protocol). Use `console.error` instead.
  • • Don't expose destructive database tools without proper user-in-the-loop safeguards.

Frequently Asked Questions

What language should I use to build an MCP server?

The official SDKs support TypeScript (JavaScript) and Python. Choose the language that best fits your existing stack. TypeScript is great for web APIs and rapid NPM distribution, while Python is ideal for data or ML tasks.

How does an MCP server communicate with Claude?

Most local MCP servers communicate via stdio (standard input/output). The Claude Desktop App spins up your script as a subprocess and sends JSON-RPC messages back and forth over stdin/stdout. That's why standard print statements can break communication.

Can I monetize a custom MCP server?

Yes, but to monetize securely, you should avoid distributing raw executable scripts locally. Instead, host your MCP server remotely (using SSE or HTTP/WebSockets transits) and require standard API keys for user authentication.

Make Your Tools Available to the World

Once you build a functional MCP server, share it with the developer community! Submit your repository to our index or skip coding entirely and use our visual creator.