"""FastMCP integration tests — tool calls against the real server, HTTP mocked.""" import json from io import BytesIO import httpx import respx from fastmcp import Client from PIL import Image as PILImage 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", "query_point", "get_time_series", } for name in expected: assert name in tool_names, f"Missing tool: {name}" finally: await server_module._client.close() server_module._client = None # --------------------------------------------------------------------------- # Image() return type tests # --------------------------------------------------------------------------- def _make_fake_jpeg() -> bytes: """Create a tiny valid JPEG for mocking WMS responses.""" buf = BytesIO() PILImage.new("RGB", (10, 10), "blue").save(buf, format="JPEG") return buf.getvalue() @respx.mock async def test_get_imagery_returns_image_content(capabilities_xml): """get_imagery returns TextContent + ImageContent (not raw dicts).""" respx.get(url__regex=r".*WMTSCapabilities\.xml").mock( return_value=httpx.Response(200, text=capabilities_xml) ) respx.get(url__regex=r".*wms\.cgi.*").mock( return_value=httpx.Response( 200, content=_make_fake_jpeg(), headers={"content-type": "image/jpeg"} ) ) server_module._client = await _init_mock_client(capabilities_xml) try: async with Client(mcp) as client: result = await client.call_tool( "get_imagery", { "layer_id": "MODIS_Terra_CorrectedReflectance_TrueColor", "date": "2025-06-01", "bbox": [-120.0, 30.0, -110.0, 40.0], }, ) assert len(result.content) == 2 assert result.content[0].type == "text" assert "Corrected Reflectance" in result.content[0].text assert result.content[1].type == "image" assert result.content[1].mimeType == "image/jpeg" finally: await server_module._client.close() server_module._client = None @respx.mock async def test_get_legend_returns_image_content(capabilities_xml): """get_legend returns TextContent + ImageContent for layers with legends.""" respx.get(url__regex=r".*WMTSCapabilities\.xml").mock( return_value=httpx.Response(200, text=capabilities_xml) ) fake_png = BytesIO() PILImage.new("RGB", (200, 20), "white").save(fake_png, format="PNG") respx.get(url__regex=r".*legends/.*\.png").mock( return_value=httpx.Response( 200, content=fake_png.getvalue(), headers={"content-type": "image/png"} ) ) server_module._client = await _init_mock_client(capabilities_xml) try: async with Client(mcp) as client: result = await client.call_tool( "get_legend", {"layer_id": "AMSR2_Sea_Ice_Concentration_12km_Monthly"}, ) assert len(result.content) == 2 assert result.content[0].type == "text" assert result.content[1].type == "image" assert result.content[1].mimeType == "image/png" finally: await server_module._client.close() server_module._client = None # --------------------------------------------------------------------------- # Dynamic resource tests # --------------------------------------------------------------------------- @respx.mock async def test_list_resources(capabilities_xml): """Server exposes all expected resources including dynamic templates.""" 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: templates = await client.list_resource_templates() template_uris = {t.uriTemplate for t in templates} assert "gibs://layer/{layer_id}" in template_uris assert "gibs://colormap/{layer_id}" in template_uris assert "gibs://dates/{layer_id}" in template_uris finally: await server_module._client.close() server_module._client = None # --------------------------------------------------------------------------- # Prompt tests # --------------------------------------------------------------------------- @respx.mock async def test_list_prompts(capabilities_xml): """All expected prompts 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: prompts = await client.list_prompts() prompt_names = {p.name for p in prompts} expected = { "investigate_event", "earth_overview", "satellite_snapshot", "climate_monitor", "layer_deep_dive", "multi_layer_story", "polar_watch", "quantitative_snapshot", "seasonal_timelapse", } for name in expected: assert name in prompt_names, f"Missing prompt: {name}" finally: await server_module._client.close() server_module._client = None @respx.mock async def test_satellite_snapshot_prompt(capabilities_xml): """satellite_snapshot prompt includes the place name and expected tool references.""" 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.get_prompt( "satellite_snapshot", {"place": "Tokyo", "date": "2025-06-01"} ) text = result.messages[0].content.text assert "Tokyo" in text assert "resolve_place" in text assert "get_imagery" in text or "get_imagery_composite" in text assert "MODIS_Terra_CorrectedReflectance_TrueColor" in text finally: await server_module._client.close() server_module._client = None @respx.mock async def test_colormap_resource(capabilities_xml, colormap_xml): """gibs://colormap/{layer_id} returns colormap explanation text.""" respx.get(url__regex=r".*WMTSCapabilities\.xml").mock( return_value=httpx.Response(200, text=capabilities_xml) ) respx.get(url__regex=r".*colormaps/v1\.3/.*\.xml").mock( return_value=httpx.Response(200, text=colormap_xml) ) server_module._client = await _init_mock_client(capabilities_xml) try: async with Client(mcp) as client: result = await client.read_resource( "gibs://colormap/AIRS_L3_Surface_Air_Temperature_Daily_Day" ) text = result[0].text assert "Surface Air Temperature" in text assert "K" in text finally: await server_module._client.close() server_module._client = None # --------------------------------------------------------------------------- # query_point tests # --------------------------------------------------------------------------- def _make_fake_png(rgb: tuple[int, int, int], size: int = 3) -> bytes: """Create a tiny PNG with uniform color for mocking point queries.""" buf = BytesIO() PILImage.new("RGBA", (size, size), (*rgb, 255)).save(buf, format="PNG") return buf.getvalue() @respx.mock async def test_query_point(capabilities_xml, colormap_xml): """query_point reverse-maps pixel RGB through colormap to data value.""" respx.get(url__regex=r".*WMTSCapabilities\.xml").mock( return_value=httpx.Response(200, text=capabilities_xml) ) # Return a 3x3 PNG with rgb (255,100,50) → maps to [290,300) K respx.get(url__regex=r".*wms\.cgi.*").mock( return_value=httpx.Response( 200, content=_make_fake_png((255, 100, 50)), headers={"content-type": "image/png"}, ) ) respx.get(url__regex=r".*colormaps/v1\.3/.*\.xml").mock( return_value=httpx.Response(200, text=colormap_xml) ) server_module._client = await _init_mock_client(capabilities_xml) try: async with Client(mcp) as client: result = await client.call_tool( "query_point", { "layer_id": "AIRS_L3_Surface_Air_Temperature_Daily_Day", "date": "2025-06-01", "lat": 35.67, "lon": 139.65, }, ) data = json.loads(result.content[0].text) assert data["lat"] == 35.67 assert data["lon"] == 139.65 assert data["quality"] == "exact" # exact RGB match assert "value" in data finally: await server_module._client.close() server_module._client = None @respx.mock async def test_query_point_no_colormap(capabilities_xml): """query_point returns error for layers without colormaps.""" 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( "query_point", { "layer_id": "MODIS_Terra_CorrectedReflectance_TrueColor", "date": "2025-06-01", "lat": 35.67, "lon": 139.65, }, ) text = result.content[0].text assert "no colormap" in text finally: await server_module._client.close() server_module._client = None # --------------------------------------------------------------------------- # get_time_series tests # --------------------------------------------------------------------------- @respx.mock async def test_get_time_series(capabilities_xml): """get_time_series returns multiple dated images.""" respx.get(url__regex=r".*WMTSCapabilities\.xml").mock( return_value=httpx.Response(200, text=capabilities_xml) ) respx.get(url__regex=r".*wms\.cgi.*").mock( return_value=httpx.Response( 200, content=_make_fake_jpeg(), headers={"content-type": "image/jpeg"}, ) ) server_module._client = await _init_mock_client(capabilities_xml) try: async with Client(mcp) as client: result = await client.call_tool( "get_time_series", { "layer_id": "MODIS_Terra_CorrectedReflectance_TrueColor", "start_date": "2025-01-01", "end_date": "2025-06-01", "bbox": [-120.0, 30.0, -110.0, 40.0], "steps": 3, }, ) # 3 steps → 3 text labels + 3 images = 6 content items texts = [c for c in result.content if c.type == "text"] images = [c for c in result.content if c.type == "image"] assert len(texts) == 3 assert len(images) == 3 assert "2025-01-01" in texts[0].text assert "2025-06-01" in texts[-1].text finally: await server_module._client.close() server_module._client = None # --------------------------------------------------------------------------- # New prompt tests # --------------------------------------------------------------------------- @respx.mock async def test_quantitative_snapshot_prompt(capabilities_xml): """quantitative_snapshot prompt includes locations and tool references.""" 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.get_prompt( "quantitative_snapshot", { "layer_id": "AIRS_L3_Surface_Air_Temperature_Daily_Day", "locations": "Tokyo, Sydney", "date": "2025-06-01", }, ) text = result.messages[0].content.text assert "Tokyo" in text assert "Sydney" in text assert "query_point" in text assert "explain_layer_colormap" in text finally: await server_module._client.close() server_module._client = None @respx.mock async def test_seasonal_timelapse_prompt(capabilities_xml): """seasonal_timelapse prompt includes dates and get_time_series reference.""" 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.get_prompt( "seasonal_timelapse", { "layer_id": "MODIS_Terra_CorrectedReflectance_TrueColor", "location": "Amazon Rainforest", "start_date": "2025-01-01", "end_date": "2025-12-01", }, ) text = result.messages[0].content.text assert "Amazon Rainforest" in text assert "get_time_series" in text assert "2025-01-01" in text assert "2025-12-01" in text finally: await server_module._client.close() server_module._client = None