Build Your First MCP Server in TypeScript

In this tutorial you'll go from an empty folder to a working MCP server connected to Claude in about 30 minutes. We'll build a small "dev utilities" server that exposes one genuinely useful tool, test it with the MCP Inspector, and wire it into a real client.

Prerequisites: Node.js 18+, basic TypeScript, and an MCP client (Claude Desktop, Claude Code, or VS Code). New to MCP? Read What Is MCP? first — it's 5 minutes.

Step 1 — Scaffold the project

mkdir devutils-mcp && cd devutils-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node

Add a minimal tsconfig.json:

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

And in package.json, set the module type and a build script:

{
  "type": "module",
  "scripts": { "build": "tsc" }
}

Step 2 — Create the server and register a tool

Create src/index.ts. We'll expose one tool: generate_id, which produces UUIDs, nano-IDs, or timestamps — something models are notoriously bad at doing reliably on their own.

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { randomUUID, randomBytes } from "node:crypto";

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

server.registerTool(
  "generate_id",
  {
    title: "Generate ID",
    description:
      "Generate unique identifiers: UUID v4, URL-safe random IDs, or Unix timestamps.",
    inputSchema: {
      kind: z.enum(["uuid", "nanoid", "timestamp"]).describe("Type of ID"),
      count: z.number().int().min(1).max(100).default(1)
        .describe("How many to generate"),
    },
  },
  async ({ kind, count }) => {
    const make = {
      uuid: () => randomUUID(),
      nanoid: () => randomBytes(12).toString("base64url"),
      timestamp: () => Date.now().toString(),
    }[kind];

    const ids = Array.from({ length: count }, make);
    return {
      content: [{ type: "text", text: ids.join("\n") }],
    };
  }
);

Three things worth noticing:

Step 3 — Connect the transport

Append this to src/index.ts:

const transport = new StdioServerTransport();
await server.connect(transport);

Gotcha: with stdio transport, never console.log() — stdout is the protocol channel, and stray output corrupts messages. Use console.error() for debugging; stderr is safe.

Build it:

npm run build

Step 4 — Test with the MCP Inspector

The Inspector is an interactive debugger for MCP servers — use it before touching any client config:

npx @modelcontextprotocol/inspector node dist/index.js

It opens a browser UI. Click Connect, open the Tools tab, and you should see generate_id. Run it with kind: "uuid", count: 3 — you should get three UUIDs back. If this works, your server is correct.

Step 5 — Connect it to Claude

Claude Desktop

Add to claude_desktop_config.json (Settings → Developer → Edit Config):

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

Claude Code

claude mcp add devutils -- node /absolute/path/to/devutils-mcp/dist/index.js

VS Code

Run MCP: Add Server from the command palette, choose stdio, and give it the same command.

Restart the client, then ask: "Generate 5 UUIDs for my test fixtures." The model will call your tool and return real UUIDs — generated by your code, not hallucinated.

Where to go from here