Skip to main content
RapidDev - Software Development Agency
mcp-tutorial

How to build your first MCP server

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.

What you'll learn

  • How to build an MCP server from scratch with TypeScript
  • How to define tools with Zod input validation and error handling
  • How to add resources for contextual data
  • How to test with MCP Inspector and connect to Claude Desktop
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner9 min read20 minNode.js 18+, TypeScript, any MCP-compatible hostMarch 2026RapidDev Engineering Team
TL;DR

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

1

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.

typescript
1mkdir weather-mcp-server
2cd weather-mcp-server
3npm init -y
4npm install @modelcontextprotocol/sdk zod
5npm install -D typescript tsx @types/node
6npx tsc --init

Expected result: A project directory with package.json, tsconfig.json, and node_modules containing the MCP SDK.

2

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.

typescript
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}
11
12// 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": true
23 },
24 "include": ["src/**/*"]
25}

Expected result: Project is configured for ESM with TypeScript and has scripts for dev, build, and inspector testing.

3

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.

typescript
1// src/index.ts
2import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4import { z } from "zod";
5
6const server = new McpServer({
7 name: "weather-server",
8 version: "1.0.0",
9});
10
11// Tools and resources will be added here
12
13const 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.

4

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.

typescript
1// Add before the transport connection in src/index.ts
2
3interface WeatherData {
4 temperature: number;
5 condition: string;
6 humidity: number;
7 wind_speed: number;
8}
9
10const 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};
17
18server.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.

5

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?'

typescript
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()];
11
12 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 }
19
20 const warmer = data1.temperature > data2.temperature ? city1 : city2;
21 const diff = Math.abs(data1.temperature - data2.temperature);
22
23 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.

6

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.

typescript
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 2
17 ),
18 }],
19 })
20);

Expected result: A weather://cities resource is available that returns a JSON list of supported cities.

7

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.

typescript
1npx -y @modelcontextprotocol/inspector tsx src/index.ts

Expected result: The MCP Inspector opens in your browser showing your tools and resources. Tool calls return weather data correctly.

8

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.

typescript
1// ~/Library/Application Support/Claude/claude_desktop_config.json
2{
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

src/index.ts
1#!/usr/bin/env node
2import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4import { z } from "zod";
5
6interface WeatherData {
7 temperature: number;
8 condition: string;
9 humidity: number;
10 wind_speed: number;
11}
12
13const 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};
20
21const server = new McpServer({ name: "weather-server", version: "1.0.0" });
22
23server.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);
39
40server.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);
55
56server.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);
68
69const 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.

ChatGPT Prompt

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.

MCP Prompt

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.