MCP TypeScript SDK v2: What Changed and the One Gotcha
V2 splits the old single package and targets the July 2026 spec. The exact versions that work, the console.log trap, and when a custom server pays off.

Version 2 of the official MCP TypeScript SDK splits the old single package into several, and it implements the Model Context Protocol specification dated 28 July 2026. If a tutorial you are following will not compile, outdated imports are the most likely reason.
A step-by-step walkthrough published this week builds a working server in roughly 100 lines, and the useful parts are the version details and the one failure mode that wastes an afternoon.
What actually changed
SDK v1 shipped as a single package, @modelcontextprotocol/sdk. V2 is split, and for building a server two matter: @modelcontextprotocol/server, plus @modelcontextprotocol/client for tests. The author pins exact versions so the example still behaves in six months — @modelcontextprotocol/server@2.4.0, zod@4.6.7, @modelcontextprotocol/client@2.4.0, typescript@7.0.2 and @types/node@22.20.5.
Two configuration details are easy to miss. The package needs "type": "module", with Node.js 20 or newer (the guide was tested on Node.js 22). And since TypeScript 6, types from @types are no longer pulled in automatically, while the SDK still refers to Node types such as Buffer — so "types": ["node"] has to be in the compiler options or you get a flood of errors.
The shape of a server
An MCP server is a small program that offers tools and data to an agent over a fixed protocol. Three roles are involved: the host is the application you work in, a client inside it holds the connection, and the server is the program you write. A server can offer tools (functions the model may call), resources (readable content addressed by URI) and prompts (templates a user picks deliberately). Tools and resources are enough to start.
Messages are JSON-RPC and travel either over stdio, where the host starts your server as a child process, or over HTTP for servers running centrally. For something that only touches your own machine and repository, stdio is the simpler choice.
The worked example is a server for Architecture Decision Records, exposing list_adrs, get_adr and create_adr_draft, with each record also readable as a resource at adr://0001.
The gotcha
The most common pitfall is mundane: a single console.log breaks the connection, because stdout belongs to the protocol. The host reads every line on stdout as a JSON-RPC message, so one non-JSON line in between ends the session — and it need not be your own code, since a library that prints a warning on import does it too. Logs therefore always go to stderr, meaning console.error.
The second thing worth copying is the path guard. In the example, the agent may only pass IDs of exactly four digits, and the resolved path must sit inside the ADR folder — so nothing escapes the directory with ../, including, as the author puts it, a model that takes a prompt from a foreign file too seriously. If your server writes files, that check is not optional.
Is it worth writing one?
There are already published servers for GitHub, databases and browsers, so the case for a custom one rests on knowledge that exists only in your team — internal conventions, architecture decisions, a service catalogue, or a tool you already drive with scripts.
The author's rule of thumb is the most portable thing in the piece: if you paste the same information into the chat for the third time, it is a candidate. The inverse is just as useful — if a single line in CLAUDE.md solves the task, you do not need a server at all. The threshold is data that changes, needs structured queries, or actions that must follow clear rules.
If you tried this with v1 and gave up, the split packages are reason enough to look again. Start with a read-only tool, confirm it in the Inspector before wiring it into a host, and keep write access behind a validated path.
More from DangMua