index.ts•12.6 kB
#!/usr/bin/env node
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import express, { Request, Response } from 'express';
import { z } from 'zod';
import { configure } from '@vendia/serverless-express';
// Create an MCP server
const server = new McpServer({
name: 'weather-mcp',
version: '1.0.0'
});
// Hardcoded cities database for testing
const CITIES_DATABASE = [
{ id: 1, cityName: 'New York', country: 'USA', latitude: 40.7128, longitude: -74.0060, population: 8336817 },
{ id: 2, cityName: 'London', country: 'UK', latitude: 51.5074, longitude: -0.1278, population: 8982000 },
{ id: 3, cityName: 'Paris', country: 'France', latitude: 48.8566, longitude: 2.3522, population: 2161000 },
{ id: 4, cityName: 'Tokyo', country: 'Japan', latitude: 35.6762, longitude: 139.6503, population: 13960000 },
{ id: 5, cityName: 'Sydney', country: 'Australia', latitude: -33.8688, longitude: 151.2093, population: 5312000 },
{ id: 6, cityName: 'Berlin', country: 'Germany', latitude: 52.5200, longitude: 13.4050, population: 3645000 },
{ id: 7, cityName: 'Toronto', country: 'Canada', latitude: 43.6532, longitude: -79.3832, population: 2930000 },
{ id: 8, cityName: 'Mumbai', country: 'India', latitude: 19.0760, longitude: 72.8777, population: 20411000 },
{ id: 9, cityName: 'São Paulo', country: 'Brazil', latitude: -23.5505, longitude: -46.6333, population: 12326000 },
{ id: 10, cityName: 'Moscow', country: 'Russia', latitude: 55.7558, longitude: 37.6173, population: 11920000 },
{ id: 11, cityName: 'Dubai', country: 'UAE', latitude: 25.2048, longitude: 55.2708, population: 3331000 },
{ id: 12, cityName: 'Singapore', country: 'Singapore', latitude: 1.3521, longitude: 103.8198, population: 5454000 },
{ id: 13, cityName: 'Barcelona', country: 'Spain', latitude: 41.3851, longitude: 2.1734, population: 1636000 },
{ id: 14, cityName: 'Rome', country: 'Italy', latitude: 41.9028, longitude: 12.4964, population: 2873000 },
{ id: 15, cityName: 'Amsterdam', country: 'Netherlands', latitude: 52.3676, longitude: 4.9041, population: 872000 },
{ id: 16, cityName: 'Istanbul', country: 'Turkey', latitude: 41.0082, longitude: 28.9784, population: 15460000 },
{ id: 17, cityName: 'Los Angeles', country: 'USA', latitude: 34.0522, longitude: -118.2437, population: 3980000 },
{ id: 18, cityName: 'Mexico City', country: 'Mexico', latitude: 19.4326, longitude: -99.1332, population: 9209000 },
{ id: 19, cityName: 'Seoul', country: 'South Korea', latitude: 37.5665, longitude: 126.9780, population: 9776000 },
{ id: 20, cityName: 'Bangkok', country: 'Thailand', latitude: 13.7563, longitude: 100.5018, population: 10539000 }
];
// Register the city search tool that returns resources
server.registerTool(
'searchCities',
{
title: 'Search Cities',
description: 'Search for cities by name and return matching results as resources',
inputSchema: {
query: z.string().describe('City name or partial name to search for')
},
outputSchema: {
results: z.array(z.object({
id: z.number(),
cityName: z.string(),
country: z.string(),
latitude: z.number(),
longitude: z.number(),
population: z.number()
})),
totalResults: z.number()
}
},
async ({ query }) => {
// Filter cities that match the query (case-insensitive)
const matchingCities = CITIES_DATABASE.filter(city =>
city.cityName.toLowerCase().includes(query.toLowerCase())
);
const output = {
results: matchingCities,
totalResults: matchingCities.length
};
// Return as resources - each city is a separate resource
const resourceContent = matchingCities.map(city => ({
type: 'resource' as const,
resource: {
uri: `city://${city.id}`,
name: city.cityName,
description: `${city.cityName}, ${city.country}`,
mimeType: 'application/json',
text: JSON.stringify(city, null, 2)
}
}));
return {
content: resourceContent,
structuredContent: output
};
}
);
// Register the Fahrenheit to Celsius conversion tool (with outputSchema)
server.registerTool(
'transformFahrenheitToCelsius',
{
title: 'Fahrenheit to Celsius Converter',
description: 'Convert temperature from Fahrenheit to Celsius',
inputSchema: {
fahrenheit: z.number().describe('Temperature in Fahrenheit')
},
outputSchema: {
celsius: z.number(),
fahrenheit: z.number(),
formatted: z.string()
}
},
async ({ fahrenheit }) => {
const celsius = ((fahrenheit - 32) * 5) / 9;
const output = {
celsius: parseFloat(celsius.toFixed(2)),
fahrenheit,
formatted: `${fahrenheit}°F is equal to ${celsius.toFixed(2)}°C`
};
return {
content: [{ type: 'text', text: JSON.stringify(output) }],
structuredContent: output
};
}
);
// Register the Fahrenheit to Celsius conversion tool (without outputSchema)
server.registerTool(
'transformFahrenheitToCelsiusNoSchema',
{
title: 'Fahrenheit to Celsius Converter (No Schema)',
description: 'Convert temperature from Fahrenheit to Celsius without outputSchema',
inputSchema: {
fahrenheit: z.number().describe('Temperature in Fahrenheit')
}
},
async ({ fahrenheit }) => {
const celsius = ((fahrenheit - 32) * 5) / 9;
const output = {
celsius: parseFloat(celsius.toFixed(2)),
fahrenheit,
formatted: `${fahrenheit}°F is equal to ${celsius.toFixed(2)}°C`
};
return {
content: [{ type: 'text', text: JSON.stringify(output) }]
};
}
);
// Register the city search tool without outputSchema
server.registerTool(
'searchCitiesNoSchema',
{
title: 'Search Cities (No Schema)',
description: 'Search for cities by name and return matching results as resources without outputSchema',
inputSchema: {
query: z.string().describe('City name or partial name to search for')
}
},
async ({ query }) => {
// Filter cities that match the query (case-insensitive)
const matchingCities = CITIES_DATABASE.filter(city =>
city.cityName.toLowerCase().includes(query.toLowerCase())
);
// Return as resources - each city is a separate resource
const resourceContent = matchingCities.map(city => ({
type: 'resource' as const,
resource: {
uri: `city://${city.id}`,
name: city.cityName,
description: `${city.cityName}, ${city.country}`,
mimeType: 'application/json',
text: JSON.stringify(city, null, 2)
}
}));
return {
content: resourceContent
};
}
);
// Register the city search tool with text content and outputSchema
server.registerTool(
'searchCitiesText',
{
title: 'Search Cities (Text)',
description: 'Search for cities by name and return matching results as text with outputSchema',
inputSchema: {
query: z.string().describe('City name or partial name to search for')
},
outputSchema: {
results: z.array(z.object({
id: z.number(),
cityName: z.string(),
country: z.string(),
latitude: z.number(),
longitude: z.number(),
population: z.number()
})),
totalResults: z.number()
}
},
async ({ query }) => {
// Filter cities that match the query (case-insensitive)
const matchingCities = CITIES_DATABASE.filter(city =>
city.cityName.toLowerCase().includes(query.toLowerCase())
);
const output = {
results: matchingCities,
totalResults: matchingCities.length
};
// Return as text - single text content with all cities
return {
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
structuredContent: output
};
}
);
// Register the city search tool with text content and no outputSchema
server.registerTool(
'searchCitiesNoSchemaText',
{
title: 'Search Cities (No Schema, Text)',
description: 'Search for cities by name and return matching results as text without outputSchema',
inputSchema: {
query: z.string().describe('City name or partial name to search for')
}
},
async ({ query }) => {
// Filter cities that match the query (case-insensitive)
const matchingCities = CITIES_DATABASE.filter(city =>
city.cityName.toLowerCase().includes(query.toLowerCase())
);
const output = {
results: matchingCities,
totalResults: matchingCities.length
};
// Return as text - single text content with all cities
return {
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }]
};
}
);
// Register the random image fetching tool
server.registerTool(
'getRandomImage',
{
title: 'Get Random Image',
description: 'Fetch a random image from picsum.photos with specified dimensions and return as base64-encoded image',
inputSchema: {
width: z.number().int().min(1).max(5000).describe('Image width in pixels'),
height: z.number().int().min(1).max(5000).describe('Image height in pixels')
}
},
async ({ width, height }) => {
try {
// Fetch image from picsum.photos
const url = `https://picsum.photos/${width}/${height}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch image: ${response.statusText}`);
}
// Get the image as array buffer
const arrayBuffer = await response.arrayBuffer();
// Convert to base64
const buffer = Buffer.from(arrayBuffer);
const base64Data = buffer.toString('base64');
// Get content type from response headers (picsum returns jpeg)
const mimeType = response.headers.get('content-type') || 'image/jpeg';
return {
content: [
{
type: 'image',
data: base64Data,
mimeType: mimeType
}
]
};
} catch (error) {
// Return error as text content
return {
content: [
{
type: 'text',
text: `Error fetching image: ${error instanceof Error ? error.message : String(error)}`
}
],
isError: true
};
}
}
);
// Set up Express and HTTP transport
const app = express();
app.use(express.json());
app.post('/mcp', async (req: Request, res: Response) => {
// Create a new transport for each request to prevent request ID collisions
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true
});
res.on('close', () => {
transport.close();
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
// Health check endpoint
app.get('/health', (req: Request, res: Response) => {
res.json({ status: 'ok', service: 'weather-mcp' });
});
// Export handler for Lambda
export const handler = configure({ app });
// Local development server
if (process.env.NODE_ENV !== 'production' || !process.env.AWS_LAMBDA_FUNCTION_NAME) {
const port = parseInt(process.env.PORT || '3000');
app.listen(port, () => {
console.log(`Weather MCP Server running on http://localhost:${port}/mcp`);
}).on('error', error => {
console.error('Server error:', error);
process.exit(1);
});
}