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

> Compare Python and TypeScript for building MCP servers. Learn complete implementation guides and see how Pydantic and Zod ensure identical protocol contracts.

- Repository: [Composio/awesome-codex-skills](https://github.com/composiohq/awesome-codex-skills)
- Tags: how-to-guide
- Published: 2026-04-26

---

**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`](https://github.com/ComposioHQ/awesome-codex-skills/blob/main/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`](https://github.com/ComposioHQ/awesome-codex-skills/blob/main/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`](https://github.com/ComposioHQ/awesome-codex-skills/blob/main/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:

```python
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("service_mcp")

```

3. **Define Pydantic Models**: Create input models for each tool using Pydantic v2 features like `ConfigDict` and `Field` validation:

```python
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)

```

4. **Implement Tool Functions**: Decorate async functions with `@mcp.tool()` and include annotation hints:

```python
@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

```

5. **Add Shared Utilities**: Implement `_make_api_request` using `httpx.AsyncClient` and `_handle_api_error` for exception normalization.

6. **Run the Server**: Execute via STDIO or HTTP:

```python
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`](https://github.com/ComposioHQ/awesome-codex-skills/blob/main/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`](https://github.com/ComposioHQ/awesome-codex-skills/blob/main/tsconfig.json).

2. **Create Server Instance**: Import and instantiate `McpServer`:

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

const server = new McpServer({ name: "service-mcp-server", version: "1.0.0" });

```

3. **Define Zod Schemas**: Construct schemas with `.describe()` for documentation and `.default()` for optional fields:

```typescript
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>;

```

4. **Register Tools**: Use `server.registerTool()` with the schema, annotations, and handler:

```typescript
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 }] };
  }
);

```

5. **Implement Helpers**: Create `makeApiRequest` using `axios` and `handleApiError` for consistent error mapping.

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

```typescript
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

```python
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

```typescript
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`](https://github.com/ComposioHQ/awesome-codex-skills/blob/main/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`](https://github.com/ComposioHQ/awesome-codex-skills/blob/main/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`](https://github.com/ComposioHQ/awesome-codex-skills/blob/main/mcp-builder/reference/python_mcp_server.md) and [`mcp-builder/reference/node_mcp_server.md`](https://github.com/ComposioHQ/awesome-codex-skills/blob/main/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`](https://github.com/ComposioHQ/awesome-codex-skills/blob/main/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.