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 82ae699c07
5 changed files with 501 additions and 1 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.

168
README.md Normal file
View File

@ -0,0 +1,168 @@
# 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 |
## Example Workflows
### Investigating a wildfire
```
User: Use the investigate_event prompt for the 2025 LA wildfires
→ search_gibs_layers("fire")
→ search_gibs_layers("true color")
→ check_layer_dates("MODIS_Terra_CorrectedReflectance_TrueColor")
→ get_imagery("MODIS_Terra_CorrectedReflectance_TrueColor", "2025-01-08", place="Los Angeles")
→ get_imagery("MODIS_Fires_Terra", "2025-01-08", place="Los Angeles")
→ compare_dates("MODIS_Terra_CorrectedReflectance_TrueColor", "2025-01-01", "2025-01-10", place="Los Angeles")
→ explain_layer_colormap("MODIS_Fires_Terra")
```
### Monitoring sea ice decline
```
User: Use climate_monitor to track Arctic sea ice from March to September
→ search_gibs_layers("sea ice")
→ check_layer_dates("AMSR2_Sea_Ice_Concentration_12km")
→ explain_layer_colormap("AMSR2_Sea_Ice_Concentration_12km")
→ compare_dates("AMSR2_Sea_Ice_Concentration_12km", "2025-03-01", "2025-09-01", bbox=[-180,60,180,90])
→ get_legend("AMSR2_Sea_Ice_Concentration_12km")
```
### Quick look at anywhere on Earth
```
User: Use satellite_snapshot for the Great Barrier Reef
→ resolve_place("Great Barrier Reef")
→ get_imagery("MODIS_Terra_CorrectedReflectance_TrueColor", "2025-06-01", place="Great Barrier Reef")
→ search_gibs_layers("chlorophyll")
→ get_imagery_composite(["MODIS_Terra_CorrectedReflectance_TrueColor", "MODIS_Terra_Chlorophyll_A"], "2025-06-01", place="Great Barrier Reef")
```
## 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

@ -2,14 +2,16 @@
name = "mcgibs" name = "mcgibs"
version = "2026.02.18" version = "2026.02.18"
description = "FastMCP server for NASA Global Imagery Browse Services (GIBS)" description = "FastMCP server for NASA Global Imagery Browse Services (GIBS)"
readme = "CLAUDE.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
license = {text = "MIT"}
authors = [{name = "Ryan Malloy", email = "ryan@supported.systems"}] authors = [{name = "Ryan Malloy", email = "ryan@supported.systems"}]
keywords = ["nasa", "gibs", "mcp", "satellite", "imagery", "earth-science"] keywords = ["nasa", "gibs", "mcp", "satellite", "imagery", "earth-science"]
classifiers = [ classifiers = [
"Development Status :: 4 - Beta", "Development Status :: 4 - Beta",
"Intended Audience :: Developers", "Intended Audience :: Developers",
"Intended Audience :: Science/Research", "Intended Audience :: Science/Research",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.14",
@ -21,6 +23,11 @@ dependencies = [
"defusedxml>=0.7.1", "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] [project.scripts]
mcgibs = "mcgibs.server:main" mcgibs = "mcgibs.server:main"

View File

@ -746,6 +746,251 @@ def earth_overview() -> str:
return "\n".join(lines) 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 # Entry point
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

View File

@ -277,6 +277,65 @@ async def test_list_resources(capabilities_xml):
server_module._client = None 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 @respx.mock
async def test_colormap_resource(capabilities_xml, colormap_xml): async def test_colormap_resource(capabilities_xml, colormap_xml):
"""gibs://colormap/{layer_id} returns colormap explanation text.""" """gibs://colormap/{layer_id} returns colormap explanation text."""