|
1 | | -import { Server } from "@modelcontextprotocol/sdk/server/index.js"; |
| 1 | +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; |
2 | 2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; |
3 | | -import { |
4 | | - CallToolRequestSchema, |
5 | | - ListToolsRequestSchema, |
6 | | -} from "@modelcontextprotocol/sdk/types.js"; |
7 | 3 | import { z } from "zod"; |
8 | 4 |
|
9 | 5 | const NWS_API_BASE = "https://api.weather.gov"; |
10 | 6 | const USER_AGENT = "weather-app/1.0"; |
11 | 7 |
|
12 | | -// Define Zod schemas for validation |
13 | | -const AlertsArgumentsSchema = z.object({ |
14 | | - state: z.string().length(2), |
15 | | -}); |
16 | | - |
17 | | -const ForecastArgumentsSchema = z.object({ |
18 | | - latitude: z.number().min(-90).max(90), |
19 | | - longitude: z.number().min(-180).max(180), |
20 | | -}); |
21 | | - |
22 | | -// Create server instance |
23 | | -const server = new Server( |
24 | | - { |
25 | | - name: "weather", |
26 | | - version: "1.0.0", |
27 | | - }, |
28 | | - { |
29 | | - capabilities: { |
30 | | - tools: {}, |
31 | | - }, |
32 | | - } |
33 | | -); |
34 | | - |
35 | | -// List available tools |
36 | | -server.setRequestHandler(ListToolsRequestSchema, async () => { |
37 | | - return { |
38 | | - tools: [ |
39 | | - { |
40 | | - name: "get-alerts", |
41 | | - description: "Get weather alerts for a state", |
42 | | - inputSchema: { |
43 | | - type: "object", |
44 | | - properties: { |
45 | | - state: { |
46 | | - type: "string", |
47 | | - description: "Two-letter state code (e.g. CA, NY)", |
48 | | - }, |
49 | | - }, |
50 | | - required: ["state"], |
51 | | - }, |
52 | | - }, |
53 | | - { |
54 | | - name: "get-forecast", |
55 | | - description: "Get weather forecast for a location", |
56 | | - inputSchema: { |
57 | | - type: "object", |
58 | | - properties: { |
59 | | - latitude: { |
60 | | - type: "number", |
61 | | - description: "Latitude of the location", |
62 | | - }, |
63 | | - longitude: { |
64 | | - type: "number", |
65 | | - description: "Longitude of the location", |
66 | | - }, |
67 | | - }, |
68 | | - required: ["latitude", "longitude"], |
69 | | - }, |
70 | | - }, |
71 | | - ], |
72 | | - }; |
73 | | -}); |
74 | | - |
75 | 8 | // Helper function for making NWS API requests |
76 | 9 | async function makeNWSRequest<T>(url: string): Promise<T | null> { |
77 | 10 | const headers = { |
@@ -139,152 +72,148 @@ interface ForecastResponse { |
139 | 72 | }; |
140 | 73 | } |
141 | 74 |
|
142 | | -// Handle tool execution |
143 | | -server.setRequestHandler(CallToolRequestSchema, async (request) => { |
144 | | - const { name, arguments: args } = request.params; |
145 | | - |
146 | | - try { |
147 | | - if (name === "get-alerts") { |
148 | | - const { state } = AlertsArgumentsSchema.parse(args); |
149 | | - const stateCode = state.toUpperCase(); |
150 | | - |
151 | | - const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`; |
152 | | - const alertsData = await makeNWSRequest<AlertsResponse>(alertsUrl); |
153 | | - |
154 | | - if (!alertsData) { |
155 | | - return { |
156 | | - content: [ |
157 | | - { |
158 | | - type: "text", |
159 | | - text: "Failed to retrieve alerts data", |
160 | | - }, |
161 | | - ], |
162 | | - }; |
163 | | - } |
164 | | - |
165 | | - const features = alertsData.features || []; |
166 | | - if (features.length === 0) { |
167 | | - return { |
168 | | - content: [ |
169 | | - { |
170 | | - type: "text", |
171 | | - text: `No active alerts for ${stateCode}`, |
172 | | - }, |
173 | | - ], |
174 | | - }; |
175 | | - } |
| 75 | +// Create server instance |
| 76 | +const server = new McpServer({ |
| 77 | + name: "weather", |
| 78 | + version: "1.0.0", |
| 79 | +}); |
176 | 80 |
|
177 | | - const formattedAlerts = features.map(formatAlert); |
178 | | - const alertsText = `Active alerts for ${stateCode}:\n\n${formattedAlerts.join( |
179 | | - "\n" |
180 | | - )}`; |
| 81 | +// Register weather tools |
| 82 | +server.tool( |
| 83 | + "get-alerts", |
| 84 | + "Get weather alerts for a state", |
| 85 | + { |
| 86 | + state: z.string().length(2).describe("Two-letter state code (e.g. CA, NY)"), |
| 87 | + }, |
| 88 | + async ({ state }) => { |
| 89 | + const stateCode = state.toUpperCase(); |
| 90 | + const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`; |
| 91 | + const alertsData = await makeNWSRequest<AlertsResponse>(alertsUrl); |
181 | 92 |
|
| 93 | + if (!alertsData) { |
182 | 94 | return { |
183 | 95 | content: [ |
184 | 96 | { |
185 | 97 | type: "text", |
186 | | - text: alertsText, |
| 98 | + text: "Failed to retrieve alerts data", |
187 | 99 | }, |
188 | 100 | ], |
189 | 101 | }; |
190 | | - } else if (name === "get-forecast") { |
191 | | - const { latitude, longitude } = ForecastArgumentsSchema.parse(args); |
192 | | - |
193 | | - // Get grid point data |
194 | | - const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed( |
195 | | - 4 |
196 | | - )},${longitude.toFixed(4)}`; |
197 | | - const pointsData = await makeNWSRequest<PointsResponse>(pointsUrl); |
| 102 | + } |
198 | 103 |
|
199 | | - if (!pointsData) { |
200 | | - return { |
201 | | - content: [ |
202 | | - { |
203 | | - type: "text", |
204 | | - text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).`, |
205 | | - }, |
206 | | - ], |
207 | | - }; |
208 | | - } |
| 104 | + const features = alertsData.features || []; |
| 105 | + if (features.length === 0) { |
| 106 | + return { |
| 107 | + content: [ |
| 108 | + { |
| 109 | + type: "text", |
| 110 | + text: `No active alerts for ${stateCode}`, |
| 111 | + }, |
| 112 | + ], |
| 113 | + }; |
| 114 | + } |
209 | 115 |
|
210 | | - const forecastUrl = pointsData.properties?.forecast; |
211 | | - if (!forecastUrl) { |
212 | | - return { |
213 | | - content: [ |
214 | | - { |
215 | | - type: "text", |
216 | | - text: "Failed to get forecast URL from grid point data", |
217 | | - }, |
218 | | - ], |
219 | | - }; |
220 | | - } |
| 116 | + const formattedAlerts = features.map(formatAlert); |
| 117 | + const alertsText = `Active alerts for ${stateCode}:\n\n${formattedAlerts.join("\n")}`; |
221 | 118 |
|
222 | | - // Get forecast data |
223 | | - const forecastData = await makeNWSRequest<ForecastResponse>(forecastUrl); |
224 | | - if (!forecastData) { |
225 | | - return { |
226 | | - content: [ |
227 | | - { |
228 | | - type: "text", |
229 | | - text: "Failed to retrieve forecast data", |
230 | | - }, |
231 | | - ], |
232 | | - }; |
233 | | - } |
| 119 | + return { |
| 120 | + content: [ |
| 121 | + { |
| 122 | + type: "text", |
| 123 | + text: alertsText, |
| 124 | + }, |
| 125 | + ], |
| 126 | + }; |
| 127 | + }, |
| 128 | +); |
234 | 129 |
|
235 | | - const periods = forecastData.properties?.periods || []; |
236 | | - if (periods.length === 0) { |
237 | | - return { |
238 | | - content: [ |
239 | | - { |
240 | | - type: "text", |
241 | | - text: "No forecast periods available", |
242 | | - }, |
243 | | - ], |
244 | | - }; |
245 | | - } |
| 130 | +server.tool( |
| 131 | + "get-forecast", |
| 132 | + "Get weather forecast for a location", |
| 133 | + { |
| 134 | + latitude: z.number().min(-90).max(90).describe("Latitude of the location"), |
| 135 | + longitude: z |
| 136 | + .number() |
| 137 | + .min(-180) |
| 138 | + .max(180) |
| 139 | + .describe("Longitude of the location"), |
| 140 | + }, |
| 141 | + async ({ latitude, longitude }) => { |
| 142 | + // Get grid point data |
| 143 | + const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`; |
| 144 | + const pointsData = await makeNWSRequest<PointsResponse>(pointsUrl); |
246 | 145 |
|
247 | | - // Format forecast periods |
248 | | - const formattedForecast = periods.map((period: ForecastPeriod) => |
249 | | - [ |
250 | | - `${period.name || "Unknown"}:`, |
251 | | - `Temperature: ${period.temperature || "Unknown"}°${ |
252 | | - period.temperatureUnit || "F" |
253 | | - }`, |
254 | | - `Wind: ${period.windSpeed || "Unknown"} ${ |
255 | | - period.windDirection || "" |
256 | | - }`, |
257 | | - `${period.shortForecast || "No forecast available"}`, |
258 | | - "---", |
259 | | - ].join("\n") |
260 | | - ); |
| 146 | + if (!pointsData) { |
| 147 | + return { |
| 148 | + content: [ |
| 149 | + { |
| 150 | + type: "text", |
| 151 | + text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).`, |
| 152 | + }, |
| 153 | + ], |
| 154 | + }; |
| 155 | + } |
261 | 156 |
|
262 | | - const forecastText = `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join( |
263 | | - "\n" |
264 | | - )}`; |
| 157 | + const forecastUrl = pointsData.properties?.forecast; |
| 158 | + if (!forecastUrl) { |
| 159 | + return { |
| 160 | + content: [ |
| 161 | + { |
| 162 | + type: "text", |
| 163 | + text: "Failed to get forecast URL from grid point data", |
| 164 | + }, |
| 165 | + ], |
| 166 | + }; |
| 167 | + } |
265 | 168 |
|
| 169 | + // Get forecast data |
| 170 | + const forecastData = await makeNWSRequest<ForecastResponse>(forecastUrl); |
| 171 | + if (!forecastData) { |
266 | 172 | return { |
267 | 173 | content: [ |
268 | 174 | { |
269 | 175 | type: "text", |
270 | | - text: forecastText, |
| 176 | + text: "Failed to retrieve forecast data", |
271 | 177 | }, |
272 | 178 | ], |
273 | 179 | }; |
274 | | - } else { |
275 | | - throw new Error(`Unknown tool: ${name}`); |
276 | 180 | } |
277 | | - } catch (error) { |
278 | | - if (error instanceof z.ZodError) { |
279 | | - throw new Error( |
280 | | - `Invalid arguments: ${error.errors |
281 | | - .map((e) => `${e.path.join(".")}: ${e.message}`) |
282 | | - .join(", ")}` |
283 | | - ); |
| 181 | + |
| 182 | + const periods = forecastData.properties?.periods || []; |
| 183 | + if (periods.length === 0) { |
| 184 | + return { |
| 185 | + content: [ |
| 186 | + { |
| 187 | + type: "text", |
| 188 | + text: "No forecast periods available", |
| 189 | + }, |
| 190 | + ], |
| 191 | + }; |
284 | 192 | } |
285 | | - throw error; |
286 | | - } |
287 | | -}); |
| 193 | + |
| 194 | + // Format forecast periods |
| 195 | + const formattedForecast = periods.map((period: ForecastPeriod) => |
| 196 | + [ |
| 197 | + `${period.name || "Unknown"}:`, |
| 198 | + `Temperature: ${period.temperature || "Unknown"}°${period.temperatureUnit || "F"}`, |
| 199 | + `Wind: ${period.windSpeed || "Unknown"} ${period.windDirection || ""}`, |
| 200 | + `${period.shortForecast || "No forecast available"}`, |
| 201 | + "---", |
| 202 | + ].join("\n"), |
| 203 | + ); |
| 204 | + |
| 205 | + const forecastText = `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join("\n")}`; |
| 206 | + |
| 207 | + return { |
| 208 | + content: [ |
| 209 | + { |
| 210 | + type: "text", |
| 211 | + text: forecastText, |
| 212 | + }, |
| 213 | + ], |
| 214 | + }; |
| 215 | + }, |
| 216 | +); |
288 | 217 |
|
289 | 218 | // Start the server |
290 | 219 | async function main() { |
|
0 commit comments