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:
FastMCPinstantiated 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.AsyncClientfor async I/O operations - Transport Options: STDIO via
mcp.run()or HTTP viamcp.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:
McpServerinstantiated 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:
axiosfor 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:
-
Install Dependencies: Create a virtual environment and install
mcp(version ≥1.6),pydantic, andhttpx. -
Initialize the Server: Import
FastMCPand create an instance:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("service_mcp")
- Define Pydantic Models: Create input models for each tool using Pydantic v2 features like
ConfigDictandFieldvalidation:
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)
- 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
-
Add Shared Utilities: Implement
_make_api_requestusinghttpx.AsyncClientand_handle_api_errorfor exception normalization. -
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:
-
Initialize Project: Run
npm init -yand install@modelcontextprotocol/sdk,zod,axios, and dev dependencies (typescript,tsx). Create a stricttsconfig.json. -
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" });
- 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>;
- 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 }] };
}
);
-
Implement Helpers: Create
makeApiRequestusingaxiosandhandleApiErrorfor consistent error mapping. -
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 explicitserver.registerTool()calls. - Error Handling: Python centralizes errors through
_handle_api_errorcatchinghttpxexceptions, while TypeScript useshandleApiErrorprocessingAxiosError. - Configuration: Python stores constants like
API_BASE_URLandCHARACTER_LIMITin module scope, while TypeScript organizes them inconstants.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
FastMCPwith Pydantic v2 models for declarative input validation and automatic JSON schema generation via decorators. - TypeScript MCP servers use
McpServerwith 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 likeStdioServerTransport. - Shared utilities follow similar patterns:
_make_api_request(Python) vsmakeApiRequest(TypeScript), and_handle_api_errorvshandleApiError. - Reference implementations are available in
mcp-builder/reference/python_mcp_server.mdandmcp-builder/reference/node_mcp_server.mdwithin theComposioHQ/awesome-codex-skillsrepository. - 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →