Build a Weather MCP Server with the Official TypeScript SDK
Build the same weather server using the official MCP TypeScript SDK. Compare the functional approach with mcp-framework's class-based style and learn when to use each.
title: "Build a Weather MCP Server with the Official TypeScript SDK" description: "Build the same weather server using the official MCP TypeScript SDK. Compare the functional approach with mcp-framework's class-based style and learn when to use each." order: 2 keywords:
- mcp typescript sdk
- official mcp sdk tutorial
- weather server typescript
- model context protocol sdk
- mcp server from scratch date: "2026-04-01" level: "beginner" duration: "25 min"
Build a Weather MCP Server using the official MCP TypeScript SDK (@modelcontextprotocol/sdk). This tutorial covers the same weather functionality as the mcp-framework version, giving you a direct comparison of both approaches.
Why the Official SDK?
The official MCP TypeScript SDK (@modelcontextprotocol/sdk) is maintained by Anthropic and provides the most direct, low-level access to the MCP protocol. While mcp-framework offers higher-level abstractions, the official SDK gives you full control over every aspect of your server.
The reference implementation of the Model Context Protocol for TypeScript/JavaScript. Published as @modelcontextprotocol/sdk on npm, it provides the core Server class, transport layers, and type definitions.
Prerequisites
- Node.js 18+ installed
- npm or yarn package manager
- Basic TypeScript knowledge
- Optional: complete the mcp-framework tutorial first for comparison
Project Setup
Create the project directory
mkdir weather-server-sdk
cd weather-server-sdk
npm init -y
Install dependencies
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
Configure TypeScript
Create tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true
},
"include": ["src/**/*"]
}
Set up the project structure
Building the Weather Helper
First, create the weather data fetching logic in src/weather.ts:
export interface WeatherResult {
location: string;
temperature: string;
windSpeed: string;
humidity: string;
condition: string;
}
export interface GeocodingResult {
city: string;
country: string;
latitude: number;
longitude: number;
}
const WEATHER_CONDITIONS: Record<number, string> = {
0: "Clear sky",
1: "Mainly clear",
2: "Partly cloudy",
3: "Overcast",
45: "Foggy",
48: "Depositing rime fog",
51: "Light drizzle",
53: "Moderate drizzle",
55: "Dense drizzle",
61: "Slight rain",
63: "Moderate rain",
65: "Heavy rain",
71: "Slight snowfall",
73: "Moderate snowfall",
75: "Heavy snowfall",
95: "Thunderstorm",
};
export async function getWeather(
latitude: number,
longitude: number,
city?: string
): Promise<WeatherResult> {
const url = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,wind_speed_10m,weather_code,relative_humidity_2m`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Weather API returned ${response.status}`);
}
const data = await response.json();
const current = data.current;
return {
location: city || `${latitude}, ${longitude}`,
temperature: `${current.temperature_2m}°C`,
windSpeed: `${current.wind_speed_10m} km/h`,
humidity: `${current.relative_humidity_2m}%`,
condition: WEATHER_CONDITIONS[current.weather_code] || "Unknown",
};
}
export async function lookupCity(name: string): Promise<GeocodingResult> {
const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(name)}&count=1`;
const response = await fetch(url);
const data = await response.json();
if (!data.results || data.results.length === 0) {
throw new Error(`City "${name}" not found`);
}
const result = data.results[0];
return {
city: result.name,
country: result.country,
latitude: result.latitude,
longitude: result.longitude,
};
}
Creating the MCP Server
Now create the main server in src/index.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { getWeather, lookupCity } from "./weather.js";
const server = new McpServer({
name: "weather-server",
version: "1.0.0",
});
// Register the get_weather tool
server.tool(
"get_weather",
"Get current weather data for a given location by latitude and longitude",
{
latitude: z.number().min(-90).max(90).describe("Latitude (-90 to 90)"),
longitude: z.number().min(-180).max(180).describe("Longitude (-180 to 180)"),
city: z.string().optional().describe("Optional city name for display"),
},
async ({ latitude, longitude, city }) => {
try {
const weather = await getWeather(latitude, longitude, city);
return {
content: [
{
type: "text" as const,
text: JSON.stringify(weather, null, 2),
},
],
};
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
return {
content: [
{
type: "text" as const,
text: JSON.stringify({ error: message }),
},
],
isError: true,
};
}
}
);
// Register the lookup_city tool
server.tool(
"lookup_city",
"Look up the latitude and longitude of a city by name",
{
city: z.string().min(1).describe("The city name to look up"),
},
async ({ city }) => {
try {
const result = await lookupCity(city);
return {
content: [
{
type: "text" as const,
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
return {
content: [
{
type: "text" as const,
text: JSON.stringify({ error: message }),
},
],
isError: true,
};
}
}
);
// Start the server with stdio transport
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Weather MCP server running on stdio");
}
main().catch(console.error);
Always wrap your tool handlers in try/catch and return isError: true when something goes wrong. This tells the AI assistant that the tool call failed, so it can inform the user or try a different approach.
Key Differences from mcp-framework
| Aspect | Official SDK | mcp-framework |
|---|---|---|
| Tool registration | server.tool() function calls | Class-based, auto-discovered |
| Project setup | Manual npm init + config | CLI scaffolding |
| Return format | { content: [...], isError? } | Return string directly |
| Schema definition | Zod objects passed to server.tool() | Schema property on class |
| Server creation | new McpServer({ name, version }) | new MCPServer() |
| Transport setup | Explicit transport connection | Handled automatically |
The official SDK and mcp-framework both produce fully compatible MCP servers. Choose the SDK when you need maximum control or minimal dependencies. Choose mcp-framework when you want faster scaffolding and convention-based structure.
Building and Testing
Add build scripts to package.json
{
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
}
}
Build and test
npm run build
npx @modelcontextprotocol/inspector node dist/index.js
Connect to Claude Desktop
Add to your Claude Desktop configuration:
{
"mcpServers": {
"weather-sdk": {
"command": "node",
"args": ["/absolute/path/to/weather-server-sdk/dist/index.js"]
}
}
}
Adding a Resource
One advantage of the SDK is how straightforward it is to add resources alongside tools. Let's add a resource that lists supported weather codes:
server.resource(
"weather-codes",
"weather://codes",
async (uri) => {
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(WEATHER_CONDITIONS, null, 2),
},
],
};
}
);
Use resources for static or semi-static data that the AI might want to read. Use tools for actions that take input and produce dynamic output. In this case, weather codes are reference data (resource), while fetching live weather is an action (tool).
Next Steps
Now that you have seen both approaches, you can:
- Explore database servers for more complex tool patterns
- Learn about error handling best practices
- Try deploying to Docker