How to build an MCP server in TypeScript
If your stack is already TypeScript, you don't need to drop into Python to ship an MCP
server. The official @modelcontextprotocol/sdk is first-class TS, and with strict
types plus Zod for argument schemas you get a server that's hard to misuse. Here's the fast
path, then the parts the quickstart leaves out.
1. Set up the project
npm init -y
npm i @modelcontextprotocol/sdk zod
npm i -D typescript @types/node
npx tsc --init
Set "type": "module" in package.json and target ES2022
with "module": "NodeNext" in tsconfig.json. MCP's SDK is ESM.
2. Define a server and a tool
Use Zod to describe tool inputs — the SDK turns the schema into what the client sees and validates arguments for you.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({ name: "weather", version: "1.0.0" });
server.tool(
"get_forecast",
{ city: z.string(), days: z.number().int().default(1) },
async ({ city, days }) => ({
content: [{ type: "text", text: `${city}: clear, ${days}d ahead` }],
})
);
const transport = new StdioServerTransport();
await server.connect(transport);
Compile with tsc and run the output with node. The server speaks
JSON-RPC over stdio, the same channel desktop clients use to launch it.
3. Connect a client
Register the launch command (e.g. node dist/server.js) in your MCP client's
config. The client spawns the process and pipes JSON-RPC through stdin/stdout.
console.log() while debugging. Don't — console.log
writes to stdout, which is the protocol stream. It corrupts the
JSON-RPC framing and the client disconnects. Use console.error (stderr) or a
logger pinned to stderr.
4. Add resources and prompts
Tools are actions; resources expose readable data and prompts
expose reusable templated messages. The SDK has server.resource(...) and
server.prompt(...) with the same ergonomics. Keep handlers thin and let Zod reject
bad input at the edge.
5. From "it runs" to "it ships"
The server above works. Putting it in front of a real agent exposes the same gaps as every other stack:
- Logging to stderr only — structured JSON, never stdout.
- Auth on privileged tools so any client that can spawn the process can't do anything.
- Rate limiting per client — agents loop, and loops are expensive.
- Backoff with jitter on outbound
fetchcalls so a transient failure retries instead of failing the tool. - Input validation — Zod gets you most of the way; enforce it everywhere.
- Graceful shutdown — handle
SIGTERM, drain in-flight work, close the transport. - Health checks and a hardened, non-root container for deploy.
Back it with tests that drive the server over the actual protocol, not mocked internals, so a refactor can't silently break a tool contract.
Or start from a base that already has all of it
The Plinth MCP Server Starter Kit ships a strict-TypeScript template (and a matching Python one) with auth gating, rate limiting, backoff, structured stderr logging, graceful shutdown, input validation, health checks and a hardened Dockerfile already wired up — and 19 TypeScript tests driving it over the real MCP protocol.
Get the kit — $39