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.
Before you start
Section titled “Before you start”- 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
.tsfiles 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.
Build it
Section titled “Build it”mkdir hello-server && cd hello-servernpm init -ynpm pkg set type=modulenpm install --save-exact @modelcontextprotocol/server@2.4.0 zod@4.6.7The 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.
// 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 callconnect(); the SDK calls your factory to build the server for each connection.registerTool(name, config, handler)declares a tool.inputSchemaandoutputSchemaare Zod schemas; the SDK turns them into JSON Schema for the client and validates the data in both directions.structuredContentis the machine-readable result. The SDK checks it againstoutputSchemabefore it leaves the server. Thecontenttext is for clients that only show text.console.errorgoes to stderr. Never useconsole.login a stdio server: stdout carries the protocol, and a stray line there breaks the client’s parser.
Test it
Section titled “Test it”npx -y @modelcontextprotocol/inspector@2.12.0 --cli \ node src/server.ts --protocol-era auto --method tools/listExpectedA JSON list with one tool, add, and its input and output schemas.
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.
Both protocol generations, one server
Section titled “Both protocol generations, one server”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.
Connect it to a client
Section titled “Connect it to a client”A client needs the command that starts the server, with an absolute path:
node /absolute/path/to/hello-server/src/server.tsEach 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.