> Saurabh Talele
downloadDownload Resume
MCP From Scratch
Back to Archive

How to Build an MCP Server in Node.js and TypeScript (Step-by-Step)

Published
June 21, 2026

A beginner-friendly, step-by-step guide to building your first Model Context Protocol (MCP) server using Node.js and TypeScript — with working code you can run in minutes.

If you've used Claude, Cursor, or any AI coding assistant lately, you've probably seen the term MCP thrown around a lot. MCP — short for Model Context Protocol — is quickly becoming the standard way for AI models to talk to external tools, files, and APIs.

The good news? Building your own MCP server is a lot simpler than it sounds. In this guide, you'll build a working MCP server from scratch using Node.js and TypeScript, in well under an hour.

What Is an MCP Server, in Plain English?

Think of an MCP server as a small, well-defined API — except instead of a frontend calling it, an AI model calls it.

Without MCP, every AI tool that wants to talk to GitHub, a database, or a weather API needs custom, one-off integration code. MCP standardizes that connection. You write one MCP server, and any MCP-compatible AI client (Claude Desktop, Claude Code, Cursor, and others) can use it immediately, with no extra glue code.

An MCP server typically exposes one or more of:

  • Tools — functions the model can call (e.g., "search inventory", "create a ticket")
  • Resources — data the model can read (e.g., files, database rows)
  • Prompts — reusable prompt templates

For this tutorial, we'll focus on tools, since that's the most common and most useful starting point.

What You'll Need

  • Node.js 18 or later
  • Basic TypeScript familiarity
  • An editor (VS Code works fine)
  • 30–45 minutes

Step 1: Set Up the Project

typescript
mkdir mcp-dice-server
cd mcp-dice-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx

Create a minimal tsconfig.json:

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

Step 2: Write the Server

We'll build something deliberately simple: a dice-rolling tool. Simple enough to fully understand in one sitting, but the pattern scales directly to real tools — database lookups, API calls, file operations, anything.

Create src/index.ts:

typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({
  name: "dice-server",
  version: "1.0.0",
});

server.registerTool(
  "roll_dice",
  {
    title: "Roll Dice",
    description: "Rolls one or more dice and returns the results",
    inputSchema: {
      sides: z.number().int().min(2).default(6),
      count: z.number().int().min(1).max(20).default(1),
    },
  },
  async ({ sides, count }) => {
    const rolls = Array.from(
      { length: count },
      () => Math.floor(Math.random() * sides) + 1
    );

    return {
      content: [
        {
          type: "text",
          text: `Rolled ${count}d${sides}: [${rolls.join(", ")}] (total: ${rolls.reduce((a, b) => a + b, 0)})`,
        },
      ],
    };
  }
);

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

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

That's it — that's a complete, working MCP server. Walking through what's happening:

  1. McpServer is the core server instance — give it a name and version.
  2. registerTool defines a callable function, with a Zod schema describing its inputs. The AI model reads this schema to know what arguments it can pass.
  3. StdioServerTransport wires the server up to communicate over stdin/stdout, which is how most local MCP clients (like Claude Desktop) talk to servers.

Step 3: Run It

Add a script to package.json:

typescript
{
  "scripts": {
    "start": "tsx src/index.ts"
  }
}

Run it:

npm start

You won't see much output — that's expected. The server is now listening on stdio, waiting for an MCP client to connect.

Step 4: Connect It to an MCP Client

To test with Claude Desktop, add this to your claude_desktop_config.json:

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

Restart Claude Desktop, and you should see "dice-server" listed under available tools. Try asking: "Roll three six-sided dice." The model will call your roll_dice tool directly and return real results — not a guess.

Common Pitfalls (and How to Avoid Them)

  • Relative paths in config — MCP client configs need absolute paths to your script, not relative ones.
  • Console logging breaks stdio transport — never use console.log inside an MCP server using stdio transport; it corrupts the protocol stream. Use console.error for debug output instead, since stderr is left untouched.
  • Schema mismatches — if the model's tool call fails silently, double-check that your Zod schema matches what you're destructuring in the handler.
  • Forgetting to await server.connect() — without this, the server process exits immediately after starting.

Where to Go From Here

Once this pattern clicks, the same structure extends to genuinely useful tools:

  • Wrapping an internal REST API as an MCP tool
  • Exposing a database query as a "resource"
  • Building a GitHub issue-creation tool for your team's workflow
  • Connecting MCP to a CI/CD pipeline for automated status checks

The dice example is intentionally trivial — the real value of MCP is that this exact same registerTool pattern is how production-grade integrations get built too.

FAQ

Is MCP only for Claude? No. MCP is an open protocol. Any client that implements the MCP spec — including several AI coding tools — can connect to your server.

Do I need to use TypeScript? No, MCP also has an official Python SDK. TypeScript is used here because of strong tooling and type-safety for tool schemas.

Can an MCP server call external APIs? Yes — inside a tool handler, you can make any network call you'd make in normal Node.js code (fetch a REST API, query a database, etc.).

Is this production-ready? The pattern is. For production, add proper error handling, input validation, logging (via stderr or a file, not stdout), and authentication if the server is exposed beyond local use.

Wrapping Up

MCP servers are simpler to build than the buzz around them suggests. With one tool, one schema, and a transport connection, you have a fully functioning server an AI model can call directly. From here, swapping the dice logic for a real API call is a small step — the architecture stays exactly the same.

Available for projects

Ready to build the next system?

Currently accepting high-impact opportunities in SaaS architecture and AI-driven products.