# Build an MCP server in TypeScript with the official SDK

> To build an MCP server in TypeScript, install `@modelcontextprotocol/server` and `zod`, create an `McpServer` and register each tool with `registerTool` and a Zod input schema. Serve it with `serveStdio`, which lets a host launch the file as a local process. Run it with `npx tsx src/index.ts`, so there is no build step, and call the tool from the MCP Inspector.

Mindket editors · Published 2026-11-04 · Updated 2026-11-05 · https://mindket.com/learn/mcp/build-mcp-server-typescript

## In short

- You will build a stdio server with one tool, `get-alerts`, that looks up US weather alerts, in a single TypeScript file.
- The Zod schema is the only schema you write. The SDK builds the rest from it, including the argument check that runs before your handler.
- The package for the current SDK is `@modelcontextprotocol/server`. Older tutorials install `@modelcontextprotocol/sdk`, the 1.x line.
- One `console.log` on a stdio server corrupts the protocol. Log with `console.error`.

## Before you start

This lesson follows the first-server tutorial in the TypeScript SDK's own repository, which runs TypeScript directly with `tsx`. The [MCP build guide](https://modelcontextprotocol.io/docs/develop/build-server) builds a similar weather server with a second tool and compiles it with `tsc`. Pick that one if you want a build step.

You need Node.js 20 or later, npm and an internet connection, because the tool calls the US National Weather Service API. The Inspector in the last step needs a patched Node release, 22.23.4, 24.22.1 or 26.12.1 or later on each line, which lesson 3 explains. Check the package name before you install anything, because TypeScript tutorials come in two generations.

| Package | SDK line | Where you meet it |
| --- | --- | --- |
| `@modelcontextprotocol/server` | v2, the current stable line | The official docs and this lesson |
| `@modelcontextprotocol/client` | v2, for code that connects to servers | Clients and test scripts |
| `@modelcontextprotocol/sdk` | v1.x, the older line, still maintained | Older tutorials and existing code |

## Build the server in six steps

The code comes in two pieces, the tool and the serve call. Both go in one file, `src/index.ts`.

````typescript
import { McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';

const NWS_API = 'https://api.weather.gov';

interface AlertsResponse {
    features: { properties: { event?: string; headline?: string } }[];
}

function createServer(): McpServer {
    const server = new McpServer({ name: 'weather', version: '1.0.0' });

    server.registerTool(
        'get-alerts',
        {
            description: 'Get the active weather alerts for a US state',
            inputSchema: z.object({
                state: z.string().length(2).describe('Two-letter US state code, e.g. CA')
            })
        },
        async ({ state }) => {
            const code = state.toUpperCase();
            const url = `${NWS_API}/alerts/active?area=${code}`;
            const res = await fetch(url, { headers: { 'User-Agent': 'mcp-weather-tutorial/1.0' } });
            if (!res.ok) {
                return { content: [{ type: 'text', text: `NWS API error: HTTP ${res.status}` }], isError: true };
            }
            const { features } = (await res.json()) as AlertsResponse;
            if (features.length === 0) {
                return { content: [{ type: 'text', text: `No active alerts for ${code}.` }] };
            }
            const lines = features.map(f => f.properties.headline ?? f.properties.event ?? 'Unnamed alert');
            return { content: [{ type: 'text', text: lines.join('\n') }] };
        }
    );

    return server;
}

void serveStdio(createServer);
console.error('weather MCP server running on stdio');

````

1. Check Node with `node --version`. It must report version 20 or later.
2. Create the project with `mkdir weather && cd weather`, then `npm init -y`, then `npm pkg set type=module`. The `type=module` setting matters, because the SDK ships ES modules only.
3. Install the SDK, Zod and tsx with `npm install @modelcontextprotocol/server zod tsx`, then create the source folder with `mkdir src`.
4. Create `src/index.ts` and paste in the code above.
5. Start the server from the project root with `npx tsx src/index.ts`. It prints `weather MCP server running on stdio` to stderr and then waits. Stop it with Ctrl+C.
6. Call the tool with `npx @modelcontextprotocol/inspector npx tsx src/index.ts`. In the browser tab it opens, click Connect, open the Tools tab, pick `get-alerts`, enter a state code such as `TX` and run it.

## What the code does

`createServer` is a factory that builds a new `McpServer` and registers one tool on it. `registerTool` takes a name, a config and an async handler. The `inputSchema` is a Zod schema, and from that one schema the SDK produces the JSON Schema for the model, checks the arguments before your handler runs and infers the handler's argument types.

The handler returns `content`, a list of typed blocks, here a single `text` block. Setting `isError: true` marks a failed result that the model can read and react to. Call `get-alerts` with `{ "state": "California" }` and the SDK refuses before your handler runs. The model sees this message:

````text
Input validation error: Invalid arguments for tool get-alerts: state: Too big: expected string to have <=2 characters

````

> The factory shape is on purpose. The MCP build guide says `serveStdio` calls it whenever it needs an instance, so every call must return a fresh `McpServer`. The SDK README's shorter example wires a server by hand with `server.connect(new StdioServerTransport())`. The build guide says a server started that way only serves clients that open with the older 2025-era handshake, so `serveStdio` is the safer choice.

## What happens when a model calls your tool

The model never sees your code. It sees the tool's name, its description and the JSON Schema derived from `inputSchema`, so those three decide whether the tool gets called at all. Plug the server into a host and a single answer takes six steps.

**One tool call, from question to answer**

1. The host sends the question. In the SDK's example the host is VS Code. It sends the question to the model together with the name, description and input schema of every available tool.
2. The model picks a tool. It decides `get-alerts` answers the question and emits a call with the arguments it chose.
3. The client calls your server. The MCP client inside the host sends a `tools/call` request over stdio.
4. The SDK runs your handler. It validates the arguments against `inputSchema` first, then runs the handler.
5. Your content goes back. The handler's `content` returns over stdout as the `tools/call` result.
6. The model writes the answer. It reads the text block and writes the reply you see in the chat.

## David Soria Parra on the stateless spec and the v2 SDKs

David Soria Parra, a member of technical staff at Anthropic and co-creator of MCP, [announced the 2026-07-28 release on X](https://x.com/dsp_/status/2082173429399142616) the day it shipped. MCP became stateless, with semantics for requests that take several round trips, and the Python and TypeScript SDKs released v2.0.0 for it. That fits this lesson's factory: with no session tying a client to one object, `serveStdio` can build a fresh `McpServer` whenever it needs one.

The same post named extensions as the other headline: MCP Apps, Tasks, Enterprise Managed Auth and more, each a domain-specific way to use MCP. The TypeScript SDK's [version 2.4.0](https://github.com/modelcontextprotocol/typescript-sdk/releases/tag/v2.4.0) added Skills over MCP on 23 October. Read the [SDK's releases](https://github.com/modelcontextprotocol/typescript-sdk/releases) before you build such a feature by hand.

Later that day he [thanked the SDK maintainers on X](https://x.com/dsp_/status/2082225962402402384) and listed what supports the spec: v2.0.0 of the TypeScript, Python and C# SDKs, plus v1.7.0 of the Go SDK. The protocol is the same in each, so pick the SDK by the language your team writes.

> Serving MCP just got much simpler and more scalable.
>
> David Soria Parra, Member of technical staff at Anthropic, co-creator of MCP, [X](https://x.com/dsp_/status/2082173429399142616) (read 2026-11-05)

[![David Soria Parra (@dsp_) on X, 28 July 2026. Thrilled to announce the MCP 2026-07-28 release. MCP is now stateless, with semantics for multi-round-trip requests. Serving MCP just got much simpler and more scalable. MCP now has extensions. Python, Typescript, C# (soon) SDKs released a v2.0.0 for the new spec with improved ergonomics.](https://mindket.com/blog/shots/x-dsp-mcp-2026-07-28-release.webp?v=cf60293d4a62)](https://x.com/dsp_/status/2082173429399142616)

David Soria Parra on X, 28 July 2026

[![David Soria Parra (@dsp_) on X, later on 28 July 2026. Special thanks to the SDK maintainers getting the v2.0.0 with fundamental upgrades ready in time with the release. Grab v2.0.0 of the Typescript, Python and C# SDKs and v1.7.0 of the Go SDK supporting the spec.](https://mindket.com/blog/shots/x-dsp-sdk-v2-maintainers.webp?v=248b22307d6b)](https://x.com/dsp_/status/2082225962402402384)

David Soria Parra on X, 28 July 2026

> Screenshots of X's own embeds of the posts, taken on 5 November 2026, with the time in UTC. X cuts both posts short with Show more. Select one to open the post on X.

## Common mistakes

- Using `console.log`. On stdio, stdout is the protocol channel, and the host reports `SyntaxError: Unexpected token ... is not valid JSON`. The quoted token is the first character of the stray line. Switch to `console.error`.
- Leaving out the `type=module` line in package.json from step 2.
- Two copies of zod in the tree. TypeScript can fail with `TS2589: Type instantiation is excessively deep`. Run `npm ls zod` and make it list one version, with an `overrides` entry in package.json if a dependency pins another.
- Running Node 18 or older. Every SDK package needs a newer runtime, and the troubleshooting page says `ReferenceError: crypto is not defined` means an older one.
- Copying a 1.x tutorial. If it installs `@modelcontextprotocol/sdk`, its imports will not match this lesson.

## Try it yourself

- Call `get-alerts` in the Inspector with `{ "state": "California" }` and compare the message with the one above.
- Add a `console.log("hello")` to the top of the file, run the Inspector command again and read what breaks. Then change it to `console.error`.
- Add a second tool next to the first inside `createServer` and check that the Tools tab lists both. The `get_forecast` tool in the [MCP build guide](https://modelcontextprotocol.io/docs/develop/build-server) is a ready example.

## Next: test it properly with the MCP Inspector

You called the tool once in the Inspector. [Lesson 3](https://mindket.com/learn/mcp/mcp-inspector) goes through every tab, the command line mode and what to check when a server misbehaves. If you want the Python version of this server, [lesson 1](https://mindket.com/learn/mcp/build-mcp-server-python) builds it.

For the bigger picture, [what is MCP](https://mindket.com/blog/what-is-mcp) explains how hosts, clients and servers fit together.

## Check yourself

1. Which package does the current TypeScript SDK use to build a server?

   A. `@modelcontextprotocol/sdk`
   B. `@modelcontextprotocol/client`
   C. `@modelcontextprotocol/server`

   Answer: C. The current SDK is `@modelcontextprotocol/server`, while `@modelcontextprotocol/sdk` is the older 1.x line that older tutorials install.

2. The host of your stdio server reports `SyntaxError: Unexpected token ... is not valid JSON`. What is the likely cause?

   A. A `console.log` call wrote to stdout, which is the protocol channel
   B. The server was started with `npx tsx` instead of a build step
   C. The Zod schema was written instead of a JSON Schema

   Answer: A. On stdio, stdout carries the protocol, so a stray line breaks it and you log with `console.error` instead.

3. A model calls `get-alerts` with `{ "state": "California" }`. What happens?

   A. Your handler runs and turns it into `CA`
   B. The Inspector converts it to a state code first
   C. The handler runs and throws a `TypeError`
   D. The SDK rejects it before your handler runs

   Answer: D. The SDK checks the arguments against `inputSchema` first, and the model reads the validation message.

4. What decides whether a model calls your tool at all?

   A. The code inside your handler
   B. The tool's name, its description and its input schema
   C. The order in which you register your tools

   Answer: B. The model never sees your code, only the name, the description and the JSON Schema built from `inputSchema`.

## Frequently asked questions

### Can I still use the old @modelcontextprotocol/sdk package?

For now. It is the 1.x line, and the README says it keeps getting bug fixes and security updates for at least six months after v2 shipped on 27 July 2026. New code should start on `@modelcontextprotocol/server`.

### What does the build guide's tsc setup add?

It compiles `src` into a `build` folder, and its package.json gains a `bin` entry, a `files` list and a `build` script that runs `tsc`. Hosts then run `node` on `build/index.js`, as in the guide's Claude Desktop config.

### Do I have to use Zod for the input schema?

No. The README says tool and prompt schemas use Standard Schema, so Zod v4, Valibot, ArkType or any compatible library works. The tutorial uses Zod, and two copies of it in one dependency tree cause type errors.

### Can I run the SDK on Bun or Deno?

Yes. The README says the SDK runs on Node.js, Bun and Deno. Install it with `bun add @modelcontextprotocol/server` or `deno add npm:@modelcontextprotocol/server`.

### How do I serve the same server over HTTP?

Hand the same `createServer` factory to the SDK's HTTP serving instead of `serveStdio`. The tutorial says to do that when one endpoint should serve many clients. The SDK also publishes small adapter packages for Express, Fastify, Hono and Node's own HTTP server.

### How do I log from a stdio server?

Use `console.error`, which writes to stderr, or a logging library that writes to stderr or files. A host captures a stdio server's stderr on its own, so nothing needs wiring up.

## Sources

- [MCP TypeScript SDK: README](https://github.com/modelcontextprotocol/typescript-sdk) (read 2026-11-04)
- [MCP TypeScript SDK docs: Build your first server](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/get-started/first-server.md) (read 2026-11-04)
- [MCP TypeScript SDK docs: Plug into a real host](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/get-started/real-host.md) (read 2026-11-04)
- [MCP TypeScript SDK docs: Troubleshooting](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/troubleshooting.md) (read 2026-11-04)
- [MCP docs: Build an MCP server](https://modelcontextprotocol.io/docs/develop/build-server) (read 2026-11-04)
- [MCP docs: MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) (read 2026-11-04)
- [MCP docs: Debugging](https://modelcontextprotocol.io/docs/2026-07-28/tools/debugging) (read 2026-11-04)
- [MCP Inspector: README](https://github.com/modelcontextprotocol/inspector) (read 2026-11-05)
- [MCP TypeScript SDK release v2.4.0](https://github.com/modelcontextprotocol/typescript-sdk/releases/tag/v2.4.0) (read 2026-11-05)
- [David Soria Parra on X: the MCP 2026-07-28 release](https://x.com/dsp_/status/2082173429399142616) (read 2026-11-05)
- [David Soria Parra on X: the SDK releases that support the spec](https://x.com/dsp_/status/2082225962402402384) (read 2026-11-05)

[Browse MCP servers](https://mindket.com/mcp-servers)
