mcp-server-tempest 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,615 @@
1
+ """
2
+ WeatherFlow Tempest MCP Server
3
+
4
+ This module provides a Model Context Protocol (MCP) server for accessing WeatherFlow Tempest
5
+ weather station data. It offers both tools (for interactive queries) and resources
6
+ (for data access) to retrieve real-time weather observations, forecasts, and station metadata.
7
+
8
+ Features:
9
+ - Real-time weather observations from personal weather stations
10
+ - Weather forecasts and current conditions
11
+ - Station and device metadata
12
+ - Automatic caching with configurable TTL
13
+ - Support for multiple stations per user account
14
+
15
+ Setup:
16
+ 1. Get an API token from https://tempestwx.com/settings/tokens
17
+ 2. Set the WEATHERFLOW_API_TOKEN environment variable
18
+ 3. Run the server: python -m weatherflow_mcp
19
+
20
+ Environment Variables:
21
+ WEATHERFLOW_API_TOKEN: Your WeatherFlow API token (required)
22
+ WEATHERFLOW_CACHE_TTL: Cache timeout in seconds (default: 300)
23
+ WEATHERFLOW_CACHE_SIZE: Maximum cache entries (default: 100)
24
+
25
+ Example Usage:
26
+ # Get available stations
27
+ stations = await client.call_tool("get_stations")
28
+
29
+ # Get current conditions for a specific station
30
+ conditions = await client.call_tool("get_observation", {"station_id": 12345})
31
+
32
+ # Access via resources
33
+ forecast = await client.read_resource("weather://tempest/forecast/12345")
34
+ """
35
+
36
+ import os
37
+ from typing import Annotated, Any, Dict
38
+
39
+ from cachetools import TTLCache
40
+ from fastmcp import Context, FastMCP
41
+ from fastmcp.exceptions import ToolError
42
+ from pydantic import Field
43
+
44
+ from .models import (
45
+ ForecastResponse,
46
+ ObservationResponse,
47
+ StationResponse,
48
+ StationsResponse,
49
+ )
50
+
51
+ from .rest import (
52
+ api_get_forecast,
53
+ api_get_observation,
54
+ api_get_station_id,
55
+ api_get_stations,
56
+ )
57
+
58
+ cache = TTLCache(
59
+ maxsize=os.getenv("WEATHERFLOW_CACHE_SIZE", 100),
60
+ ttl=os.getenv("WEATHERFLOW_CACHE_TTL", 300),
61
+ )
62
+
63
+ # Create the MCP server
64
+ mcp = FastMCP(
65
+ name="WeatherFlow Tempest API Server",
66
+ instructions="""
67
+ WeatherFlow Tempest weather station data server.
68
+
69
+ 🚀 Quick Start:
70
+ 1. Use get_stations() to see your available weather stations
71
+ 2. Use get_observation(station_id) to get current conditions
72
+ 3. Use get_forecast(station_id) to get weather forecasts
73
+
74
+ 💡 Pro Tips:
75
+ - Data is cached for 5 minutes to improve performance
76
+ - All measurements are in the units configured for each station
77
+ - Use the 'units' or 'station_units' fields to understand the unit system
78
+ - Station IDs are found in the get_stations() response
79
+
80
+ 🔧 Available Tools:
81
+ - get_stations(): List your weather stations
82
+ - get_observation(station_id): Current weather conditions
83
+ - get_forecast(station_id): Weather forecast
84
+ - get_station_id(station_id): Station details and devices
85
+ - clear_cache(): Clear the data cache (for testing)
86
+
87
+ 📊 Resource URIs:
88
+ - weather://tempest/stations - List all stations
89
+ - weather://tempest/observations/{station_id} - Current conditions
90
+ - weather://tempest/forecast/{station_id} - Weather forecast
91
+ - weather://tempest/help - Server documentation
92
+
93
+ 🔑 Setup: Set WEATHERFLOW_API_TOKEN environment variable
94
+ Get your token at: https://tempestwx.com/settings/tokens
95
+ """,
96
+ )
97
+
98
+
99
+ async def _get_api_token(env_var: str = "WEATHERFLOW_API_TOKEN") -> str:
100
+ if not (token := os.getenv(env_var)):
101
+ raise ToolError(
102
+ f"WeatherFlow API token not configured. Please set the {env_var} environment variable. "
103
+ f"You can get an API token from https://tempestwx.com/settings/tokens"
104
+ )
105
+ return token
106
+
107
+
108
+ async def _get_stations_data(ctx: Context, use_cache: bool = True) -> StationsResponse:
109
+ """Shared logic for getting stations data."""
110
+ token = await _get_api_token()
111
+
112
+ if use_cache and "stations" in cache:
113
+ await ctx.info("Using cached station data")
114
+ return cache["stations"]
115
+
116
+ await ctx.info("Getting available stations via the Tempest API")
117
+ result = await api_get_stations(token)
118
+ cache["stations"] = StationsResponse(**result)
119
+ return cache["stations"]
120
+
121
+
122
+ async def _get_station_id_data(
123
+ station_id: int, ctx: Context, use_cache: bool = True
124
+ ) -> StationResponse:
125
+ """Shared logic for getting station ID data."""
126
+ token = await _get_api_token()
127
+
128
+ cache_id = f"station_id_{station_id}"
129
+
130
+ if use_cache and cache_id in cache:
131
+ await ctx.info(f"Using cached station data for station {station_id}")
132
+ return cache[cache_id]
133
+
134
+ await ctx.info(
135
+ f"Getting station ID data for station {station_id} via the Tempest API"
136
+ )
137
+ result = await api_get_station_id(station_id, token)
138
+ cache[cache_id] = StationResponse(**result)
139
+ return cache[cache_id]
140
+
141
+
142
+ async def _get_forecast_data(
143
+ station_id: int, ctx: Context, use_cache: bool = True
144
+ ) -> ForecastResponse:
145
+ """Shared logic for getting forecast data."""
146
+ token = await _get_api_token()
147
+
148
+ cache_id = f"forecast_{station_id}"
149
+ if use_cache and cache_id in cache:
150
+ await ctx.info(f"Using cached forecast data for station {station_id}")
151
+ return cache[cache_id]
152
+
153
+ await ctx.info(f"Getting forecast for station {station_id} via the Tempest API")
154
+ result = await api_get_forecast(station_id, token)
155
+ cache[cache_id] = ForecastResponse(**result)
156
+ return cache[cache_id]
157
+
158
+
159
+ async def _get_observation_data(
160
+ station_id: int, ctx: Context, use_cache: bool = True
161
+ ) -> ObservationResponse:
162
+ """Shared logic for getting observation data."""
163
+ token = await _get_api_token()
164
+
165
+ cache_id = f"observation_{station_id}"
166
+ if use_cache and cache_id in cache:
167
+ await ctx.info(f"Using cached observation data for station {station_id}")
168
+ return cache[cache_id]
169
+
170
+ await ctx.info(f"Getting observations for station {station_id} via the Tempest API")
171
+ result = await api_get_observation(station_id, token)
172
+ cache[cache_id] = ObservationResponse(**result)
173
+ return cache[cache_id]
174
+
175
+
176
+ @mcp.tool(
177
+ annotations={
178
+ "title": "Get Weather Stations",
179
+ "readOnlyHint": True,
180
+ "openWorldHint": True,
181
+ "idempotentHint": False,
182
+ }
183
+ )
184
+ async def get_stations(
185
+ use_cache: Annotated[
186
+ bool,
187
+ Field(
188
+ default=True,
189
+ description="Whether to use the cache to store the results of the request (default: True)",
190
+ ),
191
+ ],
192
+ ctx: Context = None,
193
+ ) -> StationsResponse:
194
+ """Get a list of all weather stations accessible with your API token.
195
+
196
+ This is typically the first function you should call to discover what weather
197
+ stations are available to you. Each station contains one or more devices that
198
+ collect different types of weather data.
199
+
200
+ The response includes comprehensive information about each station:
201
+ - Station metadata (name, location, timezone, elevation)
202
+ - Connected devices and their capabilities
203
+ - Device status and last communication times
204
+ - Station configuration and settings
205
+
206
+ **Device Types:**
207
+ - **Tempest**: All-in-one weather sensor (wind, rain, temperature, etc.)
208
+ - **Air**: Temperature, humidity, pressure, lightning detection
209
+ - **Sky**: Wind, rain, solar radiation, UV index
210
+ - **Hub**: Communication hub for other devices
211
+
212
+ **Active vs Inactive Devices:**
213
+ Devices with a `serial_number` are active and collecting data.
214
+ Devices without a `serial_number` are no longer active or have been removed.
215
+
216
+ Args:
217
+ use_cache: Whether to use cached station data. Since station configurations
218
+ rarely change, caching improves performance and reduces API calls.
219
+ Cache expires after 5 minutes.
220
+
221
+ Returns:
222
+ StationsResponse containing:
223
+ - List of stations with metadata and device information
224
+ - API status and response metadata
225
+ - Station-specific settings like units and location data
226
+
227
+ Raises:
228
+ ToolError: If API token is invalid, network request fails, or you have
229
+ no accessible stations
230
+
231
+ Example Usage:
232
+ >>> stations = await get_stations()
233
+ >>> for station in stations.stations:
234
+ >>> print(f"Station: {station.name} (ID: {station.station_id})")
235
+ >>> print(f"Location: {station.latitude}, {station.longitude}")
236
+ >>> for device in station.devices:
237
+ >>> if device.serial_number: # Active device
238
+ >>> print(f" Device: {device.device_type}")
239
+
240
+ Note:
241
+ Station IDs returned by this function are used in other tools like
242
+ get_observation(), get_forecast(), and get_station_id().
243
+ """
244
+
245
+ try:
246
+ return await _get_stations_data(ctx, use_cache)
247
+ except Exception as e:
248
+ raise ToolError(f"Request failed: {str(e)}")
249
+
250
+
251
+ @mcp.tool(
252
+ annotations={
253
+ "title": "Get Weather Station Information",
254
+ "readOnlyHint": True,
255
+ "openWorldHint": True,
256
+ "idempotentHint": False,
257
+ }
258
+ )
259
+ async def get_station_id(
260
+ station_id: Annotated[
261
+ int, Field(description="The station ID to get information for", gt=0)
262
+ ],
263
+ use_cache: Annotated[
264
+ bool,
265
+ Field(
266
+ default=True,
267
+ description="Whether to use the cache to store the results of the request (default: True)",
268
+ ),
269
+ ],
270
+ ctx: Context = None,
271
+ ) -> StationResponse:
272
+ """Get comprehensive details and configuration for a specific weather station.
273
+
274
+ This function provides in-depth information about a single weather station,
275
+ including all connected devices, detailed configuration settings, and
276
+ operational status. Use this when you need complete station metadata
277
+ beyond what get_stations() provides.
278
+
279
+ **Station Information Includes:**
280
+ - Complete station metadata (name, location, elevation, timezone)
281
+ - Detailed device inventory with specifications and status
282
+ - Station configuration and measurement units
283
+ - Device communication history and health status
284
+ - Public/private settings and sharing permissions
285
+
286
+ **Device Details Include:**
287
+ - Device type, model, and firmware version
288
+ - Serial numbers and hardware revisions
289
+ - Last communication timestamps
290
+ - Device-specific settings and capabilities
291
+ - Calibration and sensor health information
292
+
293
+ **Operational Status:**
294
+ - Online/offline status for each device
295
+ - Battery levels (for battery-powered devices)
296
+ - Signal strength and communication quality
297
+ - Data collection intervals and settings
298
+
299
+ Args:
300
+ station_id: The numeric identifier of the station. Get this from
301
+ get_stations() or from your WeatherFlow account dashboard.
302
+ use_cache: Whether to use cached station data. Station configurations
303
+ change infrequently, so caching improves performance.
304
+ Cache expires after 5 minutes.
305
+
306
+ Returns:
307
+ StationResponse containing:
308
+ - Complete station metadata and settings
309
+ - Detailed device inventory and status
310
+ - Configuration parameters and unit settings
311
+ - API response metadata
312
+
313
+ Raises:
314
+ ToolError: If the station ID doesn't exist, you don't have access to it,
315
+ API token is invalid, or network request fails
316
+
317
+ Example Usage:
318
+ >>> station = await get_station_id(12345)
319
+ >>> print(f"Station: {station.name}")
320
+ >>> print(f"Location: {station.latitude}°, {station.longitude}°")
321
+ >>> print(f"Elevation: {station.station_meta.elevation}m")
322
+ >>> print(f"Units: {station.station_units}")
323
+ >>>
324
+ >>> # Check device status
325
+ >>> for device in station.devices:
326
+ >>> if device.serial_number:
327
+ >>> status = "Online" if device.device_meta else "Offline"
328
+ >>> print(f" {device.device_type}: {status}")
329
+
330
+ Note:
331
+ Use get_stations() first to discover available station IDs. This function
332
+ provides more detailed information than the station list overview.
333
+ """
334
+ try:
335
+ return await _get_station_id_data(station_id, ctx, use_cache)
336
+ except Exception as e:
337
+ raise ToolError(f"Request failed: {str(e)}")
338
+
339
+
340
+ @mcp.tool(
341
+ annotations={
342
+ "title": "Get Weather Forecast for a Station",
343
+ "readOnlyHint": True,
344
+ "openWorldHint": True,
345
+ "idempotentHint": False,
346
+ }
347
+ )
348
+ async def get_forecast(
349
+ station_id: Annotated[
350
+ int, Field(description="The ID of the station to get forecast for", gt=0)
351
+ ],
352
+ use_cache: Annotated[
353
+ bool,
354
+ Field(
355
+ default=True,
356
+ description="Whether to use the cache to store the results of the request (default: True)",
357
+ ),
358
+ ],
359
+ ctx: Context = None,
360
+ ) -> ForecastResponse:
361
+ """Get weather forecast and current conditions for a specific weather station.
362
+
363
+ This function retrieves comprehensive weather forecast data including current
364
+ conditions, hourly forecasts, and daily summaries. The forecast combines
365
+ data from your personal weather station with professional weather models
366
+ to provide hyper-local predictions.
367
+
368
+ **Current Conditions Include:**
369
+ - Real-time temperature, humidity, and pressure
370
+ - Wind speed, direction, and gusts
371
+ - Precipitation rate and accumulation
372
+ - Solar radiation and UV index
373
+ - Visibility and weather conditions
374
+ - "Feels like" temperature and comfort indices
375
+
376
+ **Forecast Data Includes:**
377
+ - Hourly forecasts for the next 24-48 hours
378
+ - Daily forecasts for the next 7-10 days
379
+ - Temperature highs and lows
380
+ - Precipitation probability and amounts
381
+ - Wind forecasts and weather condition summaries
382
+ - Sunrise/sunset times and moon phases
383
+
384
+ **Data Sources:**
385
+ The forecast combines your station's real-time observations with
386
+ professional meteorological models to provide accurate local predictions
387
+ that account for your specific microclimate and terrain.
388
+
389
+ Args:
390
+ station_id: The numeric identifier of the weather station. Get this from
391
+ get_stations() or your WeatherFlow account dashboard.
392
+ use_cache: Whether to use cached forecast data. Forecasts update
393
+ frequently, but caching for a few minutes improves performance
394
+ for repeated requests. Cache expires after 5 minutes.
395
+
396
+ Returns:
397
+ ForecastResponse containing:
398
+ - Current weather conditions and observations
399
+ - Hourly forecast data for the next 24-48 hours
400
+ - Daily forecast summaries for the next week
401
+ - Station location and unit information
402
+ - Forecast generation timestamp and metadata
403
+
404
+ Raises:
405
+ ToolError: If the station ID doesn't exist, you don't have access to it,
406
+ API token is invalid, or network request fails
407
+
408
+ Example Usage:
409
+ >>> forecast = await get_forecast(12345)
410
+ >>>
411
+ >>> # Current conditions
412
+ >>> current = forecast.current_conditions
413
+ >>> print(f"Current: {current.air_temperature}° {forecast.units.units_temp}")
414
+ >>> print(f"Conditions: {current.conditions}")
415
+ >>> print(f"Wind: {current.wind_avg} {forecast.units.units_wind}")
416
+ >>>
417
+ >>> # Today's forecast
418
+ >>> today = forecast.forecast.daily[0]
419
+ >>> print(f"High/Low: {today.air_temp_high}°/{today.air_temp_low}°")
420
+ >>> print(f"Rain chance: {today.precip_probability}%")
421
+ >>>
422
+ >>> # Next few hours
423
+ >>> for hour in forecast.forecast.hourly[:6]:
424
+ >>> time = datetime.fromtimestamp(hour.time)
425
+ >>> print(f"{time.strftime('%H:%M')}: {hour.air_temperature}°")
426
+
427
+ Note:
428
+ All measurements are returned in the units configured for your station.
429
+ Check the 'units' field in the response to understand the unit system
430
+ (e.g., Celsius vs Fahrenheit, m/s vs mph for wind speed).
431
+ """
432
+ try:
433
+ return await _get_forecast_data(station_id, ctx, use_cache)
434
+ except Exception as e:
435
+ raise ToolError(f"Request failed: {str(e)}")
436
+
437
+
438
+ @mcp.tool(
439
+ annotations={
440
+ "title": "Get Current Weather Observations for a Station",
441
+ "readOnlyHint": True,
442
+ "openWorldHint": True,
443
+ "idempotentHint": False,
444
+ }
445
+ )
446
+ async def get_observation(
447
+ station_id: Annotated[
448
+ int, Field(description="The ID of the station to get observations for", gt=0)
449
+ ],
450
+ use_cache: Annotated[
451
+ bool,
452
+ Field(
453
+ default=True,
454
+ description="Whether to use the cache to store the results of the request (default: True)",
455
+ ),
456
+ ],
457
+ ctx: Context = None,
458
+ ) -> ObservationResponse:
459
+ """Get the most recent weather observations from a station.
460
+
461
+ This function retrieves detailed current weather conditions including:
462
+ - Temperature, humidity, pressure
463
+ - Wind speed and direction
464
+ - Precipitation data
465
+ - Solar radiation and UV index
466
+ - Lightning detection data (if available)
467
+
468
+ The data is returned in the units configured for the station. Check the
469
+ 'station_units' field in the response to understand the unit system.
470
+
471
+ Args:
472
+ station_id: The numeric ID of the weather station
473
+ use_cache: Whether to use cached data (recommended for frequent requests)
474
+
475
+ Returns:
476
+ ObservationResponse containing current weather conditions and metadata
477
+
478
+ Raises:
479
+ ToolError: If the station is not accessible or API request fails
480
+
481
+ Example:
482
+ >>> obs = await get_observation(station_id=12345)
483
+ >>> temp = obs.obs[0]["air_temperature"] # Current temperature
484
+ >>> units = obs.station_units["units_temp"] # 'c' or 'f'
485
+ """
486
+
487
+ try:
488
+ return await _get_observation_data(station_id, ctx, use_cache)
489
+ except Exception as e:
490
+ raise ToolError(f"Request failed: {str(e)}")
491
+
492
+
493
+ @mcp.tool(
494
+ annotations={
495
+ "title": "Clear the Weather Data Cache",
496
+ "readOnlyHint": False,
497
+ "openWorldHint": False,
498
+ "idempotentHint": True,
499
+ }
500
+ )
501
+ async def clear_cache(ctx: Context = None) -> str:
502
+ """Clear the weather data cache (development tool)"""
503
+ cache.clear()
504
+ if ctx:
505
+ await ctx.info("Cache cleared")
506
+ return "Cache cleared successfully"
507
+
508
+
509
+ @mcp.resource(
510
+ uri="weather://tempest/stations",
511
+ name="Get Weather Stations",
512
+ mime_type="application/json",
513
+ )
514
+ async def get_stations_resource(ctx: Context = None) -> StationsResponse:
515
+ """Get a list of all your WeatherFlow stations.
516
+
517
+ This resource can be used to get a list of all of the configured weather stations that the user has access to, along with all connected devices.
518
+ Each result contains information about the station, including its name, location, devices, state, and more.
519
+ A Device wihout a serial_number indicates that Device is no longer active.
520
+
521
+ Returns:
522
+ StationsResponse object containing the list of stations and API status
523
+ """
524
+ try:
525
+ return await _get_stations_data(ctx, use_cache=True)
526
+ except Exception as e:
527
+ raise ToolError(f"Request failed: {str(e)}")
528
+
529
+
530
+ @mcp.resource(
531
+ uri="weather://tempest/stations/{station_id}",
532
+ name="GetWeatherStationByID",
533
+ mime_type="application/json",
534
+ )
535
+ async def get_station_id_resource(
536
+ station_id: Annotated[
537
+ int,
538
+ Field(description="The ID of the station to get station information for", gt=0),
539
+ ],
540
+ ctx: Context = None,
541
+ ) -> StationResponse:
542
+ """Get information and devices for a specific weather station
543
+
544
+ This resource can be used to get a list of all of the configured weather stations that the user has access to, along with all connected devices.
545
+ Each result contains information about the station, including its name, location, devices, state, and more.
546
+ A Device wihout a serial_number indicates that Device is no longer active.
547
+
548
+ Args:
549
+ station_id (int): The ID of the station to get information for
550
+
551
+ Returns:
552
+ StationResponse object containing comprehensive station metadata and device information
553
+ """
554
+
555
+ try:
556
+ return await _get_station_id_data(station_id, ctx, use_cache=True)
557
+ except Exception as e:
558
+ raise ToolError(f"Request failed: {str(e)}")
559
+
560
+
561
+ @mcp.resource(
562
+ uri="weather://tempest/forecast/{station_id}",
563
+ name="GetWeatherForecast",
564
+ mime_type="application/json",
565
+ )
566
+ async def get_forecast_resource(
567
+ station_id: Annotated[
568
+ int, Field(description="The ID of the station to get forecast for", gt=0)
569
+ ],
570
+ ctx: Context = None,
571
+ ) -> Dict[str, Any]:
572
+ """Get information and devices for a specific weather station
573
+
574
+ This resource allows the user to retrieve the weather forecast from the specified weather station.
575
+
576
+ Args:
577
+ station_id (int): The ID of the station to get information for
578
+
579
+ Returns:
580
+ ForecastResponse object containing the weather forecast and current conditions
581
+ """
582
+
583
+ try:
584
+ return await _get_forecast_data(station_id, ctx, use_cache=True)
585
+ except Exception as e:
586
+ raise ToolError(f"Request failed: {str(e)}")
587
+
588
+
589
+ @mcp.resource(
590
+ uri="weather://tempest/observations/{station_id}",
591
+ name="GetWeatherObservations",
592
+ mime_type="application/json",
593
+ )
594
+ async def get_observation_resource(
595
+ station_id: Annotated[
596
+ int, Field(description="The ID of the station to get observations for", gt=0)
597
+ ],
598
+ ctx: Context = None,
599
+ ) -> Dict[str, Any]:
600
+ """Get latest detailed observations for a specific weather station
601
+
602
+ This resource allows the user to retrieve the weather forecast from the specified weather station.
603
+
604
+ Returns:
605
+ ObservationResponse object containing the current weather observations and station metadata
606
+ """
607
+
608
+ try:
609
+ return await _get_observation_data(station_id, ctx, use_cache=True)
610
+ except Exception as e:
611
+ raise ToolError(f"Request failed: {str(e)}")
612
+
613
+
614
+ if __name__ == "__main__":
615
+ mcp.run()