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.
The complete code for this guide is available in this Github repository. You can deploy it with one click on Vercel.
Before you begin, ensure you have:
- Node.js 20 or later installed
- Vercel CLI (
npm install -g vercel) - Basic knowledge of Express and TypeScript
Adding MCP to Express creates two layers:
- Express REST API: Your endpoints work as normal HTTP routes. They handle business logic, fetch data, process requests, and return JSON.
- 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.
Create a new Express project with the Vercel CLI inside a folder called express-mcp-weather:
vc init express express-mcp-weathercd express-mcp-weatherUpdate 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 adapterzod: Schema validation (version 3.23.8 required)
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 devIn another terminal:
# Test with default metric unitscurl http://localhost:3000/api/weather/london
# Test with imperial unitscurl "http://localhost:3000/api/weather/san%20francisco?units=imperial"You should see weather data in JSON format.
Add MCP tools to src/index.ts after the weather endpoint:
// This goes at the top of the fileimport { z } from 'zod';import { createMcpHandler } from 'mcp-handler';
// This goes below the weather endpoint, // Helper function: Call your own Express APIasync 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 toolsconst 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:
- Define name and description
- Specify parameters with Zod schemas
- Call the Express endpoint with
fetch() - Format the response for MCP
- Handle errors with clear messages
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 Requestfunction 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 conversionapp.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
reqto Web APIRequest - Calls MCP handler
- Converts Web API
Responseto Expressres
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.
The MCP Inspector provides a visual interface for testing any MCP server. Start it in a new terminal:
npx @modelcontextprotocol/inspector@latest http://localhost:3000The inspector runs at http://localhost:6274. Check your terminal for the URL.
Connect to your MCP server:
- Open
http://localhost:6274 - Select "Streamable HTTP"
- In the URL field, enter:
http://localhost:3001/api/mcp - Make sure that Authentication is not enabled
- Click Connect
Test your tools:
- Click List Tools - see all four weather tools
- Select "get_temperature"
- Type a city like "London"
- Click Run Tool
- See the result:
Temperature in London, United Kingdom:- Current: 8.9°C- Feels like: 6.3°CTest each tool:
get_temperature: Paris (metric), New York (imperial)get_humidity: Tokyoget_wind_speed: Berlinget_full_weather: London
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.
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>" } }}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.
- Add tools for more endpoints
- Implement OAuth authentication for security
- Create tools that combine API calls