MCP server that exposes environmental data — weather, soil, atmospheric composition,
and satellite observations — as tools callable by any MCP-compatible AI assistant or
workflow. Tools accept a location (point or bounding box) and a date range and return
structured JSON with the data and a _meta block that includes the data license,
citations, query latency, and enough information to reproduce the query.
Status: 9 sources have been made fully functional (NASA POWER,
SSURGO, SoilGrids, GBIF, TROPOMI, OpenAQ, ESS-DIVE, NASA EMIT, NASA OCO2).
The feeds family (live events: EONET, USGS earthquakes, NASA FIRMS, NWS
alerts, USGS Water Data, Open-Meteo) and six point accessors (Daymet, Macrostrat, 3DEP
elevation, ARM, EIA, ERA5 via CDS) have been drafted and are awaiting refactoring to more
closely follow the patterns applied in the fully functional adapters.
See "Feeds and point accessors" below.
Clone the repo and pull in dependencies:
git clone https://github.com/kbaseincubator/env-data-mcp
cd env-data-mcp
uv syncStart the server (stdio transport; runs until killed with Ctrl-C)
uv run env-data-mcpThe server prints no output on start; an MCP client connects via stdio.
With the server running, verify it works with this self-contained Python snippet:
# hello_world.py — run with: uv run python hello_world.py
import asyncio
import json
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
params = StdioServerParameters(
command="uv",
args=["run", "env-data-mcp"],
)
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
raw_result = await session.call_tool(
"nasa_power_merra2_point_query",
arguments={
"latitude": 46.253,
"longitude": -119.477,
"start_date": "2023-05-01",
"end_date": "2023-05-03",
"variables": ["T2M", "PRECTOTCORR"],
"temporal_resolution": "daily",
},
)
result = json.loads(raw_result.content[0].text)
print(result)
asyncio.run(main())Expected output shape:
{
'data': [
{
'geometry': {'type': 'Point', 'coordinates': [-119.375, 46.5]},
'records': [
{'date': '2023-05-01', 'T2M': 17.22998046875, 'T2M_units': 'C', 'PRECTOTCORR': 0.65997314453125, 'PRECTOTCORR_units': 'mm'},
{'date': '2023-05-02', 'T2M': 14.72998046875, 'T2M_units': 'C', 'PRECTOTCORR': 0.1500244140625, 'PRECTOTCORR_units': 'mm'},
{'date': '2023-05-03', 'T2M': 19.72998046875, 'T2M_units': 'C', 'PRECTOTCORR': 0.0, 'PRECTOTCORR_units': 'mm'}
],
'latitude': 46.5,
'longitude': -119.375
}
],
'_meta': {
'source': 'nasa_power',
'success': True,
'geometries_returned': 1,
'total_records_returned': 3,
'latency_s': 0.803849,
'auth_required': False,
'auth_present': True,
'error': None,
'license': 'There are no restrictions on the use...',
'license_url': '',
'citation': 'NASA Prediction of Worldwide Energy Resources (POWER)...',
'query_params': {
'latitude': 46.253,
'longitude': -119.477,
'start_date': '2023-05-01',
'end_date': '2023-05-03',
'variables': ['T2M', 'PRECTOTCORR'],
'temporal_resolution': 'daily',
'max_runtime_s': 30.0
},
'variables': ['T2M', 'PRECTOTCORR'],
'variable_info': {
'T2M': {'description': 'Temperature at 2 Meters', 'units': 'C'},
'PRECTOTCORR': {'description': 'Precipitation Corrected', 'units': 'mm'}
},
'unavailable_variables': [],
'citation_urls': ['https:\/\/nasa-power.s3.amazonaws.com/CITATION.cff', 'https:\/\/power.larc.nasa.gov/docs/methodology/citations/'],
'description': "The Modern-Era Retrospective analysis...",
'description_url': 'https:\/\/registry.opendata.aws/nasa-power/',
'acknowledgements': ''
}
}To start the GUI (runs until killed with Ctrl-C):
uv run env-data-mcp --guiThen navigate to the URL specified after Running on local URL: (typically http://127.0.0.1:7860)
You should see something like this:
Click on a data provider, choose a dataset, and run the queries interactively. You will see formatted JSON output for query responses.
Add to your VS Code workspace .mcp.json to make all tools available to GitHub Copilot:
{
"mcpServers": {
"env-data": {
"command": "uv",
"args": ["--directory", "/path/to/env-data-mcp", "run", "env-data-mcp"],
"env": {
"EARTHDATA_TOKEN": "${EARTHDATA_TOKEN}",
"OPENAQ_API_KEY": "${OPENAQ_API_KEY}",
"ESSDIVE_TOKEN": "${ESSDIVE_TOKEN}"
}
}
}
}Replace /path/to/env-data-mcp with the absolute path to your local clone. The ${VAR} syntax reads from your shell environment (or from a .env file if your MCP host supports it).
If the package wheel has been installed into the JupyterHub environment:
{
"mcpServers": {
"env-data": {
"command": "uvx",
"args": ["--from", "env-data-mcp", "env-data-mcp"],
"env": {
"EARTHDATA_TOKEN": "${EARTHDATA_TOKEN}",
"OPENAQ_API_KEY": "${OPENAQ_API_KEY}",
"ESSDIVE_TOKEN": "${ESSDIVE_TOKEN}"
}
}
}
}See Credential setup for how to obtain each token.
| Tool | Source | Auth | Description |
|---|---|---|---|
essdive_bbox_query |
ESS-DIVE | ESS-DIVE token (free) | DOE environmental field datasets within a bounding box |
essdive_point_query |
ESS-DIVE | ESS-DIVE token (free) | DOE environmental field datasets near a point |
gbif_occurrence_bbox_query |
GBIF | none | Species occurrence records within a bounding box |
gbif_occurrence_point_query |
GBIF | none | Species occurrence records within a radius |
nasa_emit_bbox_query |
NASA EMIT L2B | NASA EarthData token (free) | Mineral identification over a bounding box |
nasa_emit_point_query |
NASA EMIT L2B | NASA EarthData token (free) | Mineral identification at a point |
nasa_oco2_bbox_query |
NASA OCO-2 GEOS L3 | NASA EarthData token (free) | Daily XCO2 column over a bounding box |
nasa_oco2_point_query |
NASA OCO-2 GEOS L3 | NASA EarthData token (free) | Daily XCO2 column at a point |
nasa_power_merra2_bbox_query |
NASA POWER MERRA-2 | none | Atmospheric data over a bounding box |
nasa_power_merra2_point_query |
NASA POWER MERRA-2 | none | Atmospheric data (T, precip, RH, radiation) at a point |
nasa_power_syn1deg_bbox_query |
NASA POWER CERES SYN1deg | none | Radiation fluxes over a bounding box |
nasa_power_syn1deg_point_query |
NASA POWER CERES SYN1deg | none | Radiation fluxes at a point |
openaq_bbox_query |
OpenAQ v3 | API key (free) | Surface air quality measurements within a bounding box |
openaq_point_query |
OpenAQ v3 | API key (free) | Surface air quality measurements near a point |
soilgrids_bbox_query |
ISRIC SoilGrids v2 | none | Global soil properties over a bounding box |
soilgrids_point_query |
ISRIC SoilGrids v2 | none | Global soil properties at a point |
ssurgo_*_bbox_query |
USDA SSURGO | none | Soil properties within a bounding box |
ssurgo_*_query |
USDA SSURGO | none | Soil properties for a US point |
tropomi_bbox_query |
Sentinel-5P TROPOMI | none | Atmospheric composition over a bounding box |
tropomi_point_query |
Sentinel-5P TROPOMI | none | Atmospheric composition at a point location |
* For SSURGO tools, replace the (*) with one of: area_summary, ecological_site, parent_material, seasonal_hydrology, soil_profile, soil_suitability, soil_temperature, or subsurface_barriers.
Live events share one schema, models.EventRecord (id, source, kind, title, lat, lon, geometry, t_start, t_end, magnitude, magnitude_unit, severity, url, licence + source properties), and
every feed tool returns {data: [EventRecord, …], _meta}. _meta gains ttl_s (the source's
cache TTL, honoured in-process), fetched_at, cached, and for keyed or rate-limited sources
quota = {limit, window_s, used, remaining, resets_in_s} — at the limit the tool refuses with a
normal response, never an exception. A keyed tool without its key answers
auth_required: true, auth_present: false and empty data. Keys travel in headers (or, for
FIRMS, in a URL that is never returned or logged); no key is ever echoed.
| Tool | Source | Auth | TTL | Description |
|---|---|---|---|---|
arm_nearest |
DOE ARM | none (site table) | — | The nearest ARM fixed observatory and its distance |
daymet_at |
Daymet (ORNL DAAC) | none | 24 h | Daily tmax / tmin / prcp … at a 1 km pixel on one date, North America 1980– |
eia_plants_near |
EIA API v2 (EIA-860M) | EIA_API_KEY |
24 h | Power plants within a radius, capacity by fuel [field names unverified live until the integration test runs with a key] |
elevation_3dep |
USGS 3DEP (EPQS) | none | 30 d | Ground elevation at a point (US); the service can take 10–15 s |
eonet_events |
NASA EONET v3 | none | 30 min | Curated natural events (fires, storms, volcanoes, floods, ice …), optional bbox, last N days |
era5_monthly_at |
ERA5 via CDS | CDS_API_KEY |
30 d | Monthly means at the nearest 0.25° point; a queued job — answers queued: with a job_id to resume |
macrostrat_at |
Macrostrat | none | 7 d | The geologic map unit at a point: lithology, age, interval |
nasa_firms_fires |
NASA FIRMS | NASA_FIRMS_MAP_KEY (5,000 / 10 min) |
30 min | VIIRS / MODIS fire detections in a bbox, FRP and confidence |
nws_alerts_at |
NWS api.weather.gov | none | 10 min | Active alerts at a point or for a state/marine area (US) |
open_meteo_current |
Open-Meteo | none; gated unless ENV_DATA_ALLOW_NC=1 |
15 min | Current conditions + daily forecast at a point (non-commercial terms) |
usgs_quakes_events |
USGS FDSN / ComCat | none | 5 min | Earthquakes with magnitude, depth, PAGER alert level, optional bbox |
usgs_water_latest |
USGS Water Data OGC API | none 50/h · USGS_WATERDATA_API_KEY 1,000/h |
15 min | Latest instantaneous values per monitoring location (discharge, gage height …) |
Not built, and why: NWI wetlands — the point/envelope query answers HTTP 400 for every
variant (probed 2026-09); ARM Live data — its API takes user:token in the query string, which
this server never does (a credential in a URL); the site table stands in until a header-based path
exists.
| Variable | Required by | Description |
|---|---|---|
CDS_API_KEY |
ERA5 (CDS) | A CDS Personal Access Token (cds.climate.copernicus.eu/how-to-api); accept the ERA5 dataset licence once on the CDS site |
EARTHDATA_TOKEN |
NASA EMIT, NASA OCO2 | NASA EarthData bearer token — register free at urs.earthdata.nasa.gov |
EIA_API_KEY |
EIA | Free key from eia.gov/opendata/register.php |
ENV_DATA_ALLOW_NC |
Open-Meteo | Set to 1 to attest non-commercial use; without it the tool answers gated |
ESSDIVE_TOKEN |
ESS-DIVE | ESS-DIVE API token — register free at ess-dive.lbl.gov |
NASA_FIRMS_MAP_KEY |
NASA FIRMS | Free MAP_KEY from firms.modaps.eosdis.nasa.gov/api/map_key — 5,000 transactions / 10 min |
OPENAQ_API_KEY |
OpenAQ | Free key from openaq.org — requests without a key are rejected by the API |
USGS_WATERDATA_API_KEY |
USGS Water Data (optional) | Free key from api.waterdata.usgs.gov/signup — lifts the keyless 50/h to 1,000/h |
- Python ≥ 3.11
- uv (install with
pip install uvorcurl -Lsf https://astral.sh/uv/install.sh | sh)
uv sync --extra devuv run pytest tests/unit/ -m "not integration" -vExpected output: 250+ unit tests pass; all HTTP / S3 calls are mocked.
uv run pytest tests/unit/ -m "not integration" --cov=env_data_mcp --cov-report=html
# then open htmlcov/index.htmluv run pytest tests/ -m integration -vOpenAQ integration tests also require OPENAQ_API_KEY to be set.
Available variables are cached in the repo in json files. When the test suite is run
(locally or in GitHub Actions), tests verify that the cached variable information is
up-to-date with the live services. If they are out-of-date, the tests fail. The cache
files can be re-synced by running pytest with a --update-caches flag:
uv run pytest tests/integration/test_variable_caches_live.py --update-cachesTwo demonstration notebooks are included:
| Notebook | Description |
|---|---|
notebooks/grow_point_sample_demo.ipynb |
All 6 sources for 5 real GROW field samples |
notebooks/pnnl_bbox_demo.ipynb |
NASA POWER + Sentinel-5P over the PNNL Richland bbox |
# load notebook dependencies
uv sync --extra dev --extra notebook
# Run interactively
jupyter lab notebooks/
# Or run headlessly via nbmake (network required; S5P cells take 30–120 s each)
uv run pytest notebooks/ --no-covEach data source adapter carries a LICENSE_INFO constant with SPDX identifier, full
licence name, and URL. Human-readable licence text and citation requirements for all
sources are collected in LICENSES.md.
| Source | Licence |
|---|---|
| ARM | Free and open (ARM data use guidelines) |
| Daymet | Open (ORNL DAAC) |
| EIA | Public domain (US Government) |
| ERA5 (CDS) | CC BY 4.0 |
| ESS-DIVE | Varies per dataset |
| GBIF | CC0 / CC BY / CC BY-NC per record |
| Macrostrat | CC BY 4.0 |
| NASA EMIT | Public domain (NASA) |
| NASA EONET | Public domain (NASA) |
| NASA FIRMS | NASA data policy — free and open, attribution requested |
| NASA OCO-2 | Public domain (NASA) |
| NASA POWER | Public domain (NASA) |
| NWS alerts | Public domain (NOAA) |
| Open-Meteo | CC BY 4.0 — free API for non-commercial use only (gated) |
| OpenAQ | CC BY 4.0 |
| SoilGrids v2 | CC BY 4.0 |
| TROPOMI | ESA Copernicus Open Access |
| USDA SSURGO | Public domain (USDA) |
| USGS Earthquakes | Public domain (USGS) |
| USGS Water Data | Public domain (USGS) |
| USGS 3DEP | Public domain (USGS) |
To add a new data source:
- Create a
src/env_data_mcp/sources/<name>/folder for the data source. - At minimum include a
src/env_data_mcp/sources/<name>/tools.pyfile for the MCP tools - a. Include
<name>_available_variables,<name>_point_query, and<name>_bbox_queryfunctions decorated with@mcp.tool() - b. Alternatively create
<name>_<dataset>_available_variables,<name>_<dataset>_point_query, and<name>_<dataset>_bbox_querytool functions when the data source provides multiple datasets. - Write unit tests in
tests/unit/<name>/that mock all HTTP / S3 calls. - Write integration tests in
tests/integration/test_<name>_live.pymarked@pytest.mark.integrationthat call the live service and implement thetest_common_live.pyintegration tests. - Ensure all tool functions follow existing patterns for input arguments and return common response schema validated using
src/env_data_mcp/models.pyandsrc/env_data_mcp/helpers.py.
