Why TypeScript for Production MCP
Lesson 11 · MCP Servers · ~10 min read
The pivot
Modules 1–4 built PKB in Python because Python is the fastest path from zero to “I have a working MCP tool I can call from Claude Desktop”. The SDK is mature, the syntax is light, the iteration loop is short. For learning MCP, Python wins and it is not close.
For shipping MCP to other people, the calculus changes. Once the server has to run somewhere other than your laptop, has to accept connections from someone other than you, or has to survive a security review written by someone other than yourself, every hidden cost that Python papered over starts to show up on the bill: image size, cold start, edge-runtime support, transport features the SDK shipped first elsewhere.
This lesson is the bridge. It writes no code. It establishes the trade-off honestly and gives the reader explicit permission to choose Python and stop the course here if the use-case is personal — or to commit to the TypeScript half of the course if the goal is to ship to other humans on the public internet.
The 2026 MCP ecosystem
A snapshot of what the ecosystem actually looks like in 2026: the majority of public, remote, internet-facing MCP servers are written in TypeScript. Not because TypeScript is a better language — that argument is older than this course and unwinnable — but because four concrete forces line up the same direction.
- SDK feature velocity. The TypeScript SDK ships new transport features first. HTTP+SSE, Streamable HTTP, the session-resumability prototype, the middleware shape for auth/rate-limit/audit — all landed in TS before reaching the Python SDK. If today’s recommended pattern matters today, TS gets it first.
- Container ergonomics. Node, Bun, and Deno containers are smaller and start faster than equivalent Python containers. For a registry or autoscaler cycling pods, that compounds into real pull-time and bandwidth-bill differences (see the next section for the numbers).
- Edge-runtime support. The JavaScript/TypeScript edge-runtime ecosystem — Cloudflare Workers, Vercel Functions, Deno Deploy, Fly.io machines, AWS Lambda@Edge — treats Node as a first-class citizen. Python on those same platforms is at best a second-class citizen and at worst not supported. If you want your MCP server geographically close to the user without operating your own globe of servers, the edge platforms are it, and the edge platforms speak Node.
- Author pool. The humans writing public MCP servers overlap heavily with the existing web-API developer base — people who already write TypeScript-on-Node every day. Picking the language that matches your contributor pool is free. Picking the language that doesn’t costs you reviews and pull-requests.
Python keeps two strongholds. The first is local-only servers: a Claude-Desktop-only personal tool, an editor plugin, an internal team helper that never leaves a laptop. The second is AI-research servers that wrap heavy scientific dependencies — PyTorch, transformers, scikit-learn, NumPy with custom C-kernels. Porting those tools to TS would either mean shelling out to a Python subprocess per call (slow and ugly) or re-implementing the math (years of work for no user-visible win). PKB-the-Python-version is genuinely useful for the rest of your life as a local notes assistant; PKB-the-TS-version is what you push to a domain other people can hit.
Performance and deploy ergonomics
Two numbers, both back-of-envelope but both reproducible on a modern laptop:
- Image size.
node:20-alpine+ a typical PKB-shaped server with SDK, Zod, and pino weighs in around ~80 MB.python:3.12-slim+ the same shape lands around ~250 MB; the non-slim base balloons to ~900 MB. The 3–5x ratio shows up every time a registry pulls the image, every time an autoscaler cold-spawns a pod, and every line item on the bandwidth bill. - Cold start. Node +
@modelcontextprotocol/sdkcold-imports in roughly ~150 ms. Python + themcppackage cold-imports in roughly ~800 ms — most of that is Pydantic,cryptography, and the SDK’s own dependency graph waking up. For HTTP MCP where a scale-from-zero session per user is on the table, that is the difference between “snappy” and “noticeable”.
These ratios flip in two cases. First, if your tools do heavy CPU work — embedding generation, image processing, vector math — the per-call runtime cost dwarfs cold-start and image size, and language choice matters less than tool design. Second, if you deploy as a long-running process with no autoscale (a VM, a single pod pinned to one node), cold-start is paid once at boot and never again; the steady-state cost is what matters. The decision rule is therefore narrow but sharp: edge-style HTTP MCP with scale-to-zero → TypeScript; long-running personal process → Python.
The SDK feature lead
As of 2026, the TypeScript SDK ships transport features roughly two quarters ahead of the Python SDK:
- HTTP+SSE transport. Landed in TS first, then ported to Python.
- Streamable HTTP. The post-SSE iteration; TS-first.
- Remote-session resumability. The pattern for surviving a client reconnect mid-session was prototyped in TS.
- Middleware shape for auth / rate-limit / audit. Only well-documented in TS today; the Python equivalent exists but is sparser in the official docs and examples.
Python catches up — usually within a release or two — but if you want today’s recommended pattern today, the TS SDK is the path. None of this is permanent. It reflects the protocol authors’ shipping cadence at the time of writing, and the gap could close (or invert) over time. Reassess each year before making this call from memory.
When to stay in Python
Stay in Python when
- The server is personal — one user (you), one machine (your laptop), one client (Claude Desktop). Do not pay a porting tax for a single-user system. Python wins for personal forever.
- The tools wrap scientific Python — PyTorch, transformers,
scikit-learn, NumPy with custom kernels, anything that depends on a CUDA-bound
wheel. Porting means either shelling out to a Python subprocess per call (one
forkper tool invocation, latency you cannot recover) or re-implementing the math (years of work). Wrap-and-ship the Python server; do not port. - The server is a throwaway prototype. Iteration speed wins; deploy ergonomics do not matter for code with a one-week lifetime. Get to “does this work?” in Python and decide on a port only if the prototype survives.
- Your compliance or audit shop only signs off on Python. Picking your implementation language to match your security review is a real engineering decision, not a cop-out. The TS port that never passes review is worse than the Python server that ships.
If none of those apply and you are building a server other people will connect to over the internet, the rest of Module 5 is for you.
What we carry over
Conceptually, the TS port preserves every shape from the Python half of the course. Nothing you learned in Modules 1–4 is wasted; the names and packages change but the patterns do not:
- Tool name → schema → handler. Zod replaces Pydantic; the shape is identical (input schema, runtime validation, error mapping back to the client). If you can write a Pydantic-validated FastMCP tool, you can write a Zod-validated TS tool.
- Structured logging by tool.
pinochild loggers replace thelog_tool_calldecorator from L10; the log-line format (tool=… phase=… dur_ms=…, redacted by default) is identical. - Per-user / per-tenant isolation. Bearer-token middleware in L14 replaces the env-var auth stub from L09; the isolation guarantees and the threat model carry over unchanged.
- Sampling and elicitation. The TS SDK exposes the same call shapes for both. We will not re-derive the patterns; the L07 mental model is the TS mental model.
- Local debugging.
mcp devand the Inspector are transport-agnostic and work against TS servers exactly the way they worked against PKB’s Python server in L04 and L10.
What is new in TS-land
Things that have no Python counterpart in our course so far and that will bite you the first time you meet them:
- Build pipeline.
tsccompiles TypeScript to plain JS indist/;tsxruns.tssource directly without a precompile step. We usetsxfor the dev loop (instant feedback) and thetsc-built artefacts for the production Docker image. There is no equivalent of “just runpython server.py” — you pick a runner deliberately. - ESM-only. The TS MCP SDK requires
"type": "module"inpackage.jsonand"module": "ES2022"intsconfig.json. CommonJS will not work. Be explicit about file extensions on local imports —./tools.js, not./tools— even from inside a.tsfile. ESM does not auto-resolve extensions. - Zod for runtime validation. TypeScript types vanish at compile
time; the runtime sees plain JS values. Zod gives you a runtime schema that also
produces a TS type via
z.infer<typeof Schema>. Use Zod everywhere a tool argument crosses a trust boundary — which is every tool argument. Treating Zod as “TypeScript with extra steps” misses the point. - Docker as the deploy unit. Lesson 13 makes Docker the default deployment story for the TS server. Python’s “venv on a VM” pattern is rejected on purpose: it does not reproduce, it does not scale, and it is the wrong starting point for an internet-facing service. Caddy provides automatic TLS in front of the container.
Common pitfalls
Five pitfalls before you write a line of TS
- Porting your Python server to TS first and then asking whether you
needed to. Port last, decide first. Run the decision memo from the
lab before you run
npm init. - Treating Zod as “TypeScript with extra steps”. Runtime validation is the whole point; the TS types you get for free are a bonus, not the reason you added the dependency.
- Forgetting
"type": "module"inpackage.json. You will get opaque ESM/CJS errors at import time that look unrelated to the missing field. Set it before you write the firstimport. - Skipping the
.jsextension on local imports under ESM. Thetscbuild will succeed; Node will throw “module not found” at runtime. Write./tools.jseven when the file on disk istools.ts. - Choosing TS for a single-user laptop server “because the lesson said so”. Re-read the When to stay in Python section. The lesson does not say so; the lesson says think.
References
- MCP TypeScript SDK (GitHub) — the official SDK we adopt in Lesson 12; readme covers transports, tool registration, and the middleware hooks.
- Node.js release schedule
— the LTS calendar; Module 5 targets Node 20+ and the Dockerfile pins
node:20-alpine. - Zod documentation — runtime schemas with TS
type inference; we use
z.object,z.string().min().max(), andz.inferthroughout Module 5. - tsx README — the no-compile-step runner we use for the dev loop in Lessons 12–15.
- pino documentation — the structured logger
that replaces Python’s
logging+ thelog_tool_calldecorator from L10. - node:20-alpine on Docker Hub — the base image; size and contents listing for the deploy-ergonomics numbers in this lesson.
- TS sample-server skeleton — the
existing stub at
mcp-servers/sample-server/typescript/that Lessons 12–15 grow into a hardened, multi-tenant, Docker-deployable HTTP MCP server.