Build an MCP Server with Node SDK: TypeScript Guide

By Yanni Papoutsis | 4 min read | 2026-06-27

TL;DR: The official MCP SDK for Node.js (@modelcontextprotocol/sdk) lets you build MCP servers in TypeScript. It is lower-level than FastMCP but gives you full control over the protocol. This guide covers installation, defining tools with Zod schemas, setting up resources, handling errors, and connecting to Claude. Includes working code you can adapt for any Node.js-compatible API.

Prerequisites

Step 1: Project Setup

mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
npx tsc --init

Update tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true
  },
  "include": ["src/**/*"]
}

Step 2: Create the Server

Create src/index.ts:

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";

// Define your tool input schemas with Zod
const AddNumbersSchema = z.object({
  a: z.number().describe("First number"),
  b: z.number().describe("Second number"),
});

const GreetUserSchema = z.object({
  name: z.string().describe("The user's name"),
  formal: z.boolean().optional().default(false).describe("Use formal greeting"),
});

// Create the server
const server = new Server(
  {
    name: "my-mcp-server",
    version: "1.0.0",
  },
  {
    capabilities: {
      tools: {},
    },
  }
);

// Register tools list handler
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "add_numbers",
        description: "Add two numbers together and return the result.",
        inputSchema: {
          type: "object",
          properties: {
            a: { type: "number", description: "First number" },
            b: { type: "number", description: "Second number" },
          },
          required: ["a", "b"],
        },
      },
      {
        name: "greet_user",
        description: "Generate a greeting for a user.",
        inputSchema: {
          type: "object",
          properties: {
            name: { type: "string", description: "The user's name" },
            formal: {
              type: "boolean",
              description: "Use formal greeting (default: false)",
            },
          },
          required: ["name"],
        },
      },
    ],
  };
});

// Register tool call handler
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  try {
    switch (name) {
      case "add_numbers": {
        const { a, b } = AddNumbersSchema.parse(args);
        const result = a + b;
        return {
          content: [{ type: "text", text: String(result) }],
        };
      }

      case "greet_user": {
        const { name: userName, formal } = GreetUserSchema.parse(args);
        const greeting = formal
          ? `Good day, ${userName}. How may I assist you?`
          : `Hey ${userName}! What is up?`;
        return {
          content: [{ type: "text", text: greeting }],
        };
      }

      default:
        throw new Error(`Unknown tool: ${name}`);
    }
  } catch (error) {
    return {
      content: [
        {
          type: "text",
          text: `Error: ${error instanceof Error ? error.message : String(error)}`,
        },
      ],
      isError: true,
    };
  }
});

// Start the server
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("MCP server running on stdio");
}

main().catch((error) => {
  console.error("Fatal error:", error);
  process.exit(1);
});

Step 3: Add a package.json Script

{
  "scripts": {
    "build": "tsc",
    "dev": "tsx src/index.ts",
    "start": "node dist/index.js"
  }
}

Test locally:

npm run dev

The server starts and waits on stdio. You will not see output until Claude connects.

Step 4: Connect to Claude

Add to ~/.claude/mcp_servers.json:

{
  "mcpServers": {
    "my-node-server": {
      "command": "node",
      "args": ["/absolute/path/to/my-mcp-server/dist/index.js"],
      "env": {}
    }
  }
}

Or for development without building:

{
  "mcpServers": {
    "my-node-server": {
      "command": "npx",
      "args": ["tsx", "/absolute/path/to/my-mcp-server/src/index.ts"],
      "env": {}
    }
  }
}

Restart Claude. Test:

Use my-node-server to add 10 and 32.

Step 5: Real API Integration Example

import fetch from "node-fetch";

// Add to your tool list:
// { name: "get_weather", description: "...", inputSchema: {...} }

case "get_weather": {
  const schema = z.object({
    city: z.string().describe("City name"),
    units: z.enum(["metric", "imperial"]).default("metric"),
  });
  const { city, units } = schema.parse(args);

  const apiKey = process.env.WEATHER_API_KEY;
  if (!apiKey) throw new Error("WEATHER_API_KEY not set");

  const response = await fetch(
    `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(city)}&units=${units}&appid=${apiKey}`
  );

  if (!response.ok) {
    throw new Error(`Weather API error: ${response.status}`);
  }

  const data = await response.json() as any;
  return {
    content: [{
      type: "text",
      text: JSON.stringify({
        city: data.name,
        temp: data.main.temp,
        feels_like: data.main.feels_like,
        description: data.weather[0].description,
        humidity: data.main.humidity,
      }, null, 2),
    }],
  };
}

Pass the API key in MCP config:

{
  "env": {
    "WEATHER_API_KEY": "your-key-here"
  }
}

FastMCP vs Node SDK: Which to Use?

Factor FastMCP (Python) Node SDK (TypeScript)
Speed to first tool 5 minutes 15-20 minutes
Boilerplate Minimal More (handlers, schemas)
Type safety Python type hints Full TypeScript
Ecosystem Python libraries npm ecosystem
Performance Good Good
Best for Data science, ML, scripting Web APIs, JS ecosystem integration

Choose FastMCP if you want the fastest path to a working server. Choose the Node SDK if your target APIs are in the npm ecosystem or you want strict TypeScript throughout.

Summary

The Node SDK gives you a solid TypeScript foundation for any MCP server. The boilerplate is heavier than FastMCP but the type safety and npm ecosystem integration make it worth it for production servers. The key pattern is: define schemas with Zod, return { content: [{ type: "text", text: ... }] } from every tool, and use isError: true for error responses.

See also: Python FastMCP version of this guide