Guide · SDK Comparison
MCP SDK comparison: TypeScript vs Python vs Go
Three mature SDKs exist for building MCP servers: the official TypeScript SDK (@modelcontextprotocol/sdk), the official Python SDK (mcp package with FastMCP), and the community Go SDK (mark3labs/mcp-go). They all implement the same protocol, but differ substantially in how you define tool schemas, how type safety is enforced, what transports are supported, and how you deploy and test. This guide compares them side by side so you can choose the right one for your team and use case.
TL;DR
Use TypeScript when you're building for the web ecosystem, need edge runtime support, or want the most mature SDK with the widest transport coverage. Use Python when your tools call ML models, data science libraries (NumPy, pandas, scikit-learn), or you prefer decorator-based ergonomics. Use Go when you need high concurrency, low memory footprint, CPU-bound tool handlers, or static binaries with no runtime dependency. All three are production-ready. Choose based on your team's expertise and the libraries you need, not on protocol features — the MCP protocol is identical regardless of SDK.
Transport support comparison
Transport support is the most significant functional difference between SDKs — it determines which deployment environments each SDK supports:
| Transport | TypeScript SDK | Python SDK | Go (mcp-go) |
|---|---|---|---|
| stdio (local process) | Yes — StdioServerTransport | Yes — stdio mode | Yes — NewStdioServer |
| SSE (legacy HTTP) | Yes — SSEServerTransport | Yes — FastMCP SSE mode | Yes — SSE handler |
| StreamableHTTP (current) | Yes — StreamableHTTPServerTransport | Yes — streamable-http | Yes — NewStreamableHTTPServer |
| Edge runtimes (Workers, Deno) | Yes (designed for edge) | No (Python not supported on V8 edge) | No (Go runtime not available on V8 edge) |
| WebSocket (community) | Community packages | Community packages | Community packages |
The practical implication: if you need to deploy your MCP server on Cloudflare Workers or Deno Deploy, TypeScript is your only option among the three SDKs. Python and Go require a long-lived process (container, VM, or serverless function runtime) and cannot run in V8 isolate environments.
Schema definition: Zod vs Pydantic vs struct tags
How you define tool input schemas is the most visible ergonomic difference. Here's the same tool defined in all three SDKs:
// TypeScript — Zod schemas: inline, composable, runtime validation
import { z } from "zod";
server.tool(
"search_docs",
"Search the documentation",
{
query: z.string().min(1).describe("Search query"),
limit: z.number().int().min(1).max(100).default(10).describe("Max results"),
filter: z.enum(["all", "docs", "api"]).optional().describe("Category filter"),
},
async ({ query, limit = 10, filter = "all" }) => {
// query is string, limit is number — TypeScript infers types from Zod schema
return { content: [{ type: "text", text: await search(query, limit, filter) }] };
}
);
# Python — FastMCP decorator: type hints + Pydantic validation
from mcp.server.fastmcp import FastMCP
from typing import Literal
mcp = FastMCP("docs-server")
@mcp.tool()
def search_docs(query: str, limit: int = 10, filter: Literal["all", "docs", "api"] = "all") -> str:
"""Search the documentation.
Args:
query: Search query string (non-empty)
limit: Maximum results (1-100, default 10)
filter: Category filter
"""
# query is str, limit is int — Pydantic validates at runtime
return search(query, min(max(limit, 1), 100), filter)
// Go — struct tags + JSON schema reflection
type SearchArgs struct {
Query string `json:"query" jsonschema:"description=Search query string,minLength=1"`
Limit int `json:"limit" jsonschema:"description=Max results,minimum=1,maximum=100,default=10"`
Filter string `json:"filter" jsonschema:"description=Category filter,enum=all,enum=docs,enum=api"`
}
s.AddTool(mcp.NewTool("search_docs",
mcp.WithDescription("Search the documentation"),
mcp.WithSchema(reflector.Reflect(SearchArgs{}).Definitions["SearchArgs"]),
), func(ctx context.Context, req mcp.CallToolRequest) (mcp.CallToolResult, error) {
var args SearchArgs
req.UnmarshalArguments(&args)
// Go compiler knows args.Query is string, args.Limit is int
if args.Limit == 0 { args.Limit = 10 }
return mcp.NewToolResultText(search(ctx, args.Query, args.Limit, args.Filter)), nil
})
Python's decorator approach is the most concise — tool name comes from the function name, description from the docstring, and schema from type hints. TypeScript's Zod approach is the most composable — Zod schemas can be shared, extended, and combined. Go's struct tag approach integrates naturally with Go's type system but requires more boilerplate for complex schemas.
Type safety comparison
Type safety — catching type errors before runtime — differs significantly across SDKs:
| Aspect | TypeScript | Python | Go |
|---|---|---|---|
| Tool argument types | Inferred from Zod at compile time (if using satisfies) | Type hints checked by mypy/pyright (static), Pydantic at runtime | Struct fields checked by Go compiler at compile time |
| Return type | CallToolResult checked by TypeScript if typed correctly | Return type hint checked by mypy/pyright | Compiler enforces (mcp.CallToolResult, error) signature |
| Missing tool handlers | Not caught at compile time (runtime error) | Not caught at compile time | Not caught at compile time (handler is a function argument) |
| JSON parse errors | Zod throws ZodError — must catch | Pydantic raises ValidationError — FastMCP handles automatically | UnmarshalArguments returns error — must check |
| Tool name typos | String literal — not caught at compile time | Function name — typo-resistant (decorator) | String literal — not caught at compile time |
Go has the strongest compile-time guarantees for handler types but the weakest for schema-to-handler consistency. TypeScript with strict Zod typing and the satisfies operator can approach Go-level safety for argument types. Python's FastMCP is the most ergonomic but relies on mypy/pyright for static checking — without a type checker configured, type annotations are documentation only.
Testing patterns
Each SDK has distinct testing idioms that reflect the language's testing culture:
// TypeScript — vitest or jest, mock-friendly
import { describe, it, expect, vi } from "vitest";
describe("search_docs tool", () => {
it("returns results for valid query", async () => {
vi.mock("./db", () => ({ search: vi.fn().mockResolvedValue([{ id: "1", title: "Auth guide" }]) }));
const result = await handleSearchDocs({ query: "auth", limit: 5 }, {});
expect(result.content[0].text).toContain("Auth guide");
});
it("returns isError for empty query", async () => {
// Zod throws for empty query before handler runs
await expect(handleSearchDocs({ query: "" }, {})).rejects.toThrow();
});
});
# Python — pytest, straightforward function calls
import pytest
from unittest.mock import AsyncMock, patch
from server import search_docs # FastMCP tool function is just a regular Python function
def test_search_returns_results():
with patch("server.db.search", return_value=[{"id": "1", "title": "Auth guide"}]):
result = search_docs(query="auth", limit=5)
assert "Auth guide" in result
def test_search_validates_limit():
with pytest.raises(Exception): # FastMCP/Pydantic validates at call time
search_docs(query="auth", limit=0)
// Go — testing package, table-driven, race detector
func TestHandleSearch(t *testing.T) {
tests := []struct {
name string; query string; limit int; wantErr bool
}{
{"valid", "auth", 5, false},
{"empty query", "", 5, true},
{"zero limit uses default", "auth", 0, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := handleSearch(context.Background(), makeReq(tt.query, tt.limit))
if err != nil { t.Fatal(err) }
if result.IsError != tt.wantErr { t.Errorf("IsError=%v, want %v", result.IsError, tt.wantErr) }
})
}
}
// Run with: go test ./... -race -count=1
Python's testing is the simplest — FastMCP tool functions are plain Python functions that you call directly. TypeScript tests are slightly more complex due to async/await and Zod validation happening at call time. Go's table-driven approach has the highest upfront boilerplate but scales to many test cases cleanly, and the -race flag catches concurrency bugs that don't appear in TypeScript or Python tests.
Deployment model comparison
| Aspect | TypeScript | Python | Go |
|---|---|---|---|
| Startup time | 200ms–2s (Node.js + npm module loading) | 500ms–3s (Python interpreter + imports) | 20–100ms (static binary, no interpreter) |
| Memory footprint | 50–200MB (V8 + node_modules) | 80–300MB (Python runtime + packages) | 10–50MB (static binary + goroutine stacks) |
| Docker image size | 200–500MB (Node Alpine + node_modules) | 200–600MB (Python slim + pip packages) | 15–30MB (distroless or scratch + binary) |
| Edge runtime support | Yes (Workers, Deno, Vercel Edge) | No | No |
| Dependency management | npm/pnpm lock file; bundler for edge | pip/uv/poetry lock file; venv isolation | go.sum lock file; no venv needed |
| Cold start on serverless | Good (Workers: <5ms; Lambda Node: 100–500ms) | Poor (Lambda Python: 200ms–1s; worse with pandas) | Excellent (Lambda Go: 50–200ms; no interpreter) |
Python's cold start on serverless functions (AWS Lambda, GCP Cloud Functions) is the worst of the three, particularly with data science imports. If your Python MCP server imports NumPy, pandas, or PyTorch, Lambda cold starts of 2–5 seconds are common. Mitigate with Lambda SnapStart, provisioned concurrency, or by running the server as a long-lived container instead.
Decision table: which SDK to choose
| Your situation | Best SDK |
|---|---|
| Existing Node.js/TypeScript codebase | TypeScript |
| MCP tools call ML models, NumPy, pandas, scikit-learn | Python |
| Need to deploy on Cloudflare Workers or Deno Deploy | TypeScript |
| Need >100 concurrent tool calls with low memory | Go |
| Team primarily writes Go microservices | Go |
| Need smallest possible Docker image | Go |
| Want fastest tool handler development iteration | Python (FastMCP decorator) |
| Need strong compile-time type guarantees | Go (compiler) or TypeScript (strict + Zod) |
| Building for Claude Desktop (local stdio tool) | TypeScript or Python (both well-supported) |
Monitoring: SDK-agnostic external probing
One advantage of the MCP protocol: monitoring is SDK-agnostic. AliveMCP sends the same initialize JSON-RPC request to your MCP server regardless of whether it's built in TypeScript, Python, or Go. The protocol handshake, tools/list hash, and tool call response are identical across all three SDKs — monitoring works the same way.
The monitoring differences are at the infrastructure level, not the SDK level:
- TypeScript on edge runtimes: no central process; external probing is your only monitoring view
- Python on Lambda/Cloud Functions: cold starts on the monitoring probe itself — set AliveMCP timeout higher (3–5s) for Python serverless
- Go on bare containers: fast startup; set tight AliveMCP timeout (500ms–1s); goroutine leaks → latency increase is detectable early
Frequently asked questions
Can I call a Python MCP server from a TypeScript client (or vice versa)?
Yes — the MCP protocol is language-agnostic. A TypeScript client (e.g., Claude Desktop, the TypeScript MCP SDK client) connects to any MCP server regardless of what language it's built in. The wire protocol is JSON-RPC over HTTP, SSE, or stdio. The only compatibility requirement is that both sides implement the same MCP protocol version (currently 2024-11-05). If your TypeScript client and Python server both use recent versions of their respective SDKs, they're compatible.
Is there a performance difference for tool handler execution speed?
For I/O-bound tools (the majority: API calls, database queries, HTTP requests), the difference is negligible — all three SDKs spend most time waiting on network I/O. For CPU-bound tools (JSON parsing, hashing, text processing), Go is typically 5–20× faster than Node.js which is 2–5× faster than Python (for pure Python code without numpy/numba). For NumPy-accelerated Python (BLAS-level linear algebra), Python can match or exceed Node.js performance for those specific operations. Optimize for your team's productivity first; optimize for runtime performance only when profiling identifies a real bottleneck.
Can I build a multi-language MCP setup (some tools in TypeScript, others in Python)?
Not directly within a single MCP server — a server is one process with one SDK. However, you can build a TypeScript MCP server that delegates specific tool calls to a Python subprocess or internal HTTP service. The TypeScript MCP server handles the MCP protocol; the Python service handles the ML-heavy computation. This hybrid approach lets you keep MCP clients connected to a single server endpoint while using the best language for each tool's job.
How do the SDKs handle tool streaming / progress updates?
The MCP protocol supports notifications (server → client updates during long operations) via the notifications/progress message. The TypeScript SDK has the most complete implementation of progress notifications. The Python SDK supports them in recent versions. The Go mcp-go SDK has partial support — check the GitHub releases for the latest status. If streaming progress is critical to your use case, verify support in the specific SDK version before committing to it.
Further reading
- MCP server with TypeScript — Zod schemas, strict types, and the TS SDK
- Python MCP server — FastMCP, Pydantic validation, and async handlers
- MCP server with Go — Go SDK, goroutines, and Docker deployment
- MCP server transport selection — SSE vs StreamableHTTP vs stdio
- MCP server type safety — Zod, Pydantic, and compile-time guarantees
- AliveMCP — continuous protocol monitoring for MCP servers