How to Build an MCP Server in TypeScript
Roughly 60 lines gets you a working stdio server. Here's the exact SDK, the tool shape that matters, and the parts most tutorials get wrong.

To build an MCP server in TypeScript, install @modelcontextprotocol/sdk, create an McpServer, register one or more tools with zod input schemas, and connect it to a StdioServerTransport. That's the whole loop — about 60 lines for something a client can actually call. This walks through it end to end, then the two things nobody warns you about: how many tools to ship, and how to publish so people can npx it.
If you're fuzzy on what a server even is, read what is an MCP server first — this assumes you know the shape and just want to write one.
Set up the project
Start with a plain TypeScript package and one dependency. MCP is a protocol, not a framework, so the official SDK is all you pull in:
mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm i @modelcontextprotocol/sdk zod
npm i -D typescript @types/node
Set "type": "module" in package.json — the SDK is ESM. Use a modern tsconfig.json ("module": "NodeNext", "target": "ES2022", "outDir": "dist"). zod isn't optional decoration: it's how you declare tool inputs, and the SDK turns your schema into the JSON Schema the client validates against.
Write the server
The minimum viable server is a server object, a tool, and a transport. Here's a complete src/index.ts that exposes one tool:
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: "my-mcp-server", version: "1.0.0" });
server.registerTool(
"add",
{
title: "Add two numbers",
description: "Returns the sum of a and b.",
inputSchema: { a: z.number(), b: z.number() },
},
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }],
})
);
const transport = new StdioServerTransport();
await server.connect(transport);
Three things to notice. The tool description is the prompt — the model reads it to decide when to call, so write it for the model, not for docs. The handler returns a content array, not a raw value. And you never console.log in a stdio server: stdout is the protocol channel, so any stray print corrupts the stream. Log to console.error (stderr) instead.
Choose your transport
Use stdio unless you have a specific reason not to. About 90% of MCP servers run locally over stdio, and there's a good reason: the client launches your process, talks over stdin/stdout, and you inherit the user's machine, files, and already-authenticated CLIs with zero network setup.
| Transport | When to use it | Auth | Cost to run |
|---|---|---|---|
| stdio | Local tools, filesystem, dev workflows (the default) | Inherits the shell/env | None — client spawns it |
| Streamable HTTP | Hosted/shared servers, multi-user, remote deploys | You build it (OAuth, tokens) | You host a service |
| SSE | Legacy remote setups | You build it | Deprecated — skip it |
SSE is effectively dead in the current spec; if a tutorial tells you to reach for it, the tutorial is old. Reach for Streamable HTTP only when you actually need a shared, always-on server, and take on the hosting and auth that come with it. The official Filesystem reference server is the canonical stdio example worth reading.
Don't ship 40 tools
Keep your server small on purpose. Clients have a soft budget of about 40 active tools before selection quality drops, and that budget is shared across every server the user has installed — not yours alone. If you ship 25 tools, you've eaten more than half someone's budget before they add anything else.
The median MCP server exposes around 10 tools, and that's a healthy target. Group narrowly: a coding-docs server like Context7 does its whole job with a couple of tools. Before you add a tool, ask whether the model will actually pick it correctly given everything else in the set — the full arithmetic is in the Cursor tool-limit math. Fewer, well-described tools beat a kitchen sink every time.
Test, then package
Test the server before wiring it into a client. The fastest loop is the MCP Inspector:
npx @modelcontextprotocol/inspector node dist/index.js
It spawns your server, lists the tools, and lets you call them by hand — you'll catch a broken schema or a stdout leak in seconds. Once it's clean, add it to a real client by pointing command/args at your built file (node, ["dist/index.js"]); the mechanics are the same for every client and covered in how to add an MCP server.
To let others install it, add a shebang (#!/usr/bin/env node) to your entry file, set a bin field in package.json, "files": ["dist"], run npm publish, and users can run it with npx your-package. That's the pattern behind most community servers — including the ones in the best MCP servers list.
Security is on you
Every tool you expose is an action the model can take without asking. If your add tool were instead writeFile or runShell, a bad prompt could do real damage. Scope filesystem access to explicit directories, treat write and delete tools as higher-risk than reads, and never trust tool arguments blindly — validate paths, not just types. The threat model that matters is laid out in MCP security: what actually matters. Study how the official GitHub MCP server gates write operations if you're building anything with side effects.
That's the whole job: SDK, tools, transport, test, publish. If something won't connect, the usual suspects — runtime not on PATH, stdout pollution, bad JSON — are in troubleshooting.