Build a complete MCP server in TypeScript that exposes a weather lookup tool and a city info resource. You will initialize the project, install the SDK, define tools with Zod validation, add resources, test with MCP Inspector, and connect to Claude Desktop. By the end you will have a working server that any MCP-compatible AI host can use.
Build a Weather MCP Server Step by Step
This tutorial builds a weather lookup MCP server from scratch. You will create a server that exposes a 'get_weather' tool the AI can call to check weather conditions, plus a 'cities' resource listing supported cities. This covers the complete workflow: project setup, tool definition with input validation, resource definition, testing with the Inspector, and connecting to an AI host. The weather data is mocked so you can focus on learning MCP without needing an API key.
Prerequisites
- Node.js 18+ installed
- npm package manager
- A code editor (VS Code or Cursor recommended)
- Basic TypeScript knowledge (types, async/await)
Step-by-step guide
Initialize the project and install dependencies
Initialize the project and install dependencies
Create a new directory, initialize npm with ESM support, and install the MCP SDK with Zod for schema validation. Also install tsx for running TypeScript directly during development.
1mkdir weather-mcp-server2cd weather-mcp-server3npm init -y4npm install @modelcontextprotocol/sdk zod5npm install -D typescript tsx @types/node6npx tsc --initExpected result: A project directory with package.json, tsconfig.json, and node_modules containing the MCP SDK.
Configure package.json and tsconfig.json
Configure package.json and tsconfig.json
Update package.json to use ESM modules and add development scripts. Update tsconfig.json for NodeNext module resolution which the MCP SDK requires. These configurations ensure TypeScript compiles correctly and Node.js treats imports as ESM.
1// package.json — add/update these fields:2{3 "type": "module",4 "scripts": {5 "dev": "tsx src/index.ts",6 "build": "tsc",7 "start": "node dist/index.js",8 "inspect": "npx -y @modelcontextprotocol/inspector tsx src/index.ts"9 }10}1112// tsconfig.json — update compilerOptions:13{14 "compilerOptions": {15 "target": "ES2022",16 "module": "NodeNext",17 "moduleResolution": "NodeNext",18 "outDir": "./dist",19 "rootDir": "./src",20 "strict": true,21 "esModuleInterop": true,22 "skipLibCheck": true23 },24 "include": ["src/**/*"]25}Expected result: Project is configured for ESM with TypeScript and has scripts for dev, build, and inspector testing.
Create the server entry point with McpServer
Create the server entry point with McpServer
Create src/index.ts and set up the McpServer instance. Import McpServer from the SDK, create a server with a name and version, and connect it to stdio transport. This is the skeleton that all tools and resources will be added to.
1// src/index.ts2import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";3import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";4import { z } from "zod";56const server = new McpServer({7 name: "weather-server",8 version: "1.0.0",9});1011// Tools and resources will be added here1213const transport = new StdioServerTransport();14await server.connect(transport);15console.error("Weather MCP server started");Expected result: A minimal server that starts and waits for MCP connections via stdio.
Add mock weather data and the get_weather tool
Add mock weather data and the get_weather tool
Define a mock weather database and create the get_weather tool. The tool takes a city name as input (validated with Zod), looks up the weather data, and returns a formatted response. Notice the isError flag for error cases — this tells the AI model the tool call failed so it can respond appropriately instead of treating the error message as weather data.
1// Add before the transport connection in src/index.ts23interface WeatherData {4 temperature: number;5 condition: string;6 humidity: number;7 wind_speed: number;8}910const weatherDb: Record<string, WeatherData> = {11 "new york": { temperature: 18, condition: "Partly cloudy", humidity: 65, wind_speed: 12 },12 "london": { temperature: 12, condition: "Overcast", humidity: 80, wind_speed: 20 },13 "tokyo": { temperature: 22, condition: "Clear sky", humidity: 55, wind_speed: 8 },14 "paris": { temperature: 15, condition: "Light rain", humidity: 75, wind_speed: 15 },15 "sydney": { temperature: 25, condition: "Sunny", humidity: 45, wind_speed: 10 },16};1718server.tool(19 "get_weather",20 "Get current weather conditions for a city",21 {22 city: z.string().describe("City name (e.g., 'New York', 'London', 'Tokyo')"),23 },24 async ({ city }) => {25 const data = weatherDb[city.toLowerCase()];26 if (!data) {27 return {28 content: [{29 type: "text",30 text: `Weather data not available for "${city}". Supported cities: ${Object.keys(weatherDb).join(", ")}`,31 }],32 isError: true,33 };34 }35 return {36 content: [{37 type: "text",38 text: [39 `Weather in ${city}:`,40 ` Temperature: ${data.temperature}°C`,41 ` Condition: ${data.condition}`,42 ` Humidity: ${data.humidity}%`,43 ` Wind speed: ${data.wind_speed} km/h`,44 ].join("\n"),45 }],46 };47 }48);Expected result: The server has a get_weather tool that returns weather data for supported cities or an error message for unsupported ones.
Add a compare_weather tool for multi-city comparison
Add a compare_weather tool for multi-city comparison
Add a second tool that compares weather between two cities. This demonstrates tools with multiple parameters and more complex output formatting. The AI model can use this tool when users ask questions like 'Is it warmer in Tokyo or London?'
1server.tool(2 "compare_weather",3 "Compare weather between two cities",4 {5 city1: z.string().describe("First city name"),6 city2: z.string().describe("Second city name"),7 },8 async ({ city1, city2 }) => {9 const data1 = weatherDb[city1.toLowerCase()];10 const data2 = weatherDb[city2.toLowerCase()];1112 if (!data1 || !data2) {13 const missing = [!data1 && city1, !data2 && city2].filter(Boolean);14 return {15 content: [{ type: "text", text: `Weather data not available for: ${missing.join(", ")}` }],16 isError: true,17 };18 }1920 const warmer = data1.temperature > data2.temperature ? city1 : city2;21 const diff = Math.abs(data1.temperature - data2.temperature);2223 return {24 content: [{25 type: "text",26 text: [27 `${city1}: ${data1.temperature}°C, ${data1.condition}`,28 `${city2}: ${data2.temperature}°C, ${data2.condition}`,29 ``,30 `${warmer} is ${diff}°C warmer.`,31 ].join("\n"),32 }],33 };34 }35);Expected result: The server now has two tools. The AI can check individual city weather or compare two cities.
Add a cities resource for context
Add a cities resource for context
Add a resource that lists all supported cities with their current conditions. This resource can be loaded by the host application to give the AI context about what cities are available, without the AI needing to call a tool first.
1server.resource(2 "supported-cities",3 "weather://cities",4 { description: "List of all supported cities with current conditions" },5 async (uri) => ({6 contents: [{7 uri: uri.href,8 mimeType: "application/json",9 text: JSON.stringify(10 Object.entries(weatherDb).map(([city, data]) => ({11 city: city.charAt(0).toUpperCase() + city.slice(1),12 temperature: `${data.temperature}°C`,13 condition: data.condition,14 })),15 null,16 217 ),18 }],19 })20);Expected result: A weather://cities resource is available that returns a JSON list of supported cities.
Test the server with MCP Inspector
Test the server with MCP Inspector
Run the MCP Inspector to test your server interactively. The Inspector connects to your server and lets you call tools, read resources, and see the raw JSON-RPC messages. This is the most important testing step — always verify your server works in the Inspector before connecting it to an AI host.
1npx -y @modelcontextprotocol/inspector tsx src/index.tsExpected result: The MCP Inspector opens in your browser showing your tools and resources. Tool calls return weather data correctly.
Connect to Claude Desktop
Connect to Claude Desktop
Add your server to Claude Desktop's configuration file. On macOS, edit ~/Library/Application Support/Claude/claude_desktop_config.json. On Windows, edit %APPDATA%\Claude\claude_desktop_config.json. After saving, restart Claude Desktop. Your weather tools will appear in the tool list.
1// ~/Library/Application Support/Claude/claude_desktop_config.json2{3 "mcpServers": {4 "weather": {5 "command": "npx",6 "args": ["-y", "tsx", "/absolute/path/to/weather-mcp-server/src/index.ts"]7 }8 }9}Expected result: After restarting Claude Desktop, you can ask 'What is the weather in Tokyo?' and Claude will call your get_weather tool.
Complete working example
1#!/usr/bin/env node2import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";3import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";4import { z } from "zod";56interface WeatherData {7 temperature: number;8 condition: string;9 humidity: number;10 wind_speed: number;11}1213const weatherDb: Record<string, WeatherData> = {14 "new york": { temperature: 18, condition: "Partly cloudy", humidity: 65, wind_speed: 12 },15 "london": { temperature: 12, condition: "Overcast", humidity: 80, wind_speed: 20 },16 "tokyo": { temperature: 22, condition: "Clear sky", humidity: 55, wind_speed: 8 },17 "paris": { temperature: 15, condition: "Light rain", humidity: 75, wind_speed: 15 },18 "sydney": { temperature: 25, condition: "Sunny", humidity: 45, wind_speed: 10 },19};2021const server = new McpServer({ name: "weather-server", version: "1.0.0" });2223server.tool(24 "get_weather", "Get current weather for a city",25 { city: z.string().describe("City name") },26 async ({ city }) => {27 const data = weatherDb[city.toLowerCase()];28 if (!data) {29 return {30 content: [{ type: "text", text: `Not available for "${city}". Try: ${Object.keys(weatherDb).join(", ")}` }],31 isError: true,32 };33 }34 return {35 content: [{ type: "text", text: `${city}: ${data.temperature}°C, ${data.condition}, ${data.humidity}% humidity, ${data.wind_speed} km/h wind` }],36 };37 }38);3940server.tool(41 "compare_weather", "Compare weather between two cities",42 { city1: z.string().describe("First city"), city2: z.string().describe("Second city") },43 async ({ city1, city2 }) => {44 const d1 = weatherDb[city1.toLowerCase()];45 const d2 = weatherDb[city2.toLowerCase()];46 if (!d1 || !d2) {47 return { content: [{ type: "text", text: "One or both cities not found" }], isError: true };48 }49 const warmer = d1.temperature > d2.temperature ? city1 : city2;50 return {51 content: [{ type: "text", text: `${city1}: ${d1.temperature}°C (${d1.condition})\n${city2}: ${d2.temperature}°C (${d2.condition})\n${warmer} is warmer by ${Math.abs(d1.temperature - d2.temperature)}°C` }],52 };53 }54);5556server.resource(57 "supported-cities", "weather://cities",58 { description: "List of supported cities" },59 async (uri) => ({60 contents: [{61 uri: uri.href, mimeType: "application/json",62 text: JSON.stringify(Object.entries(weatherDb).map(([city, d]) => ({63 city, temperature: d.temperature, condition: d.condition,64 }))),65 }],66 })67);6869const transport = new StdioServerTransport();70await server.connect(transport);71console.error("Weather server started");Common mistakes when building your first MCP server
Why it's a problem: Not using absolute paths in AI host configuration
How to avoid: Claude Desktop and Cursor require absolute paths to your server file. Use /Users/you/projects/weather-mcp-server/src/index.ts, not ./src/index.ts. Relative paths fail because the host's working directory is not your project.
Why it's a problem: Forgetting to restart the AI host after config changes
How to avoid: Claude Desktop and Cursor cache MCP server configurations at startup. After editing the config file, you must fully restart the application. Just closing and reopening a chat is not enough.
Why it's a problem: Not setting isError: true for error responses
How to avoid: Without isError: true, the AI model treats error messages as valid tool output. It might say 'The weather data shows City not found' instead of acknowledging the error. Always set isError: true for failure cases.
Why it's a problem: Returning data in non-text format
How to avoid: MCP tool responses must use the content array format with type: 'text' (or 'image'). Returning a plain string or JSON object directly causes protocol errors. Always wrap responses in { content: [{ type: 'text', text: '...' }] }.
Best practices
- Always test with MCP Inspector before connecting to an AI host — it shows raw protocol messages for debugging
- Use isError: true in tool responses to signal failures to the AI model
- Validate all inputs with Zod schemas and provide .describe() for every parameter
- Use absolute paths when configuring servers in AI host config files
- Return human-readable text from tools — the AI model will present this to users
- Keep mock data during development, replace with real API calls when the server logic is solid
- Log startup and errors to stderr: console.error('Server started') — never console.log
- Add the -y flag to all npx commands in config files to avoid hanging on install prompts
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Walk me through building an MCP server in TypeScript from scratch. I want a weather lookup tool that takes a city name, validates input with Zod, returns formatted weather data, and handles errors with isError. Include the complete project setup, package.json, tsconfig.json, and how to test with MCP Inspector.
Help me build my first MCP server. I want to create a weather server in TypeScript with two tools: get_weather (single city) and compare_weather (two cities). Show me the complete code, how to test it with the Inspector, and how to connect it to my current editor.
Frequently asked questions
Can I use a real weather API instead of mock data?
Yes. Replace the weatherDb lookup with a fetch call to any weather API (OpenWeatherMap, WeatherAPI, etc.). Pass the API key via environment variables in the host configuration, not hardcoded in source code.
Why does my server not show tools in Claude Desktop?
The most common causes are: (1) the config file path is wrong, (2) you used a relative path instead of absolute, (3) you did not restart Claude Desktop after saving the config, or (4) your server has a syntax error that prevents startup. Check Claude Desktop's logs for error details.
Can I add more tools to this server later?
Yes. Just add more server.tool() calls before the transport connection. Each tool gets its own name, description, schema, and handler. Restart the AI host to pick up the changes.
How do I debug tool handler errors?
Wrap your tool handler in try/catch and log errors to stderr: console.error('Error:', error). Also test with MCP Inspector first, which shows detailed error messages. For production, consider adding structured logging.
Can RapidDev help build custom MCP servers for my company?
Yes. RapidDev builds custom MCP servers that connect your internal APIs, databases, and tools to AI assistants. This includes architecture design, implementation, testing, and deployment guidance.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation