mcnoaa-tides/tests/test_tools_stations.py
Ryan Malloy 6c244b3a63 Initial implementation: FastMCP 3.0 server wrapping NOAA CO-OPS API
7 tools: station search/nearest/info, tide predictions/observations,
meteorological data (Literal selector for 8 products), and parallel
marine conditions snapshot. 3 resources, 2 prompts, full test suite.
2026-02-21 21:04:03 -07:00

140 lines
4.5 KiB
Python

"""Tests for station discovery and data retrieval tools."""
import json
from fastmcp import Client
async def test_tool_registration(mcp_client: Client):
"""All 7 tools should be registered."""
tools = await mcp_client.list_tools()
tool_names = {t.name for t in tools}
expected = {
"search_stations",
"find_nearest_stations",
"get_station_info",
"get_tide_predictions",
"get_observed_water_levels",
"get_meteorological_data",
"marine_conditions_snapshot",
}
assert expected == tool_names
async def test_search_stations_by_state(mcp_client: Client):
result = await mcp_client.call_tool("search_stations", {"state": "RI"})
stations = json.loads(result.content[0].text)
assert len(stations) == 2
assert all(s["state"] == "RI" for s in stations)
async def test_search_stations_by_name(mcp_client: Client):
result = await mcp_client.call_tool("search_stations", {"query": "providence"})
stations = json.loads(result.content[0].text)
assert len(stations) == 1
assert stations[0]["id"] == "8454000"
async def test_search_stations_no_match(mcp_client: Client):
result = await mcp_client.call_tool("search_stations", {"query": "nonexistent"})
# FastMCP may return empty content list for empty results
if result.content:
stations = json.loads(result.content[0].text)
assert len(stations) == 0
else:
# Empty content means no matches — that's correct
pass
async def test_find_nearest_stations(mcp_client: Client):
# Search near Providence coordinates
result = await mcp_client.call_tool(
"find_nearest_stations",
{"latitude": 41.8, "longitude": -71.4},
)
stations = json.loads(result.content[0].text)
assert len(stations) >= 1
# Closest should be Providence
assert stations[0]["id"] == "8454000"
assert "distance_nm" in stations[0]
# Should be sorted by distance
distances = [s["distance_nm"] for s in stations]
assert distances == sorted(distances)
async def test_get_station_info(mcp_client: Client):
result = await mcp_client.call_tool("get_station_info", {"station_id": "8454000"})
info = json.loads(result.content[0].text)
assert info["id"] == "8454000"
assert info["name"] == "Providence"
assert "sensors" in info
async def test_get_tide_predictions(mcp_client: Client):
result = await mcp_client.call_tool(
"get_tide_predictions", {"station_id": "8454000"}
)
data = json.loads(result.content[0].text)
assert "predictions" in data
preds = data["predictions"]
assert len(preds) == 4
assert preds[0]["type"] == "H"
assert preds[1]["type"] == "L"
async def test_get_observed_water_levels(mcp_client: Client):
result = await mcp_client.call_tool(
"get_observed_water_levels", {"station_id": "8454000"}
)
data = json.loads(result.content[0].text)
assert "data" in data
assert len(data["data"]) == 2
async def test_get_meteorological_data_wind(mcp_client: Client):
result = await mcp_client.call_tool(
"get_meteorological_data",
{"station_id": "8454000", "product": "wind"},
)
data = json.loads(result.content[0].text)
assert "data" in data
assert data["data"][0]["dr"] == "SW"
async def test_marine_conditions_snapshot(mcp_client: Client):
result = await mcp_client.call_tool(
"marine_conditions_snapshot", {"station_id": "8454000"}
)
snapshot = json.loads(result.content[0].text)
assert snapshot["station_id"] == "8454000"
assert "fetched_utc" in snapshot
assert "predictions" in snapshot
assert "water_level" in snapshot
assert "wind" in snapshot
async def test_resource_registration(mcp_client: Client):
"""Resources should be registered."""
resources = await mcp_client.list_resources()
uris = {str(r.uri) for r in resources}
assert "noaa://stations" in uris
async def test_prompt_registration(mcp_client: Client):
"""Prompts should be registered."""
prompts = await mcp_client.list_prompts()
prompt_names = {p.name for p in prompts}
assert "plan_fishing_trip" in prompt_names
assert "marine_safety_check" in prompt_names
async def test_prompt_plan_fishing_trip(mcp_client: Client):
result = await mcp_client.get_prompt(
"plan_fishing_trip", {"location": "Narragansett Bay"}
)
assert len(result.messages) >= 1
text = result.messages[0].content
if hasattr(text, "text"):
text = text.text
assert "Narragansett Bay" in str(text)