mcgibs/tests/test_tools.py
Ryan Malloy f7fad32a9e Implement mcgibs FastMCP server for NASA GIBS
Complete implementation of all modules:
- constants.py: GIBS API endpoints, projections, TileMatrixSet defs
- models.py: Pydantic models for layers, colormaps, geocoding
- geo.py: Nominatim geocoding with rate limiting and caching
- capabilities.py: WMTS GetCapabilities XML parser with search
- colormaps.py: Colormap v1.3 parser with natural-language summaries
- client.py: Async GIBS HTTP client wrapping all API interactions
- server.py: FastMCP 3.0 tools, resources, and prompts

11 MCP tools, 3 resources, 2 prompts. 47 tests, all passing.
2026-02-18 14:55:41 -07:00

174 lines
5.8 KiB
Python

"""FastMCP integration tests — tool calls against the real server, HTTP mocked."""
import json
import httpx
import respx
from fastmcp import Client
import mcgibs.server as server_module
from mcgibs.client import GIBSClient
from mcgibs.server import mcp
async def _init_mock_client(capabilities_xml: str) -> GIBSClient:
"""Create and initialize a GIBSClient with mocked capabilities."""
client = GIBSClient()
await client.initialize()
return client
@respx.mock
async def test_search_tool(capabilities_xml):
"""search_gibs_layers finds the sea ice layer by keyword."""
respx.get(url__regex=r".*WMTSCapabilities\.xml").mock(
return_value=httpx.Response(200, text=capabilities_xml)
)
server_module._client = await _init_mock_client(capabilities_xml)
try:
async with Client(mcp) as client:
result = await client.call_tool("search_gibs_layers", {"query": "sea ice"})
text = result.content[0].text
assert "AMSR2" in text
assert "Sea Ice" in text
# Should not match unrelated layers
assert "Surface Air Temperature" not in text
finally:
await server_module._client.close()
server_module._client = None
@respx.mock
async def test_get_layer_info_tool(capabilities_xml, layer_metadata_json):
"""get_layer_info returns enriched JSON for the true color layer."""
respx.get(url__regex=r".*WMTSCapabilities\.xml").mock(
return_value=httpx.Response(200, text=capabilities_xml)
)
layer_id = "MODIS_Terra_CorrectedReflectance_TrueColor"
respx.get(url__regex=rf".*layer-metadata.*/{layer_id}\.json").mock(
return_value=httpx.Response(200, text=layer_metadata_json)
)
server_module._client = await _init_mock_client(capabilities_xml)
try:
async with Client(mcp) as client:
result = await client.call_tool("get_layer_info", {"layer_id": layer_id})
text = result.content[0].text
info = json.loads(text)
assert info["identifier"] == layer_id
assert info["instrument"] == "MODIS"
assert info["platform"] == "Terra"
assert info["ongoing"] is True
assert "time" in info
assert info["time"]["start"] == "2000-02-24"
finally:
await server_module._client.close()
server_module._client = None
@respx.mock
async def test_resolve_place_tool(capabilities_xml):
"""resolve_place geocodes a place name via mocked Nominatim."""
respx.get(url__regex=r".*WMTSCapabilities\.xml").mock(
return_value=httpx.Response(200, text=capabilities_xml)
)
nominatim_response = [
{
"display_name": "Tokyo, Japan",
"lat": "35.6762",
"lon": "139.6503",
"boundingbox": ["35.5190", "35.8178", "138.9428", "139.9200"],
"osm_type": "relation",
"importance": 0.82,
}
]
respx.get(url__regex=r".*nominatim.*search.*").mock(
return_value=httpx.Response(200, json=nominatim_response)
)
server_module._client = await _init_mock_client(capabilities_xml)
try:
async with Client(mcp) as client:
result = await client.call_tool("resolve_place", {"place": "Tokyo"})
text = result.content[0].text
data = json.loads(text)
assert data["display_name"] == "Tokyo, Japan"
assert abs(data["lat"] - 35.6762) < 0.01
assert abs(data["lon"] - 139.6503) < 0.01
assert "bbox" in data
assert data["bbox"]["west"] < data["bbox"]["east"]
finally:
await server_module._client.close()
server_module._client = None
@respx.mock
async def test_build_tile_url_tool(capabilities_xml):
"""build_tile_url returns a properly formatted WMTS tile URL."""
respx.get(url__regex=r".*WMTSCapabilities\.xml").mock(
return_value=httpx.Response(200, text=capabilities_xml)
)
server_module._client = await _init_mock_client(capabilities_xml)
try:
async with Client(mcp) as client:
result = await client.call_tool(
"build_tile_url",
{
"layer_id": "MODIS_Terra_CorrectedReflectance_TrueColor",
"date": "2025-06-01",
"zoom": 3,
"row": 2,
"col": 4,
},
)
url = result.content[0].text
assert "MODIS_Terra_CorrectedReflectance_TrueColor" in url
assert "2025-06-01" in url
assert "epsg4326" in url
# True color layer uses JPEG and 250m matrix set
assert url.endswith(".jpg")
assert "/250m/" in url
finally:
await server_module._client.close()
server_module._client = None
@respx.mock
async def test_list_tools(capabilities_xml):
"""All expected tools are registered on the MCP server."""
respx.get(url__regex=r".*WMTSCapabilities\.xml").mock(
return_value=httpx.Response(200, text=capabilities_xml)
)
server_module._client = await _init_mock_client(capabilities_xml)
try:
async with Client(mcp) as client:
tools = await client.list_tools()
tool_names = {t.name for t in tools}
expected = {
"search_gibs_layers",
"get_layer_info",
"list_measurements",
"check_layer_dates",
"get_imagery",
"compare_dates",
"get_imagery_composite",
"explain_layer_colormap",
"get_legend",
"resolve_place",
"build_tile_url",
}
for name in expected:
assert name in tool_names, f"Missing tool: {name}"
finally:
await server_module._client.close()
server_module._client = None