Skip to main content
Glama
dstotijn

KNMI MCP Server

by dstotijn
index.ts8.34 kB
#!/usr/bin/env node /** * Copyright 2025 David Stotijn * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { KnmiApi } from "./lib/knmi-api.js"; const knmiApi = new KnmiApi({ environment: "production" }); const server = new McpServer({ name: "knmi-mcp", version: "0.1.0", }); // Define the weather output schema based on the OpenAPI spec const weatherOutputSchema = z.object({ backgrounds: z .array( z.object({ dateTime: z .string() .describe( "Date of the background, the time from when this background is to be used", ), sky: z.enum([ "dayClear", "dayLightCloud", "dayMediumCloud", "dayHeavyCloud", "nightClear", "nightMediumCloud", "nightHeavyCloud", ]), celestial: z.enum(["sun", "moon"]).optional(), clouds: z .enum([ "dayMist", "dayLightCloud", "dayMediumCloud", "dayHeavyCloud", "dayHeavyCloudLightning", "nightMist", "nightLightCloud", "nightMediumCloud", "nightHeavyCloud", "nightHeavyCloudLightning", ]) .optional(), precipitation: z .enum([ "lightRain", "mediumRain", "heavyRain", "hail", "lightSnow", "mediumSnow", "heavySnow", ]) .optional(), }), ) .optional() .describe("The weather backgrounds to be used"), summaries: z .array( z.object({ dateTime: z .string() .describe( "Date of the summary, the time from when this summary is to be used", ), temperature: z .number() .optional() .describe("Current temperature in degrees Celsius"), }), ) .describe("Summary of the current weather conditions to be used"), alerts: z .array( z.object({ level: z .enum(["none", "potential", "yellow", "orange", "red"]) .describe("The alert level"), title: z.string().describe("The title of the alert"), description: z .string() .describe("The description of the alert"), }), ) .describe("Alerts for this location in the next 48 hours"), hourly: z .object({ forecast: z .array( z.object({ dateTime: z.string().describe("Date of the forecast"), alertLevel: z .enum([ "none", "potential", "yellow", "orange", "red", ]) .optional() .describe( "Highest alert level active for the hour", ), weatherType: z .number() .describe("Type of weather condition"), temperature: z .number() .describe("Temperature for the hour"), precipitation: z .object({ amount: z .number() .describe( "Amount of rain in millimeter per hour", ), chance: z .number() .describe("Chance of rain as a percentage"), }) .describe("Precipitation information for the hour"), wind: z .object({ source: z.enum([ "N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW", "VAR", ]), speed: z .number() .describe( "The wind speed in kilometers/hour", ), gusts: z .number() .describe( "The wind gusts in kilometers/hour", ), degree: z .number() .optional() .describe( "Wind source in degrees (meteorological convention, 0 degrees represents north)", ), beaufort: z .number() .describe( "The wind speed according to the Beaufort scale", ), }) .optional() .describe("Wind information for the hour"), }), ) .describe("Entries with weather forecast"), }) .optional() .describe("Hourly forecast for the weather"), daily: z .object({ forecast: z .array( z.object({ date: z.string().describe("Date of the forecast"), alertLevels: z .array( z.enum([ "none", "potential", "yellow", "orange", "red", ]), ) .optional() .describe("All alert levels active for the day"), weatherType: z .number() .optional() .describe("Type of weather condition"), temperature: z .object({ min: z .number() .optional() .describe( "The minimum temperature in degrees Celsius", ), max: z .number() .optional() .describe( "The maximum temperature in degrees Celsius", ), }) .describe("Temperature range for the day"), precipitation: z .object({ amount: z .number() .describe( "Amount of rain in millimeter per hour", ), chance: z .number() .describe("Chance of rain as a percentage"), }) .describe("Precipitation information for the day"), }), ) .describe("Entries with weather forecast"), }) .optional() .describe("Daily forecast for the weather"), sun: z .object({ sunrise: z.string().describe("The time of sunrise"), sunset: z.string().describe("The time of sunset"), }) .optional() .describe("Sunrise and sunset times"), wind: z .object({ source: z.enum([ "N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW", "VAR", ]), speed: z.number().describe("The wind speed in kilometers/hour"), gusts: z.number().describe("The wind gusts in kilometers/hour"), degree: z .number() .optional() .describe( "Wind source in degrees (meteorological convention, 0 degrees represents north)", ), beaufort: z .number() .describe("The wind speed according to the Beaufort scale"), }) .optional() .describe("Wind information for the current weather"), uvIndex: z .object({ value: z.number().describe("UV index value"), summary: z .string() .describe("A human-readable description of the UV index"), }) .optional() .describe("UV index data for the current weather"), }); server.registerTool( "get_weather", { title: "Get Weather", description: "Get current weather information for a Dutch location. The location parameter accepts location names (e.g., 'Amsterdam', 'De Bilt') and automatically converts them to the required grid identifiers.", inputSchema: { location: z .string() .describe("Location name (e.g., 'Amsterdam', 'De Bilt')"), region: z .number() .int() .min(0) .max(15) .optional() .describe("Optional region number (0-15)"), }, outputSchema: weatherOutputSchema.shape, annotations: { readOnlyHint: true, }, }, async ({ location, region }) => { try { const weatherData = await knmiApi.getWeather(location, region); return { content: [ { type: "text", text: JSON.stringify(weatherData), }, ], structuredContent: weatherData, }; } catch (error) { return { content: [ { type: "text", text: `Error fetching weather data: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }, ); const transport = new StdioServerTransport(); console.error("Starting MCP server..."); await server.connect(transport);

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/dstotijn/knmi-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server