MCP TypeScript SDK: Complete Developer Guide
Build, test, and deploy MCP servers with the official TypeScript SDK. Covers the McpServer class, tools, resources, prompts, transport options, error handling, and a full end-to-end weather API example.
TL;DR
- Install with
npm install @modelcontextprotocol/sdk— current stable is 1.x McpServeris the high-level class; use it over the low-levelServerclass- Define tools with
server.tool(), resources withserver.resource(), prompts withserver.prompt() - Use
StdioServerTransportfor Claude Desktop;SSEServerTransportfor web/remote clients - Test locally with MCP Inspector:
npx @modelcontextprotocol/inspector - Always handle errors with
isError: true— never throw unhandled exceptions
Installation and Project Setup
The TypeScript SDK requires Node.js 18 or later. Start with a clean TypeScript project:
Terminal
mkdir weather-mcp-server && cd weather-mcp-server npm init -y npm install @modelcontextprotocol/sdk zod npm install -D typescript @types/node tsx # Create tsconfig.json npx tsc --init --module nodenext --moduleResolution nodenext --target es2022 --strict --outDir dist
Update package.json to set the module type and add build scripts:
package.json (relevant fields)
{
"type": "module",
"scripts": {
"build": "tsc",
"dev": "tsx watch src/server.ts",
"start": "node dist/server.js",
"inspect": "npx @modelcontextprotocol/inspector tsx src/server.ts"
}
}The McpServer Class
McpServer is the recommended entry point. It handles protocol negotiation, capability advertisement, and request routing automatically. You declare your tools, resources, and prompts declaratively, and the SDK wires up the JSON-RPC 2.0 handlers.
src/server.ts — minimal skeleton
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new McpServer({
name: "weather-server", // Shown in Claude Desktop UI
version: "1.0.0",
});
// Tools, resources, and prompts go here (see below)
// Start the server
const transport = new StdioServerTransport();
await server.connect(transport);Defining Tools
Tools are functions that the AI model can call. They receive typed arguments and return content (text, images, or embedded resources). Use Zod schemas for type-safe argument validation — the SDK uses them both for TypeScript types and to generate the JSON Schema that is sent to the model.
Tool definition with Zod validation
import { z } from "zod";
server.tool(
"get_current_weather",
"Get the current weather for a city. Returns temperature, conditions, and humidity.",
{
city: z.string().describe("City name, e.g. 'San Francisco'"),
units: z
.enum(["celsius", "fahrenheit"])
.default("celsius")
.describe("Temperature units"),
},
async ({ city, units }) => {
try {
const apiUrl =
`https://api.openweathermap.org/data/2.5/weather?` +
`q=${encodeURIComponent(city)}&units=${units === "celsius" ? "metric" : "imperial"}&` +
`appid=${process.env.OPENWEATHER_API_KEY}`;
const res = await fetch(apiUrl);
if (!res.ok) throw new Error(`API error: ${res.status}`);
const data = (await res.json()) as {
main: { temp: number; humidity: number };
weather: { description: string }[];
name: string;
};
const tempUnit = units === "celsius" ? "°C" : "°F";
const summary =
`Weather in ${data.name}: ${data.main.temp}${tempUnit}, ` +
`${data.weather[0].description}, humidity ${data.main.humidity}%`;
return { content: [{ type: "text", text: summary }] };
} catch (err) {
return {
content: [{ type: "text", text: `Error: ${(err as Error).message}` }],
isError: true,
};
}
}
);Defining Resources
Resources are read-only data sources that the model can access without executing any side-effecting logic. Use them for static reference data, file contents, or database records that the model needs to reason about.
Resource definition
server.resource(
"weather_units_reference",
"weather://units/reference",
{ mimeType: "text/plain" },
async () => ({
contents: [
{
uri: "weather://units/reference",
mimeType: "text/plain",
text:
"Celsius (metric): used in most countries.\n" +
"Fahrenheit (imperial): used in US, Belize, Cayman Islands.\n" +
"Kelvin: scientific use only; 0K = -273.15°C.",
},
],
})
);Defining Prompts
Prompts are reusable message templates that users can invoke by name in Claude Desktop (via the / slash command menu). They are useful for common queries that require specific formatting or context.
Prompt definition
server.prompt(
"weather_report",
"Generate a friendly weather report for a city",
{ city: z.string().describe("City to get weather for") },
({ city }) => ({
messages: [
{
role: "user",
content: {
type: "text",
text:
`Please get the current weather for ${city} and write a friendly, ` +
`conversational weather report that a local TV presenter might read. ` +
`Include temperature, conditions, and any notable weather advisory.`,
},
},
],
})
);Transport Types: stdio vs SSE
| Transport | Use Case | Pros | Cons |
|---|---|---|---|
| stdio | Claude Desktop, local tools | Zero network config, secure by default | One client at a time, local only |
| SSE | Remote clients, multi-user, web apps | Multiple clients, supports streaming, HTTP-compatible | Requires TLS + auth setup, more complex |
SSE transport setup (for remote/web clients)
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";
const app = express();
const transports = new Map<string, SSEServerTransport>();
app.get("/sse", async (req, res) => {
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
const transport = new SSEServerTransport("/messages", res);
const sessionId = crypto.randomUUID();
transports.set(sessionId, transport);
res.on("close", () => transports.delete(sessionId));
await server.connect(transport);
});
app.post("/messages", express.json(), async (req, res) => {
const sessionId = req.query.sessionId as string;
const transport = transports.get(sessionId);
if (!transport) {
res.status(404).json({ error: "Session not found" });
return;
}
await transport.handlePostMessage(req, res);
});
app.listen(3000, () => console.log("MCP server on :3000"));Error Handling
MCP tools should never throw unhandled exceptions. The protocol expects all errors to be returned as content with isError: true. This lets the model understand what went wrong and decide how to recover — rather than crashing the entire session.
Error handling pattern
server.tool("risky_tool", { input: z.string() }, async ({ input }) => {
try {
const result = await doSomethingRisky(input);
return { content: [{ type: "text", text: result }] };
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
// Log server-side for debugging
console.error("[risky_tool] Error:", message);
// Return structured error to the model
return {
content: [
{
type: "text",
text: `Tool failed: ${message}. Please try a different approach.`,
},
],
isError: true,
};
}
});Testing with MCP Inspector
MCP Inspector is the official debugging tool. It launches your server and provides a web UI where you can call tools, inspect responses, and verify your JSON Schema definitions — all without needing a full Claude Desktop session.
Launch MCP Inspector
# Inspect your server directly (requires tsx for .ts files) npx @modelcontextprotocol/inspector tsx src/server.ts # Or for compiled output npx @modelcontextprotocol/inspector node dist/server.js # Opens browser at http://localhost:5173 # You'll see all tools, resources, and prompts listed # Click any tool to test it with custom arguments
Complete Weather Server: Full Code
Here is the complete, production-ready weather server combining all the concepts above. It includes four tools, one resource, one prompt, graceful shutdown, and environment variable validation.
src/server.ts — complete implementation
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// Validate environment on startup
const API_KEY = process.env.OPENWEATHER_API_KEY;
if (!API_KEY) {
console.error("OPENWEATHER_API_KEY environment variable is required");
process.exit(1);
}
const server = new McpServer({
name: "weather-server",
version: "1.0.0",
});
// Tool 1: Current weather
server.tool(
"get_current_weather",
"Get current weather for a city. Returns temp, conditions, humidity.",
{
city: z.string().describe("City name"),
units: z.enum(["celsius", "fahrenheit"]).default("celsius"),
},
async ({ city, units }) => {
try {
const metric = units === "celsius" ? "metric" : "imperial";
const res = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(city)}&units=${metric}&appid=${API_KEY}`
);
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
const d = (await res.json()) as {
main: { temp: number; feels_like: number; humidity: number };
weather: { description: string; main: string }[];
wind: { speed: number };
name: string;
};
const u = units === "celsius" ? "°C" : "°F";
const ws = units === "celsius" ? "m/s" : "mph";
return {
content: [
{
type: "text",
text:
`${d.name}: ${d.main.temp}${u} (feels like ${d.main.feels_like}${u})\n` +
`Conditions: ${d.weather[0].description}\n` +
`Humidity: ${d.main.humidity}% | Wind: ${d.wind.speed} ${ws}`,
},
],
};
} catch (err) {
return {
content: [{ type: "text", text: `Error: ${(err as Error).message}` }],
isError: true,
};
}
}
);
// Tool 2: 5-day forecast
server.tool(
"get_forecast",
"Get a 5-day weather forecast for a city.",
{ city: z.string(), units: z.enum(["celsius", "fahrenheit"]).default("celsius") },
async ({ city, units }) => {
try {
const metric = units === "celsius" ? "metric" : "imperial";
const res = await fetch(
`https://api.openweathermap.org/data/2.5/forecast?q=${encodeURIComponent(city)}&units=${metric}&appid=${API_KEY}`
);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const d = (await res.json()) as {
list: { dt_txt: string; main: { temp_max: number; temp_min: number }; weather: { description: string }[] }[];
city: { name: string };
};
const u = units === "celsius" ? "°C" : "°F";
// One entry per day (take noon reading)
const daily = d.list
.filter((e) => e.dt_txt.includes("12:00:00"))
.slice(0, 5)
.map(
(e) =>
`${e.dt_txt.split(" ")[0]}: ${e.main.temp_max}${u}/${e.main.temp_min}${u} — ${e.weather[0].description}`
)
.join("\n");
return { content: [{ type: "text", text: `5-day forecast for ${d.city.name}:\n${daily}` }] };
} catch (err) {
return {
content: [{ type: "text", text: `Error: ${(err as Error).message}` }],
isError: true,
};
}
}
);
// Resource: Units reference
server.resource(
"units_reference",
"weather://units",
{ mimeType: "text/plain" },
async () => ({
contents: [
{
uri: "weather://units",
mimeType: "text/plain",
text: "celsius (metric) | fahrenheit (imperial)",
},
],
})
);
// Prompt: Weather briefing
server.prompt(
"morning_briefing",
"Get a morning weather briefing for a city",
{ city: z.string() },
({ city }) => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `Get the current weather and 5-day forecast for ${city}. Write a concise morning briefing.`,
},
},
],
})
);
// Graceful shutdown
process.on("SIGINT", async () => {
await server.close();
process.exit(0);
});
// Start
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Weather MCP server running");Connecting to Claude Desktop
claude_desktop_config.json
{
"mcpServers": {
"weather": {
"command": "node",
"args": ["/absolute/path/to/weather-mcp-server/dist/server.js"],
"env": {
"OPENWEATHER_API_KEY": "your_api_key_here"
}
}
}
}Build first with npm run build, restart Claude Desktop, and your weather tools appear automatically. Ask Claude: "What's the weather in Tokyo right now?" and it will call your server.