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.
@@ -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
@@ -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,9 @@
1
+ # Smithery configuration — https://smithery.ai/docs/config
2
+ startCommand:
3
+ type: stdio
4
+ configSchema:
5
+ type: object
6
+ required: []
7
+ properties: {}
8
+ commandFunction: |-
9
+ (config) => ({ command: 'uvx', args: ['nhc-mcp'] })
@@ -0,0 +1 @@
1
+ """NHC MCP Server — National Hurricane Center Data for AI Assistants."""
@@ -0,0 +1,3 @@
1
+ from .server import main
2
+
3
+ main()
@@ -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")