webdev.complete
🔌 Model Context Protocol (MCP)
The Modern Frontier
Lesson 114 of 117
35 min

Build an MCP Server

TypeScript SDK. Tools, resources, prompts. stdio and HTTP transport.

Time to stop talking about MCP and build one. By the end of this lesson you will have a complete TypeScript MCP server that exposes a single tool, runs over stdio, and is connected to Claude Desktop. Total code: under 50 lines. Total time: 10 minutes. The exact same pattern scales to production servers with dozens of tools.

What we're building

A quote-of-the-day MCP server. It exposes one tool called get_quote that returns an inspirational quote. The model can request a quote at any time, optionally filtered by topic. Boring? Yes. Educational? Also yes. You will see every moving part of an MCP server without drowning in business logic.

Step 1: project setup

Make a folder, init a Node project, and install the official SDK plus Zod (used for input schemas).

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

Set "type": "module" in your package.json so we can use ESM imports. Then create server.ts.

Step 2: the full server

Here it is. Read it once top to bottom, then we will walk through what each chunk does.

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

const quotes: Record<string, string[]> = {
  general: [
    "The way to get started is to quit talking and begin doing.",
    "Premature optimization is the root of all evil.",
    "Make it work, make it right, make it fast.",
  ],
  testing: [
    "If it ain't tested, it's broken.",
    "Trust, but verify with assertions.",
  ],
  debugging: [
    "Everybody knows that debugging is twice as hard as writing a program.",
    "When in doubt, console.log it out.",
  ],
};

const server = new McpServer({
  name: "quote-of-the-day",
  version: "1.0.0",
});

server.registerTool(
  "get_quote",
  {
    title: "Get a Quote",
    description:
      "Returns a quote. Optionally filter by topic: general, testing, debugging.",
    inputSchema: {
      topic: z
        .enum(["general", "testing", "debugging"])
        .optional()
        .describe("Quote category"),
    },
  },
  async ({ topic }) => {
    const list = quotes[topic ?? "general"];
    const quote = list[Math.floor(Math.random() * list.length)];
    return {
      content: [{ type: "text", text: quote }],
    };
  }
);

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

Walking through the code

  1. Imports: McpServer is the high-level builder. StdioServerTransport handles JSON-RPC over stdin/stdout. Zod gives us the input schema validator.
  2. Server instantiation: new McpServer(...) with a name and version. The host will display these to the user.
  3. registerTool: declares the tool. The middle object has metadata (title, description) and an inputSchema built from Zod fields. The model uses the description to decide when to call.
  4. Handler: an async function that receives the validated input. It returns an object with a content array. Each item is a typed chunk (text, image, resource). The SDK serializes this back to JSON-RPC.
  5. Transport: connect(transport) starts the read loop. The process now sits there listening on stdin.
Descriptions are prompts
The tool description, parameter descriptions, and even your enum values become part of the model's prompt. Write them clearly. "Fetches a quote, may filter by topic" is good. "qfn1 v3" is not.

Step 3: run it

Run it directly with tsx and try the inspector that ships with the SDK. The inspector is a browser GUI that lets you call your tools without wiring up a real host.

bash
# Run the server (it will just sit there waiting on stdin)
npx tsx server.ts

# In a second terminal, launch the inspector pointing at our server
npx @modelcontextprotocol/inspector npx tsx server.ts

The inspector opens at http://localhost:5173. You will see your get_quote tool, can click Call Tool, and watch the round trip. Use this constantly while developing. It is way faster than restarting Claude Desktop for every change.

Step 4: hook it up to Claude Desktop

Claude Desktop reads MCP server configuration from a JSON file. The location depends on your OS:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\\Claude\\claude_desktop_config.json
  • Linux: ~/.config/Claude/claude_desktop_config.json

Add an entry under mcpServers:

claude_desktop_config.json
{
  "mcpServers": {
    "quote-of-the-day": {
      "command": "npx",
      "args": ["tsx", "/absolute/path/to/quote-mcp/server.ts"]
    }
  }
}
Use absolute paths
Claude Desktop launches your server from its own working directory. Relative paths will fail mysteriously. Always use absolute paths.

Restart Claude Desktop. Open a chat. Look for the hammer icon (tools) in the message composer. You should see get_quote listed. Ask Claude: "Give me a debugging quote." It will call your tool, pass topic: "debugging", and weave the result into a reply.

Adding a second tool

Adding more is as simple as another registerTool. Tools compose: the model can call one, then another, chaining results.

ts
server.registerTool(
  "add_quote",
  {
    title: "Add a Quote",
    description: "Append a new quote to a topic.",
    inputSchema: {
      topic: z.enum(["general", "testing", "debugging"]),
      text: z.string().min(5),
    },
  },
  async ({ topic, text }) => {
    quotes[topic].push(text);
    return {
      content: [{ type: "text", text: "Added to " + topic + "." }],
    };
  }
);

Going further

  • Resources: register URIs your host can read. Useserver.registerResource() with a URI template.
  • Prompts: register reusable templates with server.registerPrompt(). These appear as slash commands in many hosts.
  • Streamable HTTP: swap StdioServerTransport for the HTTP transport when you want to deploy your server on the public internet (e.g. on Vercel or Cloudflare Workers).
  • Auth: HTTP transports support OAuth flows so users can grant scoped access to their data. Required for any non-trivial remote server.
The SDK does the heavy lifting
Behind that registerTool call is the JSON-RPC plumbing, schema validation, error handling, and capability negotiation that you would otherwise write by hand. The SDK is small but does a lot per line.

Quick quiz

Quiz1 / 3

What does an MCP server tool handler return?

Recap

  • Use @modelcontextprotocol/sdk + McpServer + Zod to build servers fast.
  • Tools are name + metadata + inputSchema + handler. The handler returns a typed content array.
  • StdioServerTransport for local servers, Streamable HTTP transport for remote ones.
  • Connect to Claude Desktop via claude_desktop_config.json with absolute paths.
  • Use the MCP Inspector during development. It is the fastest feedback loop you will have.
Built with Next.js, Tailwind & Sandpack.
Learn. Build. Ship.