Build an MCP Server with Weather tools using Express and Vercel

Make your Express weather API accessible to AI assistants through the Model Context Protocol.

Avatar for jeffsee-vercelcomJeff SeeSoftware Engineer
Avatar for ismaelrumzanIsmael RumzanDX Engineer
Guides/Backends
3 min read
Last updated November 3, 2025

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:

  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.

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

terminal
vc init express express-mcp-weather
cd express-mcp-weather

Update your package.json with the following dependencies:

package.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)

Replace src/index.ts with a weather endpoint:

src/index.ts
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:

terminal
vercel dev

In another terminal:

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.

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

src/index.ts
// 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

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.

src/index.ts
// 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

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

Test the MCP Server:

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

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

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

.cursor/mcp.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.

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

.cursor/mcp.json
{
"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

Was this helpful?

supported.