# Build an MCP Server with Weather tools using Express and Vercel

**Author:** Jeff See, Ismael Rumzan

---

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`:

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

## 2\. Include MCP dependencies

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

`"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:

``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: `vercel dev` In another terminal: `# 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: ``// 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. ``// 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: `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:

`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:
   

`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:

`{ "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:

`{ "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)

---

[View full KB sitemap](/kb/sitemap.md)
