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:

TransportTypeScript SDKPython SDKGo (mcp-go)
stdio (local process)Yes — StdioServerTransportYes — stdio modeYes — NewStdioServer
SSE (legacy HTTP)Yes — SSEServerTransportYes — FastMCP SSE modeYes — SSE handler
StreamableHTTP (current)Yes — StreamableHTTPServerTransportYes — streamable-httpYes — 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 packagesCommunity packagesCommunity 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:

AspectTypeScriptPythonGo
Tool argument typesInferred from Zod at compile time (if using satisfies)Type hints checked by mypy/pyright (static), Pydantic at runtimeStruct fields checked by Go compiler at compile time
Return typeCallToolResult checked by TypeScript if typed correctlyReturn type hint checked by mypy/pyrightCompiler enforces (mcp.CallToolResult, error) signature
Missing tool handlersNot caught at compile time (runtime error)Not caught at compile timeNot caught at compile time (handler is a function argument)
JSON parse errorsZod throws ZodError — must catchPydantic raises ValidationError — FastMCP handles automaticallyUnmarshalArguments returns error — must check
Tool name typosString literal — not caught at compile timeFunction 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

AspectTypeScriptPythonGo
Startup time200ms–2s (Node.js + npm module loading)500ms–3s (Python interpreter + imports)20–100ms (static binary, no interpreter)
Memory footprint50–200MB (V8 + node_modules)80–300MB (Python runtime + packages)10–50MB (static binary + goroutine stacks)
Docker image size200–500MB (Node Alpine + node_modules)200–600MB (Python slim + pip packages)15–30MB (distroless or scratch + binary)
Edge runtime supportYes (Workers, Deno, Vercel Edge)NoNo
Dependency managementnpm/pnpm lock file; bundler for edgepip/uv/poetry lock file; venv isolationgo.sum lock file; no venv needed
Cold start on serverlessGood (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 situationBest SDK
Existing Node.js/TypeScript codebaseTypeScript
MCP tools call ML models, NumPy, pandas, scikit-learnPython
Need to deploy on Cloudflare Workers or Deno DeployTypeScript
Need >100 concurrent tool calls with low memoryGo
Team primarily writes Go microservicesGo
Need smallest possible Docker imageGo
Want fastest tool handler development iterationPython (FastMCP decorator)
Need strong compile-time type guaranteesGo (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:

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

Know when your MCP server is down — before users do

AliveMCP probes your server's MCP endpoint every minute, detects protocol errors and transport failures, and pages you before users notice.

Start monitoring free