Skip to content

AI agents and MCP · 8 min read

Build an MCP Server in TypeScript (Tested Step by Step)

A tested tutorial for building an MCP server in TypeScript with the official SDK: define a tool with Zod, serve it over stdio and Streamable HTTP, test it with the Inspector, register it in Claude Code, and see what to add before production.

By Published Updated

A pair of oversized curly braces around lines of abstract code, with a terminal prompt strip below.

Build an MCP server in TypeScript: what you will end up with

By the end you will have a TypeScript MCP server with one tool, get-order-status. It runs over stdio, passes a scripted client test, shows up in the MCP Inspector and in Claude Code, and the same code can be served over Streamable HTTP. The tool reads an in-memory map, so there are no external dependencies; swap the map for a call to your own API and you have the shape of a real server. If the term is new, what an MCP server is explains the roles first.

Which package: @modelcontextprotocol/server or @modelcontextprotocol/sdk?

As documented on 2026-10-14, the main branch of the official repository is v2 of the SDK, the stable line, published as split packages: @modelcontextprotocol/server (2.3.1 on npm), @modelcontextprotocol/client (2.3.1) and an optional @modelcontextprotocol/node adapter (2.1.1). v2 implements the 2026-07-28 protocol revision.

The single package @modelcontextprotocol/sdk (1.32.1 on npm) is the v1.x maintenance line. The repository says it implements the spec up to 2025-11-25 and will keep receiving bug and security fixes for at least six months after the v2 release on 2026-07-27. This tutorial uses v2 and shows the v1 equivalent near the end. Everything below was run on Node.js 24.3.0 with TypeScript 7.0.2, zod 4.6.7 and tsx 4.23.15.

What do you need installed?

  • Node.js 20 or later. The v2 packages declare node >=20.
  • Node.js 22.19 or later if you want the MCP Inspector, whose npm package declares node >=22.19.0.
  • Basic TypeScript. Tool inputs are described with Zod.

Step 1: Create the project

mkdir orders-mcp && cd orders-mcp
npm init -y
npm pkg set type=module
npm install @modelcontextprotocol/server zod tsx
npm install -D typescript @types/node
mkdir src

type=module matters because the SDK ships ES modules only. tsx runs TypeScript directly, so there is no build step while you experiment. To type-check or build, this tsconfig.json passed in testing:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "skipLibCheck": true,
    "types": ["node"],
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src"]
}

With TypeScript 7.0.2 the Node types were not picked up without the types entry, and NodeNext is why the imports below end in .js.

Step 2: Define the server and one tool

Create src/server.ts:

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

// Demo data. In a real server this would call your own API or database.
const orders = new Map([
    ['A-1001', { status: 'shipped', total: 42.5 }],
    ['A-1002', { status: 'processing', total: 18.0 }]
]);

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

    server.registerTool(
        'get-order-status',
        {
            description: 'Look up the status and total of one order by its id',
            inputSchema: z.object({
                orderId: z.string().describe('Order id, for example A-1001')
            })
        },
        async ({ orderId }) => {
            const order = orders.get(orderId);
            if (!order) {
                return {
                    content: [{ type: 'text', text: `No order ${orderId}. Known ids: ${[...orders.keys()].join(', ')}` }],
                    isError: true
                };
            }
            return {
                content: [{ type: 'text', text: `${orderId}: ${order.status}, total ${order.total}` }]
            };
        }
    );

    return server;
}

Four things are happening.

  1. A factory. The server is built inside createServer. Both transports below take a factory, and the HTTP handler calls it once per request, so register everything inside it.
  2. registerTool(name, config, handler). The name and description are what the model reads when it decides whether to call the tool. Write them for a model.
  3. One schema. The Zod inputSchema is the only schema you write. The SDK turns it into the JSON Schema clients see in tools/list, validates arguments before your handler runs, and infers the handler’s types. The .describe() text becomes the parameter’s description.
  4. Errors the model can use. A missing order returns a normal result with isError: true and a message naming the valid ids, so the model can retry. The SDK also converts an exception thrown in a handler into the same isError: true shape.

Step 3: Serve it over stdio

Create src/stdio.ts:

import { serveStdio } from '@modelcontextprotocol/server/stdio';
import { createServer } from './server.js';

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

serveStdio reads requests on standard input, writes responses to standard output and calls your factory to build the server. Run it with npx tsx src/stdio.ts. It prints the banner and waits, which is correct: a stdio server waits for a client to speak first.

The banner uses console.error on purpose. The specification says a stdio server must not write anything to standard output that is not a valid MCP message, so a single console.log corrupts the connection.

Step 4: How do you test an MCP server without an AI app?

The MCP Inspector launches your server command and lets you call tools directly. In the browser version, click Connect, open the Tools tab, pick get-order-status and run it with A-1001:

npx @modelcontextprotocol/inspector npx tsx src/stdio.ts

Its command-line mode needs no browser:

npx @modelcontextprotocol/inspector --cli npx tsx src/stdio.ts --method tools/list
npx @modelcontextprotocol/inspector --cli npx tsx src/stdio.ts --method tools/call --tool-name get-order-status --tool-arg orderId=A-1001

The first command prints the tool with the input schema generated from your Zod object. The second prints A-1001: shipped, total 42.5.

To test from code, install the client package (npm install @modelcontextprotocol/client) and create src/client-test.ts:

import { Client } from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';

const client = new Client({ name: 'test', version: '1.0.0' });
await client.connect(new StdioClientTransport({ command: 'npx', args: ['tsx', 'src/stdio.ts'] }));

console.log((await client.listTools()).tools.map(t => t.name));
console.log(await client.callTool({ name: 'get-order-status', arguments: { orderId: 'A-1002' } }));
console.log(await client.callTool({ name: 'get-order-status', arguments: { orderId: 'nope' } }));

await client.close();

Run it with npx tsx src/client-test.ts. The second call returns the text A-1002: processing, total 18. The third returns isError: true with the list of known ids.

Step 5: Register the server in a client

Every host launches a stdio server from a command and arguments. For Claude Code, the SDK documentation gives this command, run from the project root:

claude mcp add orders -- npx tsx src/stdio.ts

Run /mcp in a Claude Code session in that directory to confirm orders is connected. VS Code reads a .vscode/mcp.json file with a stdio entry holding the same command and arguments, and Cursor reads .cursor/mcp.json. Then ask “what is the status of order A-1001?” Nothing in the prompt names the tool. The model picks it from the name, description and schema, and the specification recommends that hosts let you deny the call.

For a built copy, run npx tsc and register node dist/stdio.js instead of the tsx command.

Step 6: Serve the same server over Streamable HTTP

Stdio suits a server a host launches on your machine. To run one endpoint that many clients connect to, serve the same factory over Streamable HTTP. Install the Node adapter with npm install @modelcontextprotocol/node, then create src/http.ts:

import { createServer as createHttpServer } from 'node:http';
import { createMcpHandler } from '@modelcontextprotocol/server';
import { localhostHostValidation, localhostOriginValidation, toNodeHandler } from '@modelcontextprotocol/node';
import { createServer } from './server.js';

const handler = createMcpHandler(createServer);
const nodeHandler = toNodeHandler(handler);
const validateHost = localhostHostValidation();
const validateOrigin = localhostOriginValidation();

createHttpServer((req, res) => {
    if (!validateHost(req, res) || !validateOrigin(req, res)) return;
    void nodeHandler(req, res);
}).listen(3000, '127.0.0.1', () => {
    console.error('orders MCP server on http://127.0.0.1:3000/mcp');
});

Run npx tsx src/http.ts, then connect with the client package:

import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';

const client = new Client({ name: 'test', version: '1.0.0' });
await client.connect(new StreamableHTTPClientTransport(new URL('http://127.0.0.1:3000/mcp')));
console.log(await client.callTool({ name: 'get-order-status', arguments: { orderId: 'A-1001' } }));
await client.close();

Three details matter.

  • The factory runs per request. createMcpHandler builds a fresh server for every HTTP request and holds nothing between requests, so the endpoint is stateless and can scale horizontally. Create connection pools once at module scope and close over them.
  • The handler checks no Host, Origin or token. The SDK documentation says you add those checks in front of it. The two localhost*Validation guards do that for a local bind, and the specification requires servers to validate the Origin header to prevent DNS rebinding. In testing, a POST carrying an unrelated Origin header got a 403 before reaching the handler.
  • Bind to loopback while developing. The specification says a locally running server should bind to localhost, which is why the example listens on 127.0.0.1.

Express, Hono and Fastify have helper packages that apply the header checks by default on localhost binds.

What to add before this leaves your laptop

A tutorial server is not a production server. At minimum:

  • Authenticate remote access. For HTTP, the specification describes OAuth-based authorization in which your server accepts only tokens issued for it. The SDK’s authorization guide covers a bearer-token gate, but it never issues tokens: you supply the identity provider and a function that verifies each token.
  • Limit and sanitise. Schema validation is done for you. The specification also lists rate limits, access controls and output sanitising as server obligations, and those are yours.
  • Choose the identity. Give the downstream API a scoped credential, not an admin one.
  • Preview writes. For a tool that changes data, consider a dry-run default and an audit log, and keep read and write tools separate so a host can treat them differently.

What a hosted server should expose is a design question: MCP vs REST API covers where the line sits. Hosting it is ordinary cloud and DevOps work.

If your project is on the v1 SDK

A server with the same tool on @modelcontextprotocol/sdk 1.32.1 looks like this, with the handler body shortened:

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: 'orders', version: '1.0.0' });

server.registerTool(
    'get-order-status',
    {
        description: 'Look up the status and total of one order by its id',
        inputSchema: { orderId: z.string().describe('Order id, for example A-1001') }
    },
    async ({ orderId }) => ({ content: [{ type: 'text', text: `Looked up ${orderId}` }] })
);

await server.connect(new StdioServerTransport());

The differences are the import paths, server.connect(transport) in place of serveStdio, and inputSchema taking a plain object of Zod fields. The HTTP side differs more. The repository publishes a migration guide to v2.

Building this against your own systems

Vexio builds custom MCP servers for teams that want an assistant to work against their own systems. The pattern is the one above, applied to real data: tools designed around tasks, a scoped identity, a dry-run default on anything that writes, and an audit log. If you are weighing a build against hiring, how to choose an AI agent development company and what drives custom AI agent cost help frame it. See the MCP server development page or request a demo.

Sources

Checked on 2026-10-14. Code run locally on Node.js 24.3.0 with @modelcontextprotocol/server 2.3.1, @modelcontextprotocol/node 2.1.1, @modelcontextprotocol/client 2.3.1, zod 4.6.7, tsx 4.23.15 and typescript 7.0.2, plus @modelcontextprotocol/sdk 1.32.1 for the v1 snippet.

FAQ

Questions this guide answers.

As checked on 2026-10-14, the stable line is v2, published as @modelcontextprotocol/server, with @modelcontextprotocol/client for clients and @modelcontextprotocol/node for Node HTTP. The older single package, @modelcontextprotocol/sdk (v1.x), is the maintenance line and still receives bug and security fixes. Check the repository README for its status.

On the stdio transport, standard output is the protocol channel, and the specification says a server must not write anything to it except valid MCP messages. A single console.log line corrupts the stream. Log to standard error with console.error instead, which the specification allows for any logging.

Use the MCP Inspector, which launches your server command and lets you list and call tools in a browser or from the command line, for example npx @modelcontextprotocol/inspector npx tsx src/stdio.ts. You can also write a short script with the SDK's client package and call tools directly.

In the v2 SDK, tool schemas use the Standard Schema interface, so Zod v4, Valibot, ArkType or another compatible library can be used; the official examples use Zod. The v1 SDK lists zod as a required peer dependency. Either way, the schema is turned into the JSON Schema that clients see.

Use stdio when a host on the same machine launches your server as a subprocess. Use Streamable HTTP when you want one endpoint that many clients reach over a network, and then add authentication and Origin validation in front of the handler, because the SDK handler does not check them for you.

As checked on 2026-10-14, the v2 server, client and node packages declare Node.js 20 or later, and the official tutorial says the same. The MCP Inspector declares Node.js 22.19 or later. The v1 SDK declares Node.js 18 or later, so confirm which line your project uses.

Further reading
  • AI agents and MCP · 14 Oct 2026 · 9 min

    What is an MCP server? A plain-English explanation

    An MCP server lets an AI assistant reach a system it cannot see on its own. See what it exposes, how a tool call flows, how local and remote servers differ, and what the specification says about security.

    Read the article →

  • AI agents and MCP · 14 Oct 2026 · 10 min

    MCP vs REST API: Differences and When to Use Each

    Keep the REST API for developers and add an MCP server when AI applications need to use the system. A short decision table, the differences in discovery, errors and transport, a worked example and a design checklist.

    Read the article →

  • Custom software · 14 Oct 2026 · 10 min

    Custom AI agent development: process and cost factors

    The factors that drive the cost of custom AI agent development, how tool-calling loops and token pricing affect run cost, the six phases from scoping to operation, and how to estimate run cost from your own prototype.

    Read the article →

  • Custom software · 14 Oct 2026 · 9 min

    How to choose an AI agent development company

    A buyer's checklist for hiring an AI agent development company: six things to ask for in the first call, a score sheet, questions on evaluation, security and IP, red flags, and how to scope a pilot.

    Read the article →

Talk to us

Want this applied to your own case?

Tell us what you run or want to build. We reply by email to arrange a demo or a scoping conversation.

WhatsApp
Message us
Web
vexio.ai