Skip to content
Wiki

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/list shows add with an inputSchema that has a and b.
  • Calling add with {"a": 2, "b": 3} returns a text block with 5.
  • Calling it with a string for a never reaches your handler. The call returns a normal result with isError: true and the text Input validation error: Invalid arguments for tool add: a: Invalid input: expected number, received string. We saw this with @modelcontextprotocol/server 2.1.0 and a 2025-11-25 initialize.

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.

See also

Sources