nhc-mcp 0.1.0__tar.gz
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.
- nhc_mcp-0.1.0/.gitignore +42 -0
- nhc_mcp-0.1.0/LICENSE +21 -0
- nhc_mcp-0.1.0/PKG-INFO +98 -0
- nhc_mcp-0.1.0/README.md +68 -0
- nhc_mcp-0.1.0/nhc_track_milton.png +0 -0
- nhc_mcp-0.1.0/pyproject.toml +64 -0
- nhc_mcp-0.1.0/server.json +22 -0
- nhc_mcp-0.1.0/smithery.yaml +9 -0
- nhc_mcp-0.1.0/src/nhc_mcp/__init__.py +1 -0
- nhc_mcp-0.1.0/src/nhc_mcp/__main__.py +3 -0
- nhc_mcp-0.1.0/src/nhc_mcp/client.py +128 -0
- nhc_mcp-0.1.0/src/nhc_mcp/models.py +45 -0
- nhc_mcp-0.1.0/src/nhc_mcp/server.py +32 -0
- nhc_mcp-0.1.0/src/nhc_mcp/tools/__init__.py +1 -0
- nhc_mcp-0.1.0/src/nhc_mcp/tools/active.py +89 -0
- nhc_mcp-0.1.0/src/nhc_mcp/tools/forecast.py +236 -0
- nhc_mcp-0.1.0/src/nhc_mcp/tools/history.py +308 -0
- nhc_mcp-0.1.0/src/nhc_mcp/utils.py +465 -0
- nhc_mcp-0.1.0/tests/__init__.py +0 -0
- nhc_mcp-0.1.0/tests/test_client.py +170 -0
- nhc_mcp-0.1.0/tests/test_live.py +83 -0
- nhc_mcp-0.1.0/tests/test_utils.py +402 -0
nhc_mcp-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.egg-info/
|
|
6
|
+
*.egg
|
|
7
|
+
dist/
|
|
8
|
+
build/
|
|
9
|
+
*.whl
|
|
10
|
+
|
|
11
|
+
# Virtual environments
|
|
12
|
+
.venv/
|
|
13
|
+
venv/
|
|
14
|
+
env/
|
|
15
|
+
|
|
16
|
+
# Testing
|
|
17
|
+
.pytest_cache/
|
|
18
|
+
.coverage
|
|
19
|
+
htmlcov/
|
|
20
|
+
.tox/
|
|
21
|
+
|
|
22
|
+
# IDE
|
|
23
|
+
.idea/
|
|
24
|
+
.vscode/
|
|
25
|
+
*.swp
|
|
26
|
+
*.swo
|
|
27
|
+
*~
|
|
28
|
+
|
|
29
|
+
# OS
|
|
30
|
+
.DS_Store
|
|
31
|
+
Thumbs.db
|
|
32
|
+
|
|
33
|
+
# Linting
|
|
34
|
+
.ruff_cache/
|
|
35
|
+
.mypy_cache/
|
|
36
|
+
|
|
37
|
+
# Lock files (each server manages its own)
|
|
38
|
+
uv.lock
|
|
39
|
+
|
|
40
|
+
# Environment
|
|
41
|
+
.env
|
|
42
|
+
.env.local
|
nhc_mcp-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Mansur Ali Jisan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
nhc_mcp-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nhc-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for NHC storm tracks, advisories, and hurricane best track data
|
|
5
|
+
Project-URL: Homepage, https://github.com/mansurjisan/ocean-mcp
|
|
6
|
+
Project-URL: Repository, https://github.com/mansurjisan/ocean-mcp
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/mansurjisan/ocean-mcp/issues
|
|
8
|
+
Project-URL: Documentation, https://github.com/mansurjisan/ocean-mcp/tree/main/servers/nhc-mcp
|
|
9
|
+
Project-URL: Changelog, https://github.com/mansurjisan/ocean-mcp/releases
|
|
10
|
+
Author-email: Mansur Ali Jisan <mansur.jisan@noaa.gov>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: hurdat2,hurricane,mcp,meteorology,model-context-protocol,nhc,noaa,storm-track,tropical-cyclone
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: Science/Research
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Requires-Python: >=3.11
|
|
26
|
+
Requires-Dist: httpx>=0.27.0
|
|
27
|
+
Requires-Dist: mcp[cli]>=1.0.0
|
|
28
|
+
Requires-Dist: pydantic>=2.0.0
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# NHC MCP Server
|
|
32
|
+
|
|
33
|
+
<!-- mcp-name: io.github.mansurjisan/nhc-mcp -->
|
|
34
|
+
|
|
35
|
+
MCP server providing AI assistants with access to National Hurricane Center (NHC) storm tracks, advisories, and best track data.
|
|
36
|
+
|
|
37
|
+
**Status: Ready**
|
|
38
|
+
|
|
39
|
+
## Tools
|
|
40
|
+
|
|
41
|
+
| Tool | Description | Status |
|
|
42
|
+
|------|-------------|--------|
|
|
43
|
+
| `nhc_get_active_storms` | Get currently active tropical cyclones | Ready |
|
|
44
|
+
| `nhc_get_forecast_track` | Get official NHC 5-day forecast track positions | Ready |
|
|
45
|
+
| `nhc_get_best_track` | Get best track data for historical or recent storms | Ready |
|
|
46
|
+
| `nhc_search_storms` | Search historical storms by name, year, basin, intensity | Ready |
|
|
47
|
+
| `nhc_get_storm_watches_warnings` | Get active watches and warnings for a storm | Ready |
|
|
48
|
+
| `nhc_generate_parametric_wind` | Generate parametric wind field for a storm | Planned |
|
|
49
|
+
|
|
50
|
+
## Data Sources
|
|
51
|
+
|
|
52
|
+
- **CurrentStorms.json** — Real-time active storm information from NHC
|
|
53
|
+
- **ATCF B-deck** — Current-season best track data (Automated Tropical Cyclone Forecasting)
|
|
54
|
+
- **HURDAT2** — Atlantic (1851–2024) and East Pacific (1949–2024) hurricane database
|
|
55
|
+
- **NHC ArcGIS MapServer** — Forecast tracks, cones, and watch/warning polygons
|
|
56
|
+
|
|
57
|
+
## Quick Start
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
git clone https://github.com/mansurjisan/ocean-mcp.git
|
|
61
|
+
cd ocean-mcp/servers/nhc-mcp
|
|
62
|
+
uv sync
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Configure your MCP client
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"mcpServers": {
|
|
70
|
+
"nhc": {
|
|
71
|
+
"command": "uv",
|
|
72
|
+
"args": ["run", "--directory", "/path/to/ocean-mcp/servers/nhc-mcp", "python", "-m", "nhc_mcp"]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Example Queries
|
|
79
|
+
|
|
80
|
+
- "Are there any active tropical cyclones right now?"
|
|
81
|
+
- "Show me Hurricane Katrina's track"
|
|
82
|
+
- "Search for Category 5 hurricanes in the Atlantic since 2000"
|
|
83
|
+
- "What is the forecast track for the active storm AL052024?"
|
|
84
|
+
- "Find all storms named Maria"
|
|
85
|
+
|
|
86
|
+
## Running Tests
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# Unit tests (fast, no network)
|
|
90
|
+
uv run pytest tests/test_utils.py tests/test_client.py -v
|
|
91
|
+
|
|
92
|
+
# Live integration tests (requires internet)
|
|
93
|
+
uv run pytest tests/test_live.py -v -s
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
MIT
|
nhc_mcp-0.1.0/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# NHC MCP Server
|
|
2
|
+
|
|
3
|
+
<!-- mcp-name: io.github.mansurjisan/nhc-mcp -->
|
|
4
|
+
|
|
5
|
+
MCP server providing AI assistants with access to National Hurricane Center (NHC) storm tracks, advisories, and best track data.
|
|
6
|
+
|
|
7
|
+
**Status: Ready**
|
|
8
|
+
|
|
9
|
+
## Tools
|
|
10
|
+
|
|
11
|
+
| Tool | Description | Status |
|
|
12
|
+
|------|-------------|--------|
|
|
13
|
+
| `nhc_get_active_storms` | Get currently active tropical cyclones | Ready |
|
|
14
|
+
| `nhc_get_forecast_track` | Get official NHC 5-day forecast track positions | Ready |
|
|
15
|
+
| `nhc_get_best_track` | Get best track data for historical or recent storms | Ready |
|
|
16
|
+
| `nhc_search_storms` | Search historical storms by name, year, basin, intensity | Ready |
|
|
17
|
+
| `nhc_get_storm_watches_warnings` | Get active watches and warnings for a storm | Ready |
|
|
18
|
+
| `nhc_generate_parametric_wind` | Generate parametric wind field for a storm | Planned |
|
|
19
|
+
|
|
20
|
+
## Data Sources
|
|
21
|
+
|
|
22
|
+
- **CurrentStorms.json** — Real-time active storm information from NHC
|
|
23
|
+
- **ATCF B-deck** — Current-season best track data (Automated Tropical Cyclone Forecasting)
|
|
24
|
+
- **HURDAT2** — Atlantic (1851–2024) and East Pacific (1949–2024) hurricane database
|
|
25
|
+
- **NHC ArcGIS MapServer** — Forecast tracks, cones, and watch/warning polygons
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
git clone https://github.com/mansurjisan/ocean-mcp.git
|
|
31
|
+
cd ocean-mcp/servers/nhc-mcp
|
|
32
|
+
uv sync
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Configure your MCP client
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"mcpServers": {
|
|
40
|
+
"nhc": {
|
|
41
|
+
"command": "uv",
|
|
42
|
+
"args": ["run", "--directory", "/path/to/ocean-mcp/servers/nhc-mcp", "python", "-m", "nhc_mcp"]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Example Queries
|
|
49
|
+
|
|
50
|
+
- "Are there any active tropical cyclones right now?"
|
|
51
|
+
- "Show me Hurricane Katrina's track"
|
|
52
|
+
- "Search for Category 5 hurricanes in the Atlantic since 2000"
|
|
53
|
+
- "What is the forecast track for the active storm AL052024?"
|
|
54
|
+
- "Find all storms named Maria"
|
|
55
|
+
|
|
56
|
+
## Running Tests
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# Unit tests (fast, no network)
|
|
60
|
+
uv run pytest tests/test_utils.py tests/test_client.py -v
|
|
61
|
+
|
|
62
|
+
# Live integration tests (requires internet)
|
|
63
|
+
uv run pytest tests/test_live.py -v -s
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
MIT
|
|
Binary file
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nhc-mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MCP server for NHC storm tracks, advisories, and hurricane best track data"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
|
+
authors = [
|
|
14
|
+
{name = "Mansur Ali Jisan", email = "mansur.jisan@noaa.gov"},
|
|
15
|
+
]
|
|
16
|
+
keywords = [
|
|
17
|
+
"mcp",
|
|
18
|
+
"model-context-protocol",
|
|
19
|
+
"noaa",
|
|
20
|
+
"hurricane",
|
|
21
|
+
"nhc",
|
|
22
|
+
"tropical-cyclone",
|
|
23
|
+
"storm-track",
|
|
24
|
+
"hurdat2",
|
|
25
|
+
"meteorology",
|
|
26
|
+
]
|
|
27
|
+
classifiers = [
|
|
28
|
+
"Development Status :: 4 - Beta",
|
|
29
|
+
"Intended Audience :: Science/Research",
|
|
30
|
+
"Intended Audience :: Developers",
|
|
31
|
+
"License :: OSI Approved :: MIT License",
|
|
32
|
+
"Programming Language :: Python :: 3",
|
|
33
|
+
"Programming Language :: Python :: 3.11",
|
|
34
|
+
"Programming Language :: Python :: 3.12",
|
|
35
|
+
"Programming Language :: Python :: 3.13",
|
|
36
|
+
"Topic :: Scientific/Engineering :: Atmospheric Science",
|
|
37
|
+
"Topic :: Scientific/Engineering :: GIS",
|
|
38
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
39
|
+
]
|
|
40
|
+
dependencies = [
|
|
41
|
+
"mcp[cli]>=1.0.0",
|
|
42
|
+
"httpx>=0.27.0",
|
|
43
|
+
"pydantic>=2.0.0",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[project.urls]
|
|
47
|
+
Homepage = "https://github.com/mansurjisan/ocean-mcp"
|
|
48
|
+
Repository = "https://github.com/mansurjisan/ocean-mcp"
|
|
49
|
+
"Bug Tracker" = "https://github.com/mansurjisan/ocean-mcp/issues"
|
|
50
|
+
Documentation = "https://github.com/mansurjisan/ocean-mcp/tree/main/servers/nhc-mcp"
|
|
51
|
+
Changelog = "https://github.com/mansurjisan/ocean-mcp/releases"
|
|
52
|
+
|
|
53
|
+
[project.scripts]
|
|
54
|
+
nhc-mcp = "nhc_mcp.server:main"
|
|
55
|
+
|
|
56
|
+
[tool.hatch.build.targets.wheel]
|
|
57
|
+
packages = ["src/nhc_mcp"]
|
|
58
|
+
|
|
59
|
+
[tool.pytest.ini_options]
|
|
60
|
+
testpaths = ["tests"]
|
|
61
|
+
asyncio_mode = "auto"
|
|
62
|
+
|
|
63
|
+
[dependency-groups]
|
|
64
|
+
dev = ["pytest>=8.0.0", "pytest-asyncio>=0.24.0", "respx>=0.22.0"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.mansurjisan/nhc-mcp",
|
|
4
|
+
"title": "NHC MCP Server",
|
|
5
|
+
"description": "MCP server for NHC storm tracks, forecast advisories, watches/warnings, and HURDAT2 historical hurricane data. No API key required.",
|
|
6
|
+
"version": "0.1.0",
|
|
7
|
+
"repository": {
|
|
8
|
+
"url": "https://github.com/mansurjisan/ocean-mcp",
|
|
9
|
+
"source": "github"
|
|
10
|
+
},
|
|
11
|
+
"packages": [
|
|
12
|
+
{
|
|
13
|
+
"registryType": "pypi",
|
|
14
|
+
"identifier": "nhc-mcp",
|
|
15
|
+
"version": "0.1.0",
|
|
16
|
+
"runtimeHint": "uvx",
|
|
17
|
+
"transport": {
|
|
18
|
+
"type": "stdio"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""NHC MCP Server — National Hurricane Center Data for AI Assistants."""
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Async HTTP client for NHC data sources."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
# NHC data endpoints
|
|
10
|
+
CURRENT_STORMS_URL = "https://www.nhc.noaa.gov/CurrentStorms.json"
|
|
11
|
+
ATCF_BDECK_URL = "https://ftp.nhc.noaa.gov/atcf/btk/b{basin}{number:02d}{year}.dat"
|
|
12
|
+
HURDAT2_URLS = {
|
|
13
|
+
"al": "https://www.nhc.noaa.gov/data/hurdat/hurdat2-1851-2024-040425.txt",
|
|
14
|
+
"ep": "https://www.nhc.noaa.gov/data/hurdat/hurdat2-nepac-1949-2024-031725.txt",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
from .utils import ARCGIS_BASE_URL, build_arcgis_query_url
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class NHCAPIError(Exception):
|
|
21
|
+
"""Custom exception for NHC API errors."""
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class NHCClient:
|
|
26
|
+
"""Async client for NHC data sources (CurrentStorms, ATCF, HURDAT2, ArcGIS)."""
|
|
27
|
+
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
self._client: httpx.AsyncClient | None = None
|
|
30
|
+
self._hurdat2_cache: dict[str, str] = {}
|
|
31
|
+
|
|
32
|
+
async def _get_client(self) -> httpx.AsyncClient:
|
|
33
|
+
if self._client is None or self._client.is_closed:
|
|
34
|
+
self._client = httpx.AsyncClient(
|
|
35
|
+
timeout=60.0,
|
|
36
|
+
follow_redirects=True,
|
|
37
|
+
)
|
|
38
|
+
return self._client
|
|
39
|
+
|
|
40
|
+
async def get_active_storms(self) -> list[dict]:
|
|
41
|
+
"""Fetch currently active tropical cyclones from CurrentStorms.json.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
List of active storm dicts. May be empty outside hurricane season.
|
|
45
|
+
"""
|
|
46
|
+
client = await self._get_client()
|
|
47
|
+
response = await client.get(CURRENT_STORMS_URL)
|
|
48
|
+
response.raise_for_status()
|
|
49
|
+
data = response.json()
|
|
50
|
+
return data.get("activeStorms", [])
|
|
51
|
+
|
|
52
|
+
async def get_best_track_atcf(self, basin: str, number: int, year: int) -> str:
|
|
53
|
+
"""Fetch ATCF B-deck best track data for a specific storm.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
basin: Basin code ('al', 'ep', 'cp').
|
|
57
|
+
number: Storm number within the season.
|
|
58
|
+
year: 4-digit year.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Raw B-deck text content.
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
httpx.HTTPStatusError: If the file doesn't exist (404).
|
|
65
|
+
"""
|
|
66
|
+
url = ATCF_BDECK_URL.format(basin=basin, number=number, year=year)
|
|
67
|
+
client = await self._get_client()
|
|
68
|
+
response = await client.get(url)
|
|
69
|
+
response.raise_for_status()
|
|
70
|
+
return response.text
|
|
71
|
+
|
|
72
|
+
async def get_hurdat2(self, basin: str) -> str:
|
|
73
|
+
"""Fetch HURDAT2 data for a basin, with in-memory caching.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
basin: Basin code ('al' or 'ep'). 'cp' falls back to 'ep'.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Raw HURDAT2 text content.
|
|
80
|
+
"""
|
|
81
|
+
# Central Pacific storms are in the East Pacific HURDAT2 file
|
|
82
|
+
lookup_basin = "ep" if basin == "cp" else basin
|
|
83
|
+
|
|
84
|
+
if lookup_basin in self._hurdat2_cache:
|
|
85
|
+
return self._hurdat2_cache[lookup_basin]
|
|
86
|
+
|
|
87
|
+
url = HURDAT2_URLS.get(lookup_basin)
|
|
88
|
+
if not url:
|
|
89
|
+
raise ValueError(f"No HURDAT2 data available for basin '{basin}'.")
|
|
90
|
+
|
|
91
|
+
client = await self._get_client()
|
|
92
|
+
response = await client.get(url)
|
|
93
|
+
response.raise_for_status()
|
|
94
|
+
text = response.text
|
|
95
|
+
self._hurdat2_cache[lookup_basin] = text
|
|
96
|
+
return text
|
|
97
|
+
|
|
98
|
+
async def query_arcgis_layer(
|
|
99
|
+
self,
|
|
100
|
+
layer_id: int,
|
|
101
|
+
where: str = "1=1",
|
|
102
|
+
) -> dict[str, Any]:
|
|
103
|
+
"""Query an ArcGIS MapServer layer.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
layer_id: MapServer layer ID.
|
|
107
|
+
where: SQL WHERE clause.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
ArcGIS JSON response with features.
|
|
111
|
+
"""
|
|
112
|
+
url = build_arcgis_query_url(layer_id, where=where)
|
|
113
|
+
client = await self._get_client()
|
|
114
|
+
response = await client.get(url)
|
|
115
|
+
response.raise_for_status()
|
|
116
|
+
data = response.json()
|
|
117
|
+
|
|
118
|
+
if "error" in data:
|
|
119
|
+
error = data["error"]
|
|
120
|
+
msg = error.get("message", "Unknown ArcGIS error")
|
|
121
|
+
raise NHCAPIError(f"ArcGIS error: {msg}")
|
|
122
|
+
|
|
123
|
+
return data
|
|
124
|
+
|
|
125
|
+
async def close(self) -> None:
|
|
126
|
+
"""Close the HTTP client."""
|
|
127
|
+
if self._client and not self._client.is_closed:
|
|
128
|
+
await self._client.aclose()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Enums and constants for NHC MCP server."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Basin(str, Enum):
|
|
7
|
+
"""NHC tropical cyclone basins."""
|
|
8
|
+
|
|
9
|
+
AL = "al" # Atlantic
|
|
10
|
+
EP = "ep" # East Pacific
|
|
11
|
+
CP = "cp" # Central Pacific
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class StormClassification(str, Enum):
|
|
15
|
+
"""Tropical cyclone classification based on wind speed."""
|
|
16
|
+
|
|
17
|
+
TD = "TD" # Tropical Depression (< 34 kt)
|
|
18
|
+
TS = "TS" # Tropical Storm (34–63 kt)
|
|
19
|
+
HU = "HU" # Hurricane (64+ kt)
|
|
20
|
+
SD = "SD" # Subtropical Depression
|
|
21
|
+
SS = "SS" # Subtropical Storm
|
|
22
|
+
EX = "EX" # Extratropical
|
|
23
|
+
LO = "LO" # Low / remnant low
|
|
24
|
+
DB = "DB" # Disturbance
|
|
25
|
+
WV = "WV" # Tropical Wave
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Saffir-Simpson category thresholds (knots)
|
|
29
|
+
SAFFIR_SIMPSON = [
|
|
30
|
+
(137, "Category 5"),
|
|
31
|
+
(113, "Category 4"),
|
|
32
|
+
(96, "Category 3"),
|
|
33
|
+
(83, "Category 2"),
|
|
34
|
+
(64, "Category 1"),
|
|
35
|
+
(34, "Tropical Storm"),
|
|
36
|
+
(0, "Tropical Depression"),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def classify_wind_speed(wind_kt: int) -> str:
|
|
41
|
+
"""Return Saffir-Simpson category string for a given wind speed in knots."""
|
|
42
|
+
for threshold, label in SAFFIR_SIMPSON:
|
|
43
|
+
if wind_kt >= threshold:
|
|
44
|
+
return label
|
|
45
|
+
return "Tropical Depression"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""FastMCP server entry point for NHC MCP."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterator
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
|
|
6
|
+
from mcp.server.fastmcp import FastMCP
|
|
7
|
+
|
|
8
|
+
from .client import NHCClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@asynccontextmanager
|
|
12
|
+
async def app_lifespan(server: FastMCP) -> AsyncIterator[dict]:
|
|
13
|
+
"""Manage the shared NHC API client lifecycle."""
|
|
14
|
+
client = NHCClient()
|
|
15
|
+
try:
|
|
16
|
+
yield {"nhc_client": client}
|
|
17
|
+
finally:
|
|
18
|
+
await client.close()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
mcp = FastMCP("nhc_mcp", lifespan=app_lifespan)
|
|
22
|
+
|
|
23
|
+
# Import tool modules to register them with the server
|
|
24
|
+
from .tools import active, forecast, history # noqa: E402, F401
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main() -> None:
|
|
28
|
+
mcp.run()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if __name__ == "__main__":
|
|
32
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""NHC MCP tool modules."""
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Tool: nhc_get_active_storms — currently active tropical cyclones."""
|
|
2
|
+
|
|
3
|
+
from mcp.server.fastmcp import Context
|
|
4
|
+
from mcp.types import ToolAnnotations
|
|
5
|
+
|
|
6
|
+
from ..client import NHCClient
|
|
7
|
+
from ..models import classify_wind_speed
|
|
8
|
+
from ..server import mcp
|
|
9
|
+
from ..utils import format_tabular_data, handle_nhc_error
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _get_client(ctx: Context) -> NHCClient:
|
|
13
|
+
return ctx.request_context.lifespan_context["nhc_client"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@mcp.tool(
|
|
17
|
+
annotations=ToolAnnotations(
|
|
18
|
+
readOnlyHint=True,
|
|
19
|
+
destructiveHint=False,
|
|
20
|
+
idempotentHint=True,
|
|
21
|
+
openWorldHint=True,
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
async def nhc_get_active_storms(
|
|
25
|
+
ctx: Context,
|
|
26
|
+
response_format: str = "markdown",
|
|
27
|
+
) -> str:
|
|
28
|
+
"""Get currently active tropical cyclones from the National Hurricane Center.
|
|
29
|
+
|
|
30
|
+
Returns a list of all active storms with their current classification,
|
|
31
|
+
location, and advisory information. Returns an empty list outside of
|
|
32
|
+
hurricane season or when no storms are active.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
response_format: Output format — 'markdown' (default) or 'json'.
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
client = _get_client(ctx)
|
|
39
|
+
storms = await client.get_active_storms()
|
|
40
|
+
|
|
41
|
+
if not storms:
|
|
42
|
+
return (
|
|
43
|
+
"## Active Tropical Cyclones\n\n"
|
|
44
|
+
"No active tropical cyclones at this time.\n\n"
|
|
45
|
+
"*Tip: Use nhc_search_storms to search historical storms, or "
|
|
46
|
+
"nhc_get_best_track to retrieve track data for past storms.*"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Enrich storm data with classification
|
|
50
|
+
rows = []
|
|
51
|
+
for s in storms:
|
|
52
|
+
row = {
|
|
53
|
+
"name": s.get("name", "Unknown"),
|
|
54
|
+
"classification": s.get("classification", ""),
|
|
55
|
+
"id": s.get("id", ""),
|
|
56
|
+
"binNumber": s.get("binNumber", ""),
|
|
57
|
+
"movementDir": s.get("movementDir", ""),
|
|
58
|
+
"movementSpeed": s.get("movementSpeed", ""),
|
|
59
|
+
"pressure": s.get("pressure", ""),
|
|
60
|
+
"wind": s.get("wind", ""),
|
|
61
|
+
"lastUpdate": s.get("lastUpdate", ""),
|
|
62
|
+
"advisory_url": s.get("publicAdvisory", {}).get("url", ""),
|
|
63
|
+
}
|
|
64
|
+
rows.append(row)
|
|
65
|
+
|
|
66
|
+
if response_format == "json":
|
|
67
|
+
import json
|
|
68
|
+
return json.dumps({"active_storms": rows, "count": len(rows)}, indent=2)
|
|
69
|
+
|
|
70
|
+
columns = [
|
|
71
|
+
("name", "Name"),
|
|
72
|
+
("classification", "Type"),
|
|
73
|
+
("id", "Storm ID"),
|
|
74
|
+
("wind", "Wind"),
|
|
75
|
+
("pressure", "Pressure"),
|
|
76
|
+
("movementDir", "Movement"),
|
|
77
|
+
("movementSpeed", "Speed"),
|
|
78
|
+
("lastUpdate", "Last Update"),
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
return format_tabular_data(
|
|
82
|
+
data=rows,
|
|
83
|
+
columns=columns,
|
|
84
|
+
title="Active Tropical Cyclones",
|
|
85
|
+
count_label="active storms",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
except Exception as e:
|
|
89
|
+
return handle_nhc_error(e, "fetching active storms")
|