index.ts•6.16 kB
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { z } from "zod";
import express from "express";
import { randomUUID } from "crypto";
// Weather API endpoint
const WEATHER_API = "https://api.open-meteo.com/v1/forecast";
// Define the input schema for our weather tool
const WeatherInputSchema = z.object({
latitude: z.number().describe("Latitude coordinate"),
longitude: z.number().describe("Longitude coordinate"),
});
// Create the MCP server instance
const server = new McpServer({
name: "weather-server",
version: "1.0.0",
capabilities: {
tools: {},
},
});
// Helper function to fetch weather data
async function fetchWeatherData(latitude: number, longitude: number) {
const params = new URLSearchParams({
latitude: latitude.toString(),
longitude: longitude.toString(),
current: "temperature_2m,wind_speed_10m",
hourly: "temperature_2m,relative_humidity_2m,wind_speed_10m",
});
const url = `${WEATHER_API}?${params.toString()}`;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Weather API returned status ${response.status}`);
}
return await response.json();
} catch (error) {
throw new Error(
`Failed to fetch weather data: ${
error instanceof Error ? error.message : String(error)
}`
);
}
}
// Register the get_weather tool
server.registerTool(
"get_weather",
{
title: "Get Weather Data",
description:
"Fetches current weather and hourly forecast from Open-Meteo API for given coordinates",
inputSchema: {
latitude: z
.number()
.describe("Latitude coordinate (e.g., 52.52 for Berlin)"),
longitude: z
.number()
.describe("Longitude coordinate (e.g., 13.41 for Berlin)"),
},
},
async ({ latitude, longitude }) => {
try {
const weatherData = await fetchWeatherData(latitude, longitude);
// Format the response
const current = weatherData.current || {};
const hourly = weatherData.hourly || {};
let responseText = `Weather data for coordinates (${latitude}, ${longitude}):\n\n`;
responseText += `Current conditions:\n`;
responseText += `- Temperature: ${current.temperature_2m}°C\n`;
responseText += `- Wind Speed: ${current.wind_speed_10m} km/h\n\n`;
responseText += `Hourly forecast available for:\n`;
responseText += `- Temperature (${
hourly.temperature_2m?.length || 0
} hours)\n`;
responseText += `- Relative Humidity (${
hourly.relative_humidity_2m?.length || 0
} hours)\n`;
responseText += `- Wind Speed (${
hourly.wind_speed_10m?.length || 0
} hours)\n`;
// Include raw data for reference
responseText += `\nRaw API Response:\n${JSON.stringify(
weatherData,
null,
2
)}`;
return {
content: [
{
type: "text",
text: responseText,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error fetching weather data: ${
error instanceof Error ? error.message : String(error)
}`,
},
],
isError: true,
};
}
}
);
// HTTP Server setup
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.json());
// Store active transports by session ID
const transports = new Map<string, StreamableHTTPServerTransport>();
// Health check endpoint
app.get("/health", (_req, res) => {
res.json({ status: "ok", server: "weather-mcp-server" });
});
// Main MCP endpoint
app.post("/mcp", async (req, res) => {
const sessionId = req.headers["mcp-session-id"] as string | undefined;
let transport: StreamableHTTPServerTransport;
if (sessionId && transports.has(sessionId)) {
// Use existing transport for this session
transport = transports.get(sessionId)!;
} else {
// Create new transport for new session
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (newSessionId) => {
transports.set(newSessionId, transport);
console.log(`New session initialized: ${newSessionId}`);
},
});
transport.onclose = () => {
if (transport.sessionId) {
transports.delete(transport.sessionId);
console.log(`Session closed: ${transport.sessionId}`);
}
};
await server.connect(transport);
}
await transport.handleRequest(req, res, req.body);
});
// GET endpoint for SSE (Server-Sent Events)
app.get("/mcp", async (req, res) => {
const sessionId = req.headers["mcp-session-id"] as string | undefined;
if (!sessionId || !transports.has(sessionId)) {
res.status(400).json({ error: "Invalid or missing session ID" });
return;
}
const transport = transports.get(sessionId)!;
await transport.handleRequest(req, res);
});
// DELETE endpoint to close sessions
app.delete("/mcp", async (req, res) => {
const sessionId = req.headers["mcp-session-id"] as string | undefined;
if (!sessionId || !transports.has(sessionId)) {
res.status(400).json({ error: "Invalid or missing session ID" });
return;
}
const transport = transports.get(sessionId)!;
await transport.handleRequest(req, res);
});
// Start the HTTP server
async function main() {
app.listen(PORT, () => {
console.log(`\n🌤️ Weather MCP Server running on HTTP`);
console.log(`📡 Endpoint: http://localhost:${PORT}/mcp`);
console.log(`🏥 Health check: http://localhost:${PORT}/health`);
console.log(`\nPress Ctrl+C to stop the server\n`);
});
}
main().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});