diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3edc958 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Ryan Malloy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5b392f0 --- /dev/null +++ b/README.md @@ -0,0 +1,274 @@ +# mcgibs + +NASA Earth science visualizations for LLMs. + +An [MCP](https://modelcontextprotocol.io/) server that connects language models to [NASA GIBS](https://www.earthdata.nasa.gov/engage/open-data-services-software/earthdata-developer-portal/gibs-api) (Global Imagery Browse Services) — 1000+ visualization layers covering satellite imagery, scientific data products, and derived Earth observations, updated daily. + +**Three pillars:** + +- **Discovery** — search layers by keyword, browse measurement categories, check date availability +- **Visualization** — fetch imagery and data products by place name and date, compare dates side-by-side, composite multiple layers +- **Interpretation** — natural-language colormap explanations, legend graphics, scientific context + +No API key required. All data is freely available from NASA. + +## Quick Start + +### From PyPI + +```bash +uvx mcgibs +``` + +### Add to Claude Code + +```bash +claude mcp add mcgibs -- uvx mcgibs +``` + +### Local development + +```bash +git clone https://git.supported.systems/mcp/mcgibs.git +cd mcgibs +uv sync --all-extras +uv run mcgibs +``` + +Or add a local dev server to Claude Code: + +```bash +claude mcp add mcgibs-local -- uv run --directory /path/to/mcgibs mcgibs +``` + +## Tools + +| Tool | Description | +|------|-------------| +| `search_gibs_layers` | Search 1000+ layers by keyword, measurement, period, or status | +| `get_layer_info` | Full metadata for a layer — instrument, platform, resolution, dates | +| `list_measurements` | All measurement categories with layer counts | +| `check_layer_dates` | Available date range for a layer (capabilities + live DescribeDomains) | +| `get_imagery` | Fetch a visualization by layer, date, and place name or bbox | +| `compare_dates` | Side-by-side comparison of two dates for change detection | +| `get_imagery_composite` | Overlay up to 5 layers into a single composite image | +| `explain_layer_colormap` | Natural-language explanation of what colors represent | +| `get_legend` | Pre-rendered legend graphic for a layer | +| `resolve_place` | Geocode a place name to coordinates and bounding box | +| `build_tile_url` | Construct a direct WMTS tile URL for embedding | + +## Resources + +| URI | Description | +|-----|-------------| +| `gibs://catalog` | Full layer catalog grouped by measurement category | +| `gibs://layer/{layer_id}` | Individual layer metadata as JSON | +| `gibs://colormap/{layer_id}` | Colormap explanation for a layer | +| `gibs://dates/{layer_id}` | Available date range for a layer | +| `gibs://projections` | Supported GIBS projections and endpoints | + +## Prompts + +| Prompt | Parameters | Description | +|--------|------------|-------------| +| `earth_overview` | *(none)* | Introduction to GIBS with suggested explorations | +| `investigate_event` | `event_type`, `location`, `date` | Guided workflow for investigating natural events | +| `satellite_snapshot` | `place`, `date` | Quick satellite view of any location | +| `climate_monitor` | `indicator`, `location`, `start_date`, `end_date` | Track climate changes over time | +| `layer_deep_dive` | `layer_id`, `location`, `date` | Full scientific analysis of a single layer | +| `multi_layer_story` | `topic`, `location`, `date` | Data journalism — composite layers to tell a story | +| `polar_watch` | `pole`, `date`, `compare_date` | Arctic/Antarctic ice and snow monitoring | + +## Conversational Examples + +These examples show what interacting with mcgibs looks like from any MCP client. Tool calls and responses are abbreviated for readability. + +### "What's happening in the Amazon right now?" + +*Try this with the `satellite_snapshot` prompt: `place="Amazon Rainforest"`* + +> **You:** Show me what the Amazon rainforest looks like from space today. + +The assistant resolves the location, fetches true color imagery, and searches for relevant science layers: + +``` +→ resolve_place("Amazon Rainforest") + {"display_name": "Amazon Rainforest", "lat": -3.46, "lon": -62.21, + "bbox": {"west": -73.98, "south": -16.52, "east": -43.75, "north": 5.27}} + +→ get_imagery("MODIS_Terra_CorrectedReflectance_TrueColor", "2025-06-01", + place="Amazon Rainforest") + [image: true color view of the Amazon basin] + +→ search_gibs_layers("vegetation") + Found 12 layer(s) matching 'vegetation': + MODIS_Terra_NDVI_8Day + MODIS/Terra Vegetation Index (NDVI), 8-Day — from 2000-02-18 (8-Day) + [has colormap] + MODIS_Terra_EVI_8Day + MODIS/Terra Enhanced Vegetation Index (EVI), 8-Day — from 2000-02-18 (8-Day) + [has colormap] + ... +``` + +> **Assistant:** Here's the Amazon from MODIS Terra today — you can see the river system threading through dense canopy, with some cloud cover over the western basin. I also found vegetation index layers. Want me to overlay NDVI to show vegetation density, or check for fire/deforestation activity? + +--- + +### Understanding what the colors mean + +*Try this with the `layer_deep_dive` prompt: `layer_id="AIRS_L3_Surface_Air_Temperature_Daily_Day"`* + +> **You:** What does the AIRS surface temperature layer actually show? + +The assistant fetches metadata and the colormap interpretation: + +``` +→ get_layer_info("AIRS_L3_Surface_Air_Temperature_Daily_Day") + {"identifier": "AIRS_L3_Surface_Air_Temperature_Daily_Day", + "title": "Surface Air Temperature (Day, Daily)", + "instrument": "AIRS", "platform": "Aqua", + "period": "Daily", "ongoing": true, + "time": {"start": "2002-08-30", "end": null}} + +→ explain_layer_colormap("AIRS_L3_Surface_Air_Temperature_Daily_Day") + Surface Air Temperature (K): + - dark blue = ~213 K (-60 C) -- extreme cold + - cyan = ~243 K (-30 C) -- severe cold + - green = ~268 K (-5 C) -- near freezing + - yellow = ~293 K (20 C) -- mild + - dark red = ~323 K (50 C) -- extreme heat + Color gradient runs from dark blues (low values) through greens to dark reds (high values). + (254 color stops total, 5 key values shown) + Transparent / no-data: No Data + +→ get_legend("AIRS_L3_Surface_Air_Temperature_Daily_Day") + [image: horizontal color bar from blue to red, labeled 213K to 323K] +``` + +> **Assistant:** The AIRS instrument on NASA's Aqua satellite measures surface air temperature globally every day, and has been doing so since August 2002. The colormap runs from dark blue (around -60°C, polar regions) through greens (near freezing) to dark red (above 50°C, desert extremes). The 254 color stops give smooth gradation — areas with no valid retrieval show as transparent. + +--- + +### Tracking change over time + +*Try this with the `climate_monitor` prompt: `indicator="sea ice", location="Arctic", start_date="2025-03-01", end_date="2025-09-01"`* + +> **You:** How has Arctic sea ice changed between winter and summer this year? + +The assistant uses the `climate_monitor` workflow — search, verify dates, compare: + +``` +→ search_gibs_layers("sea ice concentration") + Found 6 layer(s) matching 'sea ice concentration': + AMSR2_Sea_Ice_Concentration_12km + AMSR2 Sea Ice Concentration, 12 km — from 2012-07-02 (Daily) + [has colormap] + ... + +→ explain_layer_colormap("AMSR2_Sea_Ice_Concentration_12km") + Sea Ice Concentration (%): + - dark blue = ~15% + - cyan = ~40% + - light grey = ~70% + - white = ~100% + Color gradient runs from dark blues (low values) through cyans to whites (high values). + Transparent / no-data: No Data, Land + +→ compare_dates("AMSR2_Sea_Ice_Concentration_12km", + "2025-03-01", "2025-09-01", + bbox=[-180, 60, 180, 90]) + [image: side-by-side comparison — March (left) vs September (right)] +``` + +> **Assistant:** The side-by-side view is striking. In March, the Arctic Ocean is almost entirely white (90-100% ice concentration), with the ice edge extending well into the Bering Sea and Barents Sea. By September, the ice has retreated dramatically — the white core is confined to a smaller region around the pole, with large stretches of dark blue (15-40%) and open water visible along the Northeast Passage and in Hudson Bay. The colormap tells us white = solid pack ice and dark blue = scattered ice at 15% concentration. + +--- + +### Compositing layers to tell a story + +*Try this with the `investigate_event` prompt: `event_type="wildfire", location="Los Angeles", date="2025-01-08"`* + +> **You:** Can you show me how wildfire smoke from the 2025 LA fires affected air quality? + +The assistant searches for complementary layers, checks dates, and builds a composite: + +``` +→ search_gibs_layers("aerosol") + Found 8 layer(s) matching 'aerosol': + MODIS_Terra_Aerosol_Optical_Depth + MODIS/Terra Aerosol Optical Depth — from 2000-02-24 (Daily) + [has colormap] + ... + +→ search_gibs_layers("fire thermal") + Found 4 layer(s) matching 'fire thermal': + MODIS_Fires_Terra + MODIS/Terra Thermal Anomalies and Fire — from 2000-02-24 (Daily) + [has colormap] + ... + +→ get_imagery_composite( + ["MODIS_Terra_CorrectedReflectance_TrueColor", + "MODIS_Terra_Aerosol_Optical_Depth"], + "2025-01-08", place="Los Angeles") + [image: true color with aerosol optical depth overlay] + +→ explain_layer_colormap("MODIS_Terra_Aerosol_Optical_Depth") + Aerosol Optical Depth: + - yellow-green = ~0.15 (clean air) + - orange = ~0.55 (moderate haze) + - dark red = ~1.0 (thick smoke/dust) + ... +``` + +> **Assistant:** The composite overlay tells the story clearly. Over the LA basin, the true color base shows a visible smoke plume extending southwest over the Pacific. The aerosol optical depth overlay confirms it — the orange-to-red values (0.5-1.0) directly over the fire zone indicate thick particulate loading, fading to yellow-green (0.15, clean air) further out over the ocean. The fire thermal anomaly layer pinpoints the active burn areas as bright red hotspots in the foothills. + +## Projections + +| EPSG | Description | Use case | +|------|-------------|----------| +| 4326 | Geographic (WGS84) | Default — global coverage, most layers | +| 3857 | Web Mercator | Web map tiles, Leaflet/Mapbox integration | +| 3413 | Arctic Polar Stereographic | Arctic-focused imagery | +| 3031 | Antarctic Polar Stereographic | Antarctic-focused imagery | + +## Development + +```bash +uv sync --all-extras + +# Lint +uv run ruff check src/ tests/ + +# Tests +uv run pytest + +# Build +uv build +``` + +## Architecture + +``` +src/mcgibs/ + server.py MCP server — tools, resources, prompts, middleware + client.py GIBS HTTP client — WMS, WMTS, colormaps, geocoding + capabilities.py WMTS GetCapabilities parser and layer search + colormaps.py Colormap XML parser and natural-language interpreter + models.py Pydantic models — Layer, BBox, GeoResult, ColormapEntry + constants.py API endpoints, projections, tile matrix definitions + geo.py Bounding box math and geocoding helpers +``` + +## License + +[MIT](LICENSE) + +## Links + +- [NASA GIBS](https://www.earthdata.nasa.gov/engage/open-data-services-software/earthdata-developer-portal/gibs-api) +- [GIBS API Documentation](https://nasa-gibs.github.io/gibs-api-docs/) +- [Worldview](https://worldview.earthdata.nasa.gov/) — NASA's browser-based GIBS viewer +- [FastMCP](https://gofastmcp.com/) — the MCP framework powering this server +- [Source](https://git.supported.systems/mcp/mcgibs) diff --git a/pyproject.toml b/pyproject.toml index 5eb4d89..0c7c617 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,15 +1,17 @@ [project] name = "mcgibs" -version = "2026.02.18" +version = "2026.02.18.3" description = "FastMCP server for NASA Global Imagery Browse Services (GIBS)" -readme = "CLAUDE.md" +readme = "README.md" requires-python = ">=3.12" +license = {text = "MIT"} authors = [{name = "Ryan Malloy", email = "ryan@supported.systems"}] keywords = ["nasa", "gibs", "mcp", "satellite", "imagery", "earth-science"] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", @@ -21,6 +23,11 @@ dependencies = [ "defusedxml>=0.7.1", ] +[project.urls] +Homepage = "https://git.supported.systems/mcp/mcgibs" +Documentation = "https://nasa-gibs.github.io/gibs-api-docs/" +"Bug Tracker" = "https://git.supported.systems/mcp/mcgibs/issues" + [project.scripts] mcgibs = "mcgibs.server:main" diff --git a/src/mcgibs/__init__.py b/src/mcgibs/__init__.py index 46b0976..b126b39 100644 --- a/src/mcgibs/__init__.py +++ b/src/mcgibs/__init__.py @@ -1 +1 @@ -__version__ = "2026.02.18" +__version__ = "2026.02.18.3" diff --git a/src/mcgibs/server.py b/src/mcgibs/server.py index a5bf7dc..79313dc 100644 --- a/src/mcgibs/server.py +++ b/src/mcgibs/server.py @@ -746,6 +746,251 @@ def earth_overview() -> str: return "\n".join(lines) +@mcp.prompt +def satellite_snapshot( + place: str, + date: str = "today", +) -> str: + """Quick satellite view of any place on Earth. + + Args: + place: Location to view (e.g. "Tokyo", "Amazon Rainforest", "Sahara Desert"). + date: Date to view (YYYY-MM-DD), or "today" for the most recent imagery. + """ + lines = [ + f"Show me a satellite view of {place} on {date}.", + "", + "Follow this workflow:", + "", + f'1. **Geocode**: Use resolve_place to get coordinates for "{place}".', + "", + f"2. **True color first**: Fetch imagery for " + f'"MODIS_Terra_CorrectedReflectance_TrueColor" ' + f"on {date} at that location. This gives a natural-looking view.", + "", + f"3. **Find science layers**: Search for layers relevant to " + f'"{place}" — consider what would be scientifically interesting ' + f"for this region (vegetation for forests, sea surface temperature " + f"for coastal areas, snow cover for mountains, etc.).", + "", + "4. **Check dates**: Verify the science layers have data for " + f"{date} using check_layer_dates.", + "", + "5. **Overlay**: If a relevant science layer is available, " + "use get_imagery_composite to overlay it on the true color base.", + "", + "6. **Interpret**: If the overlaid layer has a colormap, " + "use explain_layer_colormap to describe what the colors mean.", + "", + f"7. **Describe**: Summarize what's visible in the satellite view " + f"of {place} — notable features, weather patterns, land use, " + f"or any phenomena visible from orbit.", + ] + return "\n".join(lines) + + +@mcp.prompt +def climate_monitor( + indicator: str, + location: str, + start_date: str, + end_date: str, +) -> str: + """Track a climate indicator over time at a specific location. + + Args: + indicator: What to monitor (e.g. "sea ice", "vegetation", "temperature", "snow cover"). + location: Region to monitor (e.g. "Arctic", "Amazon Basin", "Greenland"). + start_date: Beginning of monitoring period (YYYY-MM-DD). + end_date: End of monitoring period (YYYY-MM-DD). + """ + lines = [ + f"Monitor {indicator} changes near {location} " + f"from {start_date} to {end_date}.", + "", + "Follow this workflow:", + "", + f'1. **Find layers**: Search for GIBS layers related to "{indicator}". ' + f"Look for layers with colormaps — these encode quantitative data.", + "", + "2. **Check availability**: Use check_layer_dates on the best " + f"matching layer to confirm data exists for both {start_date} " + f"and {end_date}.", + "", + "3. **Understand the scale**: Use explain_layer_colormap to learn " + "what the color values represent (units, range). " + "Also fetch the legend with get_legend for visual reference.", + "", + f"4. **Compare endpoints**: Use compare_dates to create a " + f"side-by-side view of {start_date} vs {end_date} " + f'for "{location}". This immediately shows the magnitude of change.', + "", + "5. **Intermediate snapshots**: Fetch 1-2 additional dates between " + "start and end using get_imagery to show the progression " + "(e.g. mid-point of the range).", + "", + "6. **Synthesize**: Describe the observed trend, referencing " + "specific colormap values where possible. Is the indicator " + "increasing, decreasing, or showing seasonal variation? " + "What does the spatial pattern reveal?", + ] + return "\n".join(lines) + + +@mcp.prompt +def layer_deep_dive( + layer_id: str, + location: str = "global", + date: str = "", +) -> str: + """Full scientific analysis of a single GIBS layer. + + Args: + layer_id: The GIBS layer identifier (e.g. "AIRS_L3_Surface_Air_Temperature_Daily_Day"). + location: Region to focus on, or "global" for worldwide view. + date: Specific date (YYYY-MM-DD), or empty for the most recent available. + """ + location_desc = f'for "{location}"' if location != "global" else "at global scale" + date_desc = f"on {date}" if date else "using the most recent available date" + + lines = [ + f"Do a deep scientific analysis of the {layer_id} layer " + f"{location_desc} {date_desc}.", + "", + "Follow this workflow:", + "", + "1. **Layer metadata**: Use get_layer_info to retrieve full metadata " + f'for "{layer_id}" — instrument, platform, resolution, date range, ' + "and scientific description.", + "", + "2. **Color interpretation**: Use explain_layer_colormap to get a " + "detailed description of what each color represents, including " + "physical units and value ranges. Fetch the legend with get_legend.", + "", + f"3. **Sample imagery**: Use get_imagery to fetch the layer " + f"{location_desc} {date_desc}. If no date was specified, " + "use check_layer_dates to find the latest available date first.", + "", + "4. **Image interpretation**: Walk through what's visible in the " + "image, referencing specific colormap values. Identify notable " + "spatial patterns, anomalies, or features.", + "", + "5. **Scientific context**: Explain what this layer measures, " + "why it matters, what instrument collects the data, and how " + "scientists use it. Connect the visualization to the underlying " + "Earth science.", + ] + return "\n".join(lines) + + +@mcp.prompt +def multi_layer_story( + topic: str, + location: str, + date: str, +) -> str: + """Compose a data-driven narrative by combining multiple GIBS layers. + + Args: + topic: Story theme (e.g. "wildfire smoke", "ocean productivity", "dust storm"). + location: Region of interest. + date: Date for the story (YYYY-MM-DD). + """ + lines = [ + f'Tell the story of "{topic}" near {location} on {date} ' + f"by compositing multiple satellite layers.", + "", + "Follow this workflow:", + "", + f'1. **Find related layers**: Search for 2-4 layers related to "{topic}". ' + f"Choose layers that complement each other — for example, " + f"fire detections + aerosol optical depth + true color for wildfires, " + f"or SST + chlorophyll + true color for ocean events.", + "", + "2. **Verify dates**: Use check_layer_dates on each candidate " + f"to confirm data is available for {date}.", + "", + "3. **Understand each layer**: For each layer with a colormap, " + "use explain_layer_colormap to understand the value encoding.", + "", + "4. **Composite view**: Use get_imagery_composite to overlay " + "the layers into a single image. Put the base layer " + "(true color or reference) first in the list.", + "", + "5. **Individual layers**: Also fetch get_imagery for each layer " + "separately so you can describe what each one contributes.", + "", + "6. **Narrative**: Write a data-journalism style narrative " + "describing how the layers interact and what story they tell " + "together. Reference specific colormap values and spatial " + "patterns. Explain cause and effect where the data supports it.", + ] + return "\n".join(lines) + + +@mcp.prompt +def polar_watch( + pole: str, + date: str, + compare_date: str = "", +) -> str: + """Monitor Arctic or Antarctic ice and snow conditions. + + Args: + pole: Which pole — "arctic" or "antarctic". + date: Primary observation date (YYYY-MM-DD). + compare_date: Optional second date for before/after comparison (YYYY-MM-DD). + """ + if pole.lower().startswith("ant"): + region = "Antarctic" + bbox = "[-180, -90, 180, -60]" + else: + region = "Arctic" + bbox = "[-180, 60, 180, 90]" + + lines = [ + f"Monitor {region} ice and snow conditions on {date}.", + "", + "Follow this workflow:", + "", + f'1. **Find polar layers**: Search for "sea ice", "snow", ' + f'and "ice sheet" layers. Focus on layers that cover the ' + f"{region} region.", + "", + f"2. **Check availability**: Use check_layer_dates to verify " + f"data exists for {date} on the selected layers.", + "", + f"3. **Polar imagery**: Fetch imagery using the {region} " + f"bounding box {bbox}. Get true color first, then ice/snow " + f"concentration layers.", + "", + "4. **Color interpretation**: Use explain_layer_colormap on each " + "science layer and fetch legends with get_legend to understand " + "concentration values and classifications.", + "", + "5. **Embeddable tiles**: Use build_tile_url to generate a " + "WMTS URL for the primary ice layer — useful for embedding " + "in reports or dashboards.", + ] + + if compare_date: + lines.extend([ + "", + f"6. **Temporal comparison**: Use compare_dates to show " + f"{compare_date} vs {date} side-by-side. Describe the " + f"change in ice extent and concentration between these dates.", + ]) + else: + lines.extend([ + "", + f"6. **Summarize**: Describe the current {region} ice/snow " + f"state visible in the imagery, referencing colormap values " + f"for concentration percentages.", + ]) + + return "\n".join(lines) + + # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # Entry point # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/tests/test_tools.py b/tests/test_tools.py index ba4e4d7..3185b7d 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -277,6 +277,65 @@ async def test_list_resources(capabilities_xml): 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", + } + + 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."""