Build a minimal MCP server with the TypeScript SDK's McpServer, registerTool and stdio
DrFritzi · Reviewed · Updated 28 Sept 2026 · Markdown
Answer
Install @modelcontextprotocol/server and zod. Create an McpServer, register a tool with server.registerTool(name, { description, inputSchema }, handler), and hand a factory for the server to serveStdio. Then test it with npx @modelcontextprotocol/inspector npx tsx src/index.ts. This page uses SDK v2, which implements MCP specification 2026-07-28 and still serves 2025-era clients over stdio by default. The older v1 package @modelcontextprotocol/sdk is covered in the version note at the end.
Details
1. Set up the project
The SDK ships ES modules only and needs Node.js 20 or later.
mkdir hello-mcp && cd hello-mcp
npm init -y
npm pkg set type=module
npm install @modelcontextprotocol/server zod tsx
mkdir src
2. Write the server (src/index.ts)
import { McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';
function createServer(): McpServer {
const server = new McpServer({ name: 'hello', version: '1.0.0' });
server.registerTool(
'add',
{
description: 'Add two numbers',
inputSchema: z.object({ a: z.number(), b: z.number() })
},
async ({ a, b }) => ({
content: [{ type: 'text', text: String(a + b) }]
})
);
return server;
}
void serveStdio(createServer);
console.error('hello MCP server running on stdio'); // stderr, never stdout
The SDK builds the JSON Schema that the model sees from the one Zod schema. It checks arguments against that schema before your handler runs and infers the handler's argument types from it. A handler can return isError: true to report a failure that the model can read.
The SDK README's Getting Started example wires the transport by hand instead, with new StdioServerTransport() from @modelcontextprotocol/server/stdio and await server.connect(transport). The v2 stdio guide says serveStdio replaces that pattern.
3. Run it
npx tsx src/index.ts
Only the stderr banner appears. A stdio server waits on stdin until a client talks to it. Press Ctrl+C to stop it.
4. Test with the MCP Inspector
npx @modelcontextprotocol/inspector npx tsx src/index.ts
The Inspector launches your command and connects over stdio. It prints a URL with a one-time session token. Open the URL, click Connect, open the Tools tab, pick add and run it. For CI, the same package has a scriptable mode:
npx @modelcontextprotocol/inspector --cli npx tsx src/index.ts --method tools/list
The Inspector itself needs Node 22.19.0 or newer.
Verification checklist
tools/listshowsaddwith aninputSchemathat hasaandb.- Calling
addwith{"a": 2, "b": 3}returns a text block with5. - Calling it with a string for
anever reaches your handler. The call returns a normal result withisError: trueand the textInput validation error: Invalid arguments for tool add: a: Invalid input: expected number, received string. We saw this with@modelcontextprotocol/server2.1.0 and a 2025-11-25initialize.
Pitfall: console.log
stdout is the protocol channel. One console.log, whether yours or a dependency's, puts a line on it that no JSON-RPC parser accepts, and the host then fails to parse the stream. Use console.error.
Version note: v1 vs v2
v1 (@modelcontextprotocol/sdk, 1.30.x) |
v2 (@modelcontextprotocol/server, 2.x) |
|
|---|---|---|
| Import | @modelcontextprotocol/sdk/server/mcp.js, .../server/stdio.js |
@modelcontextprotocol/server, .../server/stdio |
| stdio wiring | new StdioServerTransport() + server.connect(transport) |
serveStdio(factory) |
inputSchema |
Raw shape: { a: z.number() } |
Schema object: z.object({ a: z.number() }) |
v1 remains on the v1.x branch and gets bug fixes and security updates for at least six months after v2's release. If you copy a snippet from an older tutorial, check which of these three columns it matches.