MCP Directory

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.

Hua·June 30, 2026·6 min read
Vivid abstract image of dynamic blue light patterns creating a captivating visual effect.
Photo by photoGraph on Pexels

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.

TransportWhen to use itAuthCost to run
stdioLocal tools, filesystem, dev workflows (the default)Inherits the shell/envNone — client spawns it
Streamable HTTPHosted/shared servers, multi-user, remote deploysYou build it (OAuth, tokens)You host a service
SSELegacy remote setupsYou build itDeprecated — 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.

FAQ

Is the MCP TypeScript SDK free to use?

Yes. @modelcontextprotocol/sdk is the official open-source SDK, MIT-licensed and free on npm. Building and publishing an MCP server costs nothing unless you choose to host a remote (HTTP) server, which means paying for wherever you run it. Local stdio servers have no hosting cost because the client launches the process on the user's machine.

Do I need to know the MCP protocol internals to build a server?

No. The SDK handles the JSON-RPC framing, the initialize handshake, and schema generation. You register tools with zod input schemas and return a content array; the SDK does the rest. You only touch protocol details if you write a custom transport or debug a low-level issue.

What's the difference between stdio and HTTP for a TypeScript MCP server?

Stdio runs locally: the client spawns your process and talks over stdin/stdout, so you inherit the user's environment with no auth or hosting. HTTP (Streamable HTTP) is for shared or remote servers you host yourself, where you're responsible for auth. About 90% of servers use stdio — start there unless you specifically need a multi-user, always-on server.

How do I publish my MCP server so others can install it with npx?

Add a #!/usr/bin/env node shebang to your built entry file, set a bin field in package.json pointing at it, include "files": ["dist"], then run npm publish. Users then run npx your-package as the command in their client config. Test with npx @modelcontextprotocol/inspector first.

How many tools should my MCP server expose?

Aim for around 10 or fewer. Clients degrade past roughly 40 active tools, and that budget is shared across every server a user installs, not yours alone. The median server ships about 10 tools. Fewer tools with sharp descriptions get called more accurately than a large, overlapping set.

Put this into practice

Browse MCP servers by capability, or check your own setup's tool budget and security.

More essays