Skip to content

Your first stateless MCP server in TypeScript

Spec
2026-07-28
Evidence
E2Reproducedarm64
TypeScript SDK (server)
@modelcontextprotocol/server@2.4.0
MCP Inspector
@modelcontextprotocol/inspector@2.12.0
Verified

This guide builds a server with one tool, add, that takes two numbers and returns their sum as structured data. It is small on purpose: once it works, every other feature is a matter of adding more tools.

  • Node.js 22.19 or later. The SDK itself needs Node 20, but the MCP Inspector used for testing needs 22.19. Node 22.18 and later also run .ts files directly, so there is no build step in this guide. We tested on Node 24.21.
  • A terminal. No MCP client is needed until the last step.
TerminalCreate the project and install the SDK
Terminal window
mkdir hello-server && cd hello-server
npm init -y
npm pkg set type=module
npm install --save-exact @modelcontextprotocol/server@2.4.0 zod@4.6.7

The package is @modelcontextprotocol/server@2.4.0. Version 2 split the old @modelcontextprotocol/sdk into separate client and server packages, so tutorials that install @modelcontextprotocol/sdk are describing version 1.

EditorWrite src/server.ts
src/server.ts
// The smallest useful MCP server: one tool with typed input and structured output.
// Every example in examples/mcp/ exposes the same `add` tool so the harness can
// compare SDKs with the same calls.
import { McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod';
serveStdio(() => {
const server = new McpServer({ name: 'hello-ts', version: '1.0.0' });
server.registerTool(
'add',
{
description: 'Add two numbers and return the sum.',
inputSchema: z.object({ a: z.number(), b: z.number() }),
outputSchema: z.object({ sum: z.number() }),
},
async ({ a, b }) => {
const output = { sum: a + b };
return { content: [{ type: 'text', text: JSON.stringify(output) }], structuredContent: output };
},
);
return server;
});
// stdout carries JSON-RPC; anything meant for humans goes to stderr.
console.error('hello-ts: ready on stdio');

What each part does:

  • serveStdio(factory) runs the server over stdio. You do not create a transport or call connect(); the SDK calls your factory to build the server for each connection.
  • registerTool(name, config, handler) declares a tool. inputSchema and outputSchema are Zod schemas; the SDK turns them into JSON Schema for the client and validates the data in both directions.
  • structuredContent is the machine-readable result. The SDK checks it against outputSchema before it leaves the server. The content text is for clients that only show text.
  • console.error goes to stderr. Never use console.log in a stdio server: stdout carries the protocol, and a stray line there breaks the client’s parser.
TerminalList the tools with the Inspector CLI
Terminal window
npx -y @modelcontextprotocol/inspector@2.12.0 --cli \
node src/server.ts --protocol-era auto --method tools/list

ExpectedA JSON list with one tool, add, and its input and output schemas.

TerminalCall the tool
Terminal window
npx -y @modelcontextprotocol/inspector@2.12.0 --cli \
node src/server.ts --protocol-era auto \
--method tools/call --tool-name add --tool-arg a=2 b=3 --format json
{"result":{"_meta":{"io.modelcontextprotocol/serverInfo":{"name":"hello-ts","version":"1.0.0"}},
"content":[{"type":"text","text":"{\"sum\":5}"}],"structuredContent":{"sum":5}}}

ExpectedstructuredContent is {"sum": 5}, and _meta names the server hello-ts.

The _meta field with serverInfo only appears on the 2026-07-28 protocol. Run the same command with --protocol-era legacy and it disappears, because the Inspector then connects with the 2025-11-25 handshake. The Inspector uses legacy by default when you give it a command directly, which is why this guide passes auto.

We connected the finished server with the TypeScript client SDK in three ways:

Client setting Negotiated version
Default (no versionNegotiation) 2025-11-25
versionNegotiation: { mode: 'auto' } 2026-07-28
versionNegotiation: { mode: { pin: '2026-07-28' } } 2026-07-28

You do not write anything extra for older clients; the SDK handles both. The reverse is worth knowing if you write clients: the TypeScript client SDK v2 still uses the 2025-11-25 handshake unless you set versionNegotiation.

A client needs the command that starts the server, with an absolute path:

Terminal window
node /absolute/path/to/hello-server/src/server.ts

Each client stores that command in its own format. See Codex. If the client reports Connection closed, see the error entry.

Sources

MCPDev is community documentation. It is not affiliated with the Model Context Protocol project or with the vendors of the clients it covers.