How to Build MCP Servers in Python vs TypeScript: A Complete Implementation Guide

Python MCP servers use the FastMCP class with Pydantic models for automatic JSON schema generation, while TypeScript implementations leverage the McpServer class with Zod schemas for compile-time type safety—both produce identical MCP protocol contracts.

The Model Context Protocol (MCP) SDK enables you to build interoperable servers that expose tools to AI systems. Whether you choose Python or TypeScript, your MCP server will implement the same core responsibilities: transport handling, tool registration, input validation, and error management. This guide references the implementation patterns found in the ComposioHQ/awesome-codex-skills repository to show you exactly how to construct production-ready MCP servers in both languages.

Architecture and SDK Comparison

Both Python and TypeScript MCP servers share identical architectural responsibilities but use language-specific SDKs and validation libraries.

Python Implementation Structure

Python MCP servers rely on the FastMCP class from mcp.server.fastmcp as the primary server abstraction. According to the reference implementation in mcp-builder/reference/python_mcp_server.md, the architecture centers on:

  • Server Class: FastMCP instantiated with a service name (e.g., FastMCP("service_mcp"))
  • Input Validation: Pydantic v2 models that automatically generate JSON schemas for MCP tool definitions
  • HTTP Client: httpx.AsyncClient for async I/O operations
  • Transport Options: STDIO via mcp.run() or HTTP via mcp.run(transport="streamable_http", port=8000)

The Python implementation uses decorator-based tool registration. You apply @mcp.tool(name="...", annotations={...}) to async functions, passing Pydantic models as input parameters. Shared utilities like _make_api_request and _handle_api_error centralize HTTP logic and error handling using httpx exceptions.

TypeScript Implementation Structure

TypeScript implementations use the McpServer class from @modelcontextprotocol/sdk/server/mcp as detailed in mcp-builder/reference/node_mcp_server.md:

  • Server Class: McpServer instantiated with metadata (e.g., new McpServer({name: "service-mcp-server", version: "1.0.0"}))
  • Input Validation: Zod schemas that provide both runtime validation and TypeScript type inference
  • HTTP Client: axios for async request handling
  • Transport Options: StdioServerTransport, SSEServerTransport, or custom HTTP transports

Tool registration in TypeScript uses the imperative server.registerTool(name, config, handler) method. The configuration object includes the Zod schema, annotations, and metadata. Helper functions like makeApiRequest and handleApiError manage axios calls and centralized error processing.

Step-by-Step Implementation Guide

Building an MCP Server in Python

Follow this workflow to create a Python MCP server according to the mcp-builder/reference/python_mcp_server.md specification:

  1. Install Dependencies: Create a virtual environment and install mcp (version ≥1.6), pydantic, and httpx.

  2. Initialize the Server: Import FastMCP and create an instance:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("service_mcp")
  1. Define Pydantic Models: Create input models for each tool using Pydantic v2 features like ConfigDict and Field validation:
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional

class UserSearchInput(BaseModel):
    model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True, extra="forbid")
    query: str = Field(..., min_length=2, max_length=200)
    limit: Optional[int] = Field(default=20, ge=1, le=100)
  1. Implement Tool Functions: Decorate async functions with @mcp.tool() and include annotation hints:
@mcp.tool(name="search_users", annotations={
    "readOnlyHint": True,
    "destructiveHint": False,
    "idempotentHint": True,
    "openWorldHint": True
})
async def search_users(params: UserSearchInput) -> str:
    # Implementation with _make_api_request helper

    pass
  1. Add Shared Utilities: Implement _make_api_request using httpx.AsyncClient and _handle_api_error for exception normalization.

  2. Run the Server: Execute via STDIO or HTTP:

if __name__ == "__main__":
    mcp.run()  # STDIO default

    # Or: mcp.run(transport="streamable_http", port=8000)

Building an MCP Server in TypeScript

The TypeScript workflow in mcp-builder/reference/node_mcp_server.md follows these steps:

  1. Initialize Project: Run npm init -y and install @modelcontextprotocol/sdk, zod, axios, and dev dependencies (typescript, tsx). Create a strict tsconfig.json.

  2. Create Server Instance: Import and instantiate McpServer:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

const server = new McpServer({ name: "service-mcp-server", version: "1.0.0" });
  1. Define Zod Schemas: Construct schemas with .describe() for documentation and .default() for optional fields:
import { z } from "zod";

const UserSearchInputSchema = z.object({
  query: z.string().min(2).max(200).describe("Search term"),
  limit: z.number().int().min(1).max(100).default(20),
  offset: z.number().int().min(0).default(0)
}).strict();

type UserSearchInput = z.infer<typeof UserSearchInputSchema>;
  1. Register Tools: Use server.registerTool() with the schema, annotations, and handler:
server.registerTool(
  "search_users",
  {
    title: "Search Users",
    description: "Search users by name or email",
    inputSchema: UserSearchInputSchema,
    annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
  },
  async (params: UserSearchInput) => {
    // Implementation using makeApiRequest
    return { content: [{ type: "text", text: formatted }] };
  }
);
  1. Implement Helpers: Create makeApiRequest using axios and handleApiError for consistent error mapping.

  2. Configure Transport: Connect to STDIO or SSE transport and run:

import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

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

Code Examples: Search Tool Implementation

Here are complete implementations of an identical "search users" tool in both languages, following the patterns in ComposioHQ/awesome-codex-skills.

Python Implementation

from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional, List
import httpx

# Server initialization

mcp = FastMCP("example_mcp")

# Input validation model

class UserSearchInput(BaseModel):
    model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True, extra="forbid")
    query: str = Field(..., min_length=2, max_length=200, description="Search term")
    limit: Optional[int] = Field(default=20, ge=1, le=100)
    offset: Optional[int] = Field(default=0, ge=0)
    response_format: str = Field(default="markdown", description="markdown|json")

# Shared API helper

async def _make_api_request(endpoint: str, params: dict) -> dict:
    async with httpx.AsyncClient() as client:
        r = await client.get(
            f"https://api.example.com/v1/{endpoint}",
            params=params,
            timeout=30
        )
        r.raise_for_status()
        return r.json()

# Tool registration with annotations

@mcp.tool(
    name="example_search_users",
    annotations={
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": True
    }
)
async def example_search_users(params: UserSearchInput) -> str:
    data = await _make_api_request(
        "users/search",
        {"q": params.query, "limit": params.limit, "offset": params.offset}
    )
    # Format logic here

    return formatted_response

# Entry point

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

TypeScript Implementation

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

// Server initialization
const server = new McpServer({ name: "example-mcp", version: "1.0.0" });

// Input validation schema
enum ResponseFormat {
  MARKDOWN = "markdown",
  JSON = "json",
}

const UserSearchInputSchema = z.object({
  query: z.string().min(2).max(200).describe("Search term"),
  limit: z.number().int().min(1).max(100).default(20),
  offset: z.number().int().min(0).default(0),
  response_format: z.nativeEnum(ResponseFormat).default(ResponseFormat.MARKDOWN),
}).strict();

type UserSearchInput = z.infer<typeof UserSearchInputSchema>;

// Shared API helper
async function makeApiRequest<T>(endpoint: string, params: unknown): Promise<T> {
  const r = await axios.get(`https://api.example.com/v1/${endpoint}`, {
    params,
    timeout: 30000
  });
  return r.data;
}

// Tool registration
server.registerTool(
  "example_search_users",
  {
    title: "Search Example Users",
    description: "Search users by name, email or team. Returns markdown or JSON.",
    inputSchema: UserSearchInputSchema,
    annotations: {
      readOnlyHint: true,
      destructiveHint: false,
      idempotentHint: true,
      openWorldHint: true
    },
  },
  async (params: UserSearchInput) => {
    const data = await makeApiRequest<unknown>("users/search", {
      q: params.query,
      limit: params.limit,
      offset: params.offset,
    });
    // Format logic here
    return { content: [{ type: "text", text: formatted }] };
  }
);

// Transport and execution
(async () => {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Example MCP server running via stdio");
})();

Key Differences and Selection Criteria

When deciding between Python and TypeScript for your MCP server, consider these technical distinctions:

  • Type System: Python uses Pydantic for runtime validation with type hints, while TypeScript combines Zod for runtime checks with compile-time static typing.
  • Tool Registration: Python favors the @mcp.tool() decorator pattern, whereas TypeScript uses explicit server.registerTool() calls.
  • Error Handling: Python centralizes errors through _handle_api_error catching httpx exceptions, while TypeScript uses handleApiError processing AxiosError.
  • Configuration: Python stores constants like API_BASE_URL and CHARACTER_LIMIT in module scope, while TypeScript organizes them in constants.ts.
  • Deployment: Python requires only a Python runtime (≥3.10), while TypeScript needs Node.js ≥18 and a build step (TypeScript to JavaScript).

Both implementations follow the identical best-practice checklist documented in mcp-builder/reference/mcp_best_practices.md, covering tool naming conventions, pagination, character limits, and annotation usage.

Summary

  • Python MCP servers use FastMCP with Pydantic v2 models for declarative input validation and automatic JSON schema generation via decorators.
  • TypeScript MCP servers use McpServer with Zod schemas for runtime validation and compile-time type safety through explicit registration.
  • Both support STDIO and HTTP transports, with Python using mcp.run() and TypeScript using transport classes like StdioServerTransport.
  • Shared utilities follow similar patterns: _make_api_request (Python) vs makeApiRequest (TypeScript), and _handle_api_error vs handleApiError.
  • Reference implementations are available in mcp-builder/reference/python_mcp_server.md and mcp-builder/reference/node_mcp_server.md within the ComposioHQ/awesome-codex-skills repository.
  • Annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) are supported in both languages to provide metadata about tool behavior.

Frequently Asked Questions

Can I mix Python and TypeScript MCP servers in the same application?

Yes. Because both implementations produce identical MCP protocol contracts, a client application can consume Python and TypeScript MCP servers simultaneously. The protocol standardizes tool definitions, JSON schemas, and transport mechanisms, making language interoperability transparent to the consuming client.

Which language offers better performance for high-concurrency MCP servers?

TypeScript may offer marginally better performance in high-concurrency I/O scenarios due to the V8 event loop optimization, while Python uses async/await with httpx.AsyncClient. However, both achieve comparable async I/O throughput. For CPU-bound operations, TypeScript's single-threaded nature requires worker threads, while Python can leverage multiprocessing.

How do I handle pagination and character limits in both languages?

Both implementations follow the same patterns documented in mcp-builder/reference/mcp_best_practices.md. You implement pagination by accepting limit and offset parameters in your input schemas (Pydantic or Zod), and enforce character limits using shared utilities like _make_api_request or makeApiRequest that truncate responses before returning them to the client.

What are the annotation hints and do they work differently across languages?

Annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) provide metadata about tool side effects and safety. In Python, you pass them as a dictionary to the @mcp.tool() decorator. In TypeScript, you include them in the annotations object of the registerTool configuration. Both pass this metadata identically to the MCP client.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →