---
title: Build an MCP Server with Weather tools using Express and Vercel
description: Make your Express weather API accessible to AI assistants through the Model Context Protocol.
url: /kb/guide/mcp-server-with-weather-tool-express
canonical_url: "https://vercel.com/kb/guide/mcp-server-with-weather-tool-express"
published: 2025-11-26
last_updated: 2026-07-16
authors: Jeff See, Ismael Rumzan
related:
  - /docs/mcp
  - /docs/frameworks/backend
install_vercel_plugin: npx plugins add vercel/vercel-plugin
---

In this tutorial, you will expose the functions of a weather API as tools that AI agents can discover and use through the Model Context Protocol (MCP) by:

- Creating an Express API using Vercel's CLI and adding an endpoint that returns real time weather data.
  
- Build an MCP server with four specialized tools
  
- Test your MCP server with the MCP Inspector and the Cursor IDE
  

You will re-use the code from [How to Build a Weather API with Express and Vercel](https://vercel.com/guides/weather-api-with-express).

## Deploy the example

The complete code for this guide is available in this [Github repository.](https://github.com/vercel/examples/tree/main/guides/express-weather-mcp-server) You can [deploy it with one click](https://vercel.com/new/clone?repository-url=https://github.com/vercel/examples/tree/main/guides/express-weather-mcp-server&project-name=express-weather-mcp-server&repository-name=express-weather-mcp-server) on Vercel.

## Prerequisites

Before you begin, ensure you have:

- Node.js 20 or later installed
  
- Vercel CLI (`npm install -g vercel`)
  
- Basic knowledge of Express and TypeScript
  

## How MCP integration works

Adding MCP to Express creates two layers:

1. **Express REST API:** Your endpoints work as normal HTTP routes. They handle business logic, fetch data, process requests, and return JSON.
   
2. **MCP Tools:** MCP tools wrap your Express endpoints. When an AI agent calls a tool, the tool requests your Express endpoint and formats the response.
   

The `mcp-handler` package handles MCP protocol details. Because it expects Web API Request/Response objects, you will add a small adapter to convert Express req/res.

## 1\. Initialize an Express project

Create a new Express project with the Vercel CLI inside a folder called `express-mcp-weather`:

```bash
vc init express express-mcp-weather
cd express-mcp-weather
```

## 2\. Include MCP dependencies

Update your `package.json` with the following dependencies:

```json
"dependencies": {
    "express": "^5.1.0",
    "mcp-handler": "^1.0.3",
    "zod": "3.23.8"
  },
```

These provide:

- `mcp-handler`: Vercel's MCP adapter
  
- `zod`: Schema validation (version 3.23.8 required)
  

## 3\. Build the weather API endpoint

Replace `src/index.ts` with a weather endpoint:

```typescript
import express from 'express';

const app = express();
const PORT = process.env.PORT || 3000;

app.use(express.json());

app.get('/api/weather/:city', async (req, res) => {
  try {
    const city = req.params.city;
    const units = req.query.units as string | undefined;
    
    // Normalize units parameter
    const normalizedUnits = units === 'imperial' ? 'imperial' : 'metric';
    
    // Step 1: Geocode city to get coordinates
    const geoParams = new URLSearchParams({
      name: city,
      count: '1',
      language: 'en',
      format: 'json'
    });
    
    const geoResponse = await fetch(`https://geocoding-api.open-meteo.com/v1/search?${geoParams}`);
    
    if (!geoResponse.ok) {
      return res.status(geoResponse.status).json({ 
        error: 'Failed to fetch geocoding data' 
      });
    }
    
    const geoData = await geoResponse.json();
    
    if (!geoData.results || geoData.results.length === 0) {
      return res.status(404).json({ error: `City '${city}' not found` });
    }
    
    const location = geoData.results[0];
    const { name, country, latitude, longitude } = location;
    
    // Step 2: Fetch current weather data
    const weatherParams: Record<string, string> = {
      latitude: latitude.toString(),
      longitude: longitude.toString(),
      current: 'temperature_2m,relative_humidity_2m,apparent_temperature,wind_speed_10m',
      timezone: 'auto'
    };
    
    // Add unit parameters for imperial if needed
    if (units === 'imperial') {
      weatherParams.temperature_unit = 'fahrenheit';
      weatherParams.wind_speed_unit = 'mph';
    }
    
    const weatherUrlParams = new URLSearchParams(weatherParams);
    const weatherResponse = await fetch(`https://api.open-meteo.com/v1/forecast?${weatherUrlParams}`);
    
    if (!weatherResponse.ok) {
      return res.status(weatherResponse.status).json({ 
        error: 'Failed to fetch weather data' 
      });
    }
    
    const weatherData = await weatherResponse.json();
    
    // Return structured weather data
    res.json({
      city: name,
      country,
      latitude,
      longitude,
      units: normalizedUnits,
      current: weatherData.current
    });
    
  } catch (error) {
    console.error('Weather API error:', error);
    res.status(500).json({ 
      error: 'Failed to fetch weather data',
      message: error instanceof Error ? error.message : 'Unknown error'
    });
  }
});
```

This endpoint geocodes city names and fetches weather from the Open-Meteo API.

Test the endpoint with the `vercel` CLI. You will be asked to link your code with an existing or new project on your Vercel account:

```bash
vercel dev
```

In another terminal:

```bash
# Test with default metric units
curl http://localhost:3000/api/weather/london

# Test with imperial units
curl "http://localhost:3000/api/weather/san%20francisco?units=imperial"
```

You should see weather data in JSON format.

## 4\. Add MCP tools that call your API

Add MCP tools to `src/index.ts` after the weather endpoint:

```typescript
// This goes at the top of the file
import { z } from 'zod';
import { createMcpHandler } from 'mcp-handler';

// This goes below the weather endpoint, 
// Helper function: Call your own Express API
async function callWeatherAPI(
  city: string,
  units: 'metric' | 'imperial' = 'metric'
) {
  const response = await fetch(
    `http://localhost:${PORT}/api/weather/${city}?units=${units}`
  );

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.error || 'Failed to fetch weather');
  }

  return await response.json();
}

// Create MCP handler with tools
const mcpHandler = createMcpHandler(
  (server) => {
    // Tool 1: Get Temperature
    server.tool(
      'get_temperature',
      'Get current temperature and "feels like" temperature for a city',
      {
        city: z.string().describe('City name (e.g., "London", "Tokyo")'),
        units: z.enum(['metric', 'imperial'])
          .optional()
          .describe('metric (Celsius) or imperial (Fahrenheit)')
      },
      async ({ city, units = 'metric' }) => {
        try {
          const data = await callWeatherAPI(city, units);
          const tempUnit = units === 'imperial' ? '°F' : '°C';

          return {
            content: [{
              type: 'text',
              text: `Temperature in ${data.city}, ${data.country}:
- Current: ${data.current.temperature_2m}${tempUnit}
- Feels like: ${data.current.apparent_temperature}${tempUnit}`
            }]
          };
        } catch (error) {
          return {
            content: [{
              type: 'text',
              text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`
            }],
            isError: true
          };
        }
      }
    );

    // Tool 2: Get Humidity
    server.tool(
      'get_humidity',
      'Get current relative humidity for a city',
      {
        city: z.string().describe('City name (e.g., "London", "Tokyo")')
      },
      async ({ city }) => {
        try {
          const data = await callWeatherAPI(city, 'metric');

          return {
            content: [{
              type: 'text',
              text: `Humidity in ${data.city}, ${data.country}:
- Relative Humidity: ${data.current.relative_humidity_2m}%`
            }]
          };
        } catch (error) {
          return {
            content: [{
              type: 'text',
              text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`
            }],
            isError: true
          };
        }
      }
    );

    // Tool 3: Get Wind Speed
    server.tool(
      'get_wind_speed',
      'Get current wind speed for a city',
      {
        city: z.string().describe('City name (e.g., "London", "Tokyo")'),
        units: z.enum(['metric', 'imperial'])
          .optional()
          .describe('metric (km/h) or imperial (mph)')
      },
      async ({ city, units = 'metric' }) => {
        try {
          const data = await callWeatherAPI(city, units);
          const speedUnit = units === 'imperial' ? 'mph' : 'km/h';

          return {
            content: [{
              type: 'text',
              text: `Wind Speed in ${data.city}, ${data.country}:
- Current: ${data.current.wind_speed_10m} ${speedUnit}`
            }]
          };
        } catch (error) {
          return {
            content: [{
              type: 'text',
              text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`
            }],
            isError: true
          };
        }
      }
    );

    // Tool 4: Get Full Weather
    server.tool(
      'get_full_weather',
      'Get complete weather information for a city',
      {
        city: z.string().describe('City name (e.g., "London", "Tokyo")'),
        units: z.enum(['metric', 'imperial'])
          .optional()
          .describe('metric or imperial units')
      },
      async ({ city, units = 'metric' }) => {
        try {
          const data = await callWeatherAPI(city, units);
          const tempUnit = units === 'imperial' ? '°F' : '°C';
          const speedUnit = units === 'imperial' ? 'mph' : 'km/h';

          return {
            content: [{
              type: 'text',
              text: `Weather for ${data.city}, ${data.country}:

📍 Location: ${data.latitude}, ${data.longitude}
🌡️ Temperature: ${data.current.temperature_2m}${tempUnit} (feels like ${data.current.apparent_temperature}${tempUnit})
💧 Humidity: ${data.current.relative_humidity_2m}%
💨 Wind: ${data.current.wind_speed_10m} ${speedUnit}
🕐 Updated: ${data.current.time}`
            }]
          };
        } catch (error) {
          return {
            content: [{
              type: 'text',
              text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`
            }],
            isError: true
          };
        }
      }
    );
  },
  {},
  { basePath: '/api' }
);
```

Each tool follows this pattern:

1. Define name and description
   
2. Specify parameters with Zod schemas
   
3. Call the Express endpoint with `fetch()`
   
4. Format the response for MCP
   
5. Handle errors with clear messages
   

## 5\. Mount the MCP handler on Express

The `mcp-handler` expects Web API Request/Response objects. Since Express uses its own req/res format, add this adapter after your MCP handler to handle this transformation.

```typescript
// Helper: Convert Express request to Web Request
function toWebRequest(req: express.Request): Request {
  const url = `http://${req.headers.host}${req.url}`;
  return new Request(url, {
    method: req.method,
    headers: req.headers as HeadersInit,
    body: req.method !== 'GET' && req.method !== 'HEAD'
      ? JSON.stringify(req.body)
      : undefined,
  });
}

// Mount MCP handler with proper conversion
app.all('/api/mcp', async (req, res) => {
  try {
    const webRequest = toWebRequest(req);
    const webResponse = await mcpHandler(webRequest);

    // Convert Web Response back to Express response
    res.status(webResponse.status);
    webResponse.headers.forEach((value, key) => {
      res.setHeader(key, value);
    });

    const body = await webResponse.text();
    res.send(body);
  } catch (error) {
    console.error('MCP handler error:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});
```

The adapter:

- Converts Express `req` to Web API `Request`
  
- Calls MCP handler
  
- Converts Web API `Response` to Express `res`
  

## 6\. Test the MCP server locally

Your server is already running at `http://localhost:3000` using `vercel dev`.

Test the MCP Server:

```bash
curl -X POST http://localhost:3000/api/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

You should see all four tools listed.

## 7\. Test with the MCP inspector

The [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) provides a visual interface for testing any MCP server. Start it in a new terminal:

```bash
npx @modelcontextprotocol/inspector@latest http://localhost:3000
```

The inspector runs at `http://localhost:6274`. Check your terminal for the URL.

**Connect to your MCP server:**

1. Open `http://localhost:6274`
   
2. Select "Streamable HTTP"
   
3. In the URL field, enter: `http://localhost:3001/api/mcp`
   
4. Make sure that Authentication is not enabled
   
5. Click **Connect**
   

**Test your tools:**

1. Click **List Tools** - see all four weather tools
   
2. Select "get\_temperature"
   
3. Type a city like "London"
   
4. Click **Run Tool**
   
5. See the result:
   

```text
Temperature in London, United Kingdom:
- Current: 8.9°C
- Feels like: 6.3°C
```

Test each tool:

- `get_temperature`: Paris (metric), New York (imperial)
  
- `get_humidity`: Tokyo
  
- `get_wind_speed`: Berlin
  
- `get_full_weather`: London
  

## 8\. Configure the Cursor IDE

In a sample folder that you open in your Cursor IDE, create a `mcp.json` file in the `.cursor` `folder` and paste:

```json
{
  "mcpServers": {
    "weather": {
      "url": "http://localhost:3000/api/mcp"
    }
  }
}
```

Cursor shows a dialog to enable the MCP server. Restart Cursor if needed.

Ask the following example questions in Cursor Chat:

- "What's the temperature in Paris?"
  
- "Give me complete weather for Tokyo"
  
- "Compare humidity in London and Berlin"
  
- "What's the wind speed in San Francisco in mph?"
  

Cursor detects when to use tools. It shows a dialog like "Run get\_temperature" with extracted parameters like "London". Choose "allowlist" or "run" to proceed.

For the question about comparison, Cursor runs "get\_humidity" twice (once per city) and returns the comparison.

## Deploy your MCP server

To deploy your project to production, run `vercel --prod` in your project folder. Update `.cursor/mcp.json` with the deployed production URL:

```json
{
  "mcpServers": {
    "weather": {
      "url": "<https://your-app.vercel.app/api/mcp>"
    }
  }
}
```

## Summary

In this tutorial, you enabled MCP with your Express API in a single Express project that you deployed to Vercel that can be used by an AI agent that supports MCP.

### Next steps for exploration:

- Add tools for more endpoints
  
- Implement OAuth authentication for security
  
- Create tools that combine API calls
  

## Resources:

- [Vercel MCP documentation](https://vercel.com/docs/mcp)
  
- [MCP Inspector](https://github.com/modelcontextprotocol/inspector)
  
- [How to Build a Weather API with Express and Vercel](https://vercel.com/guides/weather-api-with-express)
  
- [Backends on Vercel](https://vercel.com/docs/frameworks/backend)