Add MCP prompts, README, LICENSE, and PyPI metadata

Five new prompts guide LLMs through multi-tool workflows:
satellite_snapshot, climate_monitor, layer_deep_dive,
multi_layer_story, polar_watch. README documents all 11 tools,
5 resources, and 7 prompts with example conversations.
MIT license, project URLs, and updated classifiers for PyPI.
This commit is contained in:
Ryan Malloy 2026-02-18 19:36:08 -07:00
parent 4a5035ca52
commit 110b85c829
6 changed files with 609 additions and 3 deletions

21
LICENSE Normal file
View File

@ -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.

274
README.md Normal file
View File

@ -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/rpm/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/rpm/mcgibs)

View File

@ -1,15 +1,17 @@
[project]
name = "mcgibs"
version = "2026.02.18"
version = "2026.02.18.2"
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/rpm/mcgibs"
Documentation = "https://nasa-gibs.github.io/gibs-api-docs/"
"Bug Tracker" = "https://git.supported.systems/rpm/mcgibs/issues"
[project.scripts]
mcgibs = "mcgibs.server:main"

View File

@ -1 +1 @@
__version__ = "2026.02.18"
__version__ = "2026.02.18.2"

View File

@ -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
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

View File

@ -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."""