Build Your First MCP Server: Weather API Tutorial
Follow the official MCP quickstart to build a weather server with two tools, then connect it to Claude Desktop and Cursor.
--- title: Build your first MCP server with the Weather API description: A hands-on walkthrough of building an MCP server from scratch using the official quickstart guide. Two tools, zero API keys, fully working in under an hour. author: FractionalSkill ---
Build your first MCP server with the Weather API
Reading about MCP servers is one thing. Building one yourself is where everything clicks. This is the hands-on tutorial where you go from an empty folder to a working MCP server that fetches live weather data and runs inside Claude Desktop or Cursor.
We're following the official MCP quickstart docs for this. The reason is practical: the quickstart introduces every building block you'll reuse in every server you build later. The weather server becomes your starter template. Once it works, you swap the weather code for whatever API your next project needs.
The National Weather Service API is free, requires no API key, and returns real data. That makes it the perfect first target. No credentials to manage, no billing to worry about.
Set up the project
Every MCP server starts the same way: a folder, some dependencies, and a TypeScript config. Get comfortable with this sequence because you'll repeat it for every server you build.
Create the project folder and initialize it.
mkdir weather
cd weather
npm init -y
Install the MCP SDK and Zod.
npm install @modelcontextprotocol/sdk zod
npm install -D @types/node typescript
The first line brings in the official TypeScript SDK for building MCP servers plus Zod, a library for defining and validating data types. The second line installs TypeScript and Node types as developer dependencies.
Configure the project files.
You need three files adjusted before writing any server code. In package.json, change the type to "module" and replace the default scripts with a build command:
{
"type": "module",
"scripts": {
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\""
}
}
Create a tsconfig.json at the project root:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"]
}
Then create your source file:
mkdir src
touch src/index.ts
> Template this. Save a copy of this empty project somewhere. Every new MCP server you build starts from this exact skeleton. You'll replace the weather-specific code with whatever API you're targeting, but the project structure stays identical.
Register your tools with the server
Here is where MCP-specific code begins. Everything before this was standard TypeScript project setup. The src/index.ts file is where you define what your server can do.
Initialize the server.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
const NWS_API_BASE = "https://api.weather.gov";
const server = new McpServer({
name: "weather",
version: "1.0.0",
});
That McpServer instance gives you all the methods you need to register tools. The name and version are metadata that clients display when they connect.
Register the get-alerts tool.
server.tool(
"get-alerts",
"Get weather alerts for a US state",
{ state: z.string().length(2).describe("Two-letter US state code") },
async ({ state }) => {
const response = await fetch(
`${NWS_API_BASE}/alerts?area=${state}`
);
const data = await response.json();
const alerts = data.features?.map((f: any) => f.properties) || [];
return {
content: [{
type: "text",
text: alerts.length > 0
? alerts.map((a: any) =>
`${a.event}\n${a.headline}\n${a.description}`
).join("\n---\n")
: "No active alerts for this state.",
}],
};
}
);
The server.tool method takes four arguments in order: the tool name, a description, the input parameters defined with Zod, and a callback function that executes when the tool is called. The name and description are what the LLM reads to decide whether to invoke this tool based on your query.
Zod handles input validation. When you define state as a string with length 2, the server rejects anything that doesn't match before your code runs.
Register the get-forecast tool. Same pattern, different parameters:
server.tool(
"get-forecast",
"Get the weather forecast for a location",
{
latitude: z.number().min(-90).max(90).describe("Latitude"),
longitude: z.number().min(-180).max(180).describe("Longitude"),
},
async ({ latitude, longitude }) => {
// Get the forecast grid endpoint for this location
const pointRes = await fetch(
`${NWS_API_BASE}/points/${latitude},${longitude}`
);
const pointData = await pointRes.json();
const forecastUrl = pointData.properties?.forecast;
const forecastRes = await fetch(forecastUrl);
const forecastData = await forecastRes.json();
const periods = forecastData.properties?.periods || [];
return {
content: [{
type: "text",
text: periods.map((p: any) =>
`${p.name}: ${p.temperature}${p.temperatureUnit} - ${p.detailedForecast}`
).join("\n\n"),
}],
};
}
);
Notice how the LLM handles the conversion for you. You type "get the weather for Austin, Texas" and the model figures out the latitude and longitude values to pass into the tool. You never manually supply coordinates.
Connect the transport and compile
Below your two tool registrations, add the main function that starts the server:
import { StdioServerTransport } from
"@modelcontextprotocol/sdk/server/stdio.js";
const transport = new StdioServerTransport();
await server.connect(transport);
The transport layer handles communication between your server and the MCP client. StdioServerTransport uses standard input/output, which is what Claude Desktop and Cursor expect. You don't need to configure ports or URLs.
Build the project.
npm run build
This compiles your TypeScript into JavaScript inside the build/ folder. The output file at build/index.js is what MCP clients will execute. Every time you change your source code, run this build step again.
> Check the output. After building, open build/index.js and verify it contains actual code. If the file is empty, your src/index.ts likely has a syntax error. TypeScript will print the error in your terminal when you run the build command.
Connect to Claude Desktop and Cursor
Your server is built. It needs a client to connect to. Both Claude Desktop and Cursor use a JSON config file to discover MCP servers.
For Claude Desktop, open or create claudedesktopconfig.json:
- macOS:
~/Library/Application Support/Claude/claudedesktopconfig.json - Windows:
%APPDATA%\Claude\claudedesktopconfig.json
Add your weather server:
{
"mcpServers": {
"weather": {
"command": "node",
"args": ["/ABSOLUTE/PATH/TO/weather/build/index.js"]
}
}
}
Replace /ABSOLUTE/PATH/TO/ with the actual path to your weather project folder. Restart Claude Desktop after saving the file.
For Cursor, the process is similar. Open Cursor settings, navigate to the MCP section, and add the same server configuration pointing to your compiled build/index.js file.
> Use absolute paths. Relative paths in the config file will fail silently. Always use the full path from root to your build/index.js. On macOS, you can get this by dragging the file into Terminal, which prints the full path.
Test it with a real query
Once Claude Desktop restarts with the server configured, you should see a hammer icon or tool indicator showing your weather server is connected.
You: What are the current weather alerts for California?
Claude: I'll check the weather alerts for California using the
get-alerts tool.
[Calling get-alerts with state: "CA"]
There are currently 3 active alerts for California:
1. Heat Advisory - Excessive heat expected in the Central Valley...
2. Red Flag Warning - Critical fire weather conditions...
3. Beach Hazard Statement - High surf along the coast...
You: What's the forecast for Austin, Texas?
Claude: Let me get the forecast for Austin.
[Calling get-forecast with latitude: 30.2672, longitude: -97.7431]
Tonight: 72F - Partly cloudy with a low around 72...
Thursday: 95F - Sunny with a high near 95...
The model automatically selected the right tool, extracted the correct parameters from your natural language query, and returned formatted results. You didn't specify coordinates or state codes. The LLM parsed your intent and mapped it to the tool's required inputs.
This is the foundation for every MCP server you'll build. The weather API gets replaced with a CRM, a project management tool, a client database, or whatever system your work requires. The pattern of initializing a server, registering tools with typed parameters, and connecting via transport stays exactly the same.
Your next step is building a server for a tool you actually use every day.