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).
mkdir quote-mcp && cd quote-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D tsx typescript @types/nodeSet "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.
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
- Imports:
McpServeris the high-level builder.StdioServerTransporthandles JSON-RPC over stdin/stdout. Zod gives us the input schema validator. - Server instantiation:
new McpServer(...)with a name and version. The host will display these to the user. - registerTool: declares the tool. The middle object has metadata (title, description) and an
inputSchemabuilt from Zod fields. The model uses the description to decide when to call. - Handler: an async function that receives the validated input. It returns an object with a
contentarray. Each item is a typed chunk (text,image,resource). The SDK serializes this back to JSON-RPC. - Transport:
connect(transport)starts the read loop. The process now sits there listening on stdin.
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.
# 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.tsThe 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:
{
"mcpServers": {
"quote-of-the-day": {
"command": "npx",
"args": ["tsx", "/absolute/path/to/quote-mcp/server.ts"]
}
}
}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.
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. Use
server.registerResource()with a URI template. - Prompts: register reusable templates with
server.registerPrompt(). These appear as slash commands in many hosts. - Streamable HTTP: swap
StdioServerTransportfor 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.
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
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 typedcontentarray. StdioServerTransportfor local servers, Streamable HTTP transport for remote ones.- Connect to Claude Desktop via
claude_desktop_config.jsonwith absolute paths. - Use the MCP Inspector during development. It is the fastest feedback loop you will have.