stac-eo-mcp 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- stac_eo_mcp-0.1.0/.gitignore +11 -0
- stac_eo_mcp-0.1.0/LICENSE +21 -0
- stac_eo_mcp-0.1.0/PKG-INFO +223 -0
- stac_eo_mcp-0.1.0/README.md +193 -0
- stac_eo_mcp-0.1.0/pyproject.toml +75 -0
- stac_eo_mcp-0.1.0/src/stac_eo_mcp/__init__.py +3 -0
- stac_eo_mcp-0.1.0/src/stac_eo_mcp/__main__.py +4 -0
- stac_eo_mcp-0.1.0/src/stac_eo_mcp/analysis.py +241 -0
- stac_eo_mcp-0.1.0/src/stac_eo_mcp/catalog.py +353 -0
- stac_eo_mcp-0.1.0/src/stac_eo_mcp/config.py +41 -0
- stac_eo_mcp-0.1.0/src/stac_eo_mcp/errors.py +39 -0
- stac_eo_mcp-0.1.0/src/stac_eo_mcp/indices.py +186 -0
- stac_eo_mcp-0.1.0/src/stac_eo_mcp/models.py +312 -0
- stac_eo_mcp-0.1.0/src/stac_eo_mcp/raster.py +105 -0
- stac_eo_mcp-0.1.0/src/stac_eo_mcp/server.py +338 -0
- stac_eo_mcp-0.1.0/tests/__init__.py +0 -0
- stac_eo_mcp-0.1.0/tests/conftest.py +72 -0
- stac_eo_mcp-0.1.0/tests/integration/__init__.py +0 -0
- stac_eo_mcp-0.1.0/tests/integration/test_catalogs.py +60 -0
- stac_eo_mcp-0.1.0/tests/integration/test_compute_index.py +49 -0
- stac_eo_mcp-0.1.0/tests/integration/test_stdio.py +47 -0
- stac_eo_mcp-0.1.0/tests/test_analysis.py +119 -0
- stac_eo_mcp-0.1.0/tests/test_catalog.py +165 -0
- stac_eo_mcp-0.1.0/tests/test_config.py +32 -0
- stac_eo_mcp-0.1.0/tests/test_errors.py +11 -0
- stac_eo_mcp-0.1.0/tests/test_indices.py +152 -0
- stac_eo_mcp-0.1.0/tests/test_models.py +109 -0
- stac_eo_mcp-0.1.0/tests/test_server.py +237 -0
- stac_eo_mcp-0.1.0/tests/test_smoke.py +5 -0
- stac_eo_mcp-0.1.0/tests/test_time_series.py +166 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 bbrauzzi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: stac-eo-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server that lets AI agents search STAC catalogs and compute Sentinel-2 spectral indices (NDVI, NDWI, NBR).
|
|
5
|
+
Project-URL: Homepage, https://github.com/bbrauzzi/stac-eo-mcp
|
|
6
|
+
Project-URL: Repository, https://github.com/bbrauzzi/stac-eo-mcp
|
|
7
|
+
Project-URL: Issues, https://github.com/bbrauzzi/stac-eo-mcp/issues
|
|
8
|
+
Author-email: bbrauzzi <brauzzi@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agents,earth-observation,llm,mcp,ndvi,remote-sensing,sentinel-2,stac
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Requires-Dist: matplotlib>=3.9
|
|
22
|
+
Requires-Dist: mcp<3,>=2.3
|
|
23
|
+
Requires-Dist: odc-stac>=0.5
|
|
24
|
+
Requires-Dist: planetary-computer>=1.0
|
|
25
|
+
Requires-Dist: pydantic-settings>=2.4
|
|
26
|
+
Requires-Dist: pydantic>=2.7
|
|
27
|
+
Requires-Dist: pystac-client>=0.9
|
|
28
|
+
Requires-Dist: rioxarray>=0.17
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# stac-eo-mcp
|
|
32
|
+
|
|
33
|
+
[](https://github.com/bbrauzzi/stac-eo-mcp/actions/workflows/ci.yml)
|
|
34
|
+
[](https://pypi.org/project/stac-eo-mcp/)
|
|
35
|
+
[](https://pypi.org/project/stac-eo-mcp/)
|
|
36
|
+
[](LICENSE)
|
|
37
|
+
|
|
38
|
+
An [MCP](https://modelcontextprotocol.io) server that lets AI agents (Claude Desktop, Claude Code,
|
|
39
|
+
Cursor) **search satellite imagery in STAC catalogs and measure vegetation, water and burn
|
|
40
|
+
indices** on Sentinel-2 scenes, without downloading whole images.
|
|
41
|
+
|
|
42
|
+
Ask *"How green was Villa Borghese in Rome during 2025?"* and the agent finds the scenes, reads only
|
|
43
|
+
the pixels inside your area, masks clouds and answers with numbers, a map and a trend.
|
|
44
|
+
|
|
45
|
+
<p align="center">
|
|
46
|
+
<img src="https://raw.githubusercontent.com/bbrauzzi/stac-eo-mcp/main/docs/ndvi_map_rome.png" width="48%" alt="NDVI map of Villa Borghese, Rome, 30 July 2025">
|
|
47
|
+
<img src="https://raw.githubusercontent.com/bbrauzzi/stac-eo-mcp/main/docs/ndvi_timeseries_rome.png" width="50%" alt="NDVI time series over Villa Borghese, 2025">
|
|
48
|
+
</p>
|
|
49
|
+
<p align="center"><sub>Produced by <code>compute_index</code> and <code>time_series</code> on real data.
|
|
50
|
+
White pixels are clouds and shadows masked with the Sentinel-2 SCL layer.</sub></p>
|
|
51
|
+
|
|
52
|
+
<p align="center">
|
|
53
|
+
<img src="https://raw.githubusercontent.com/bbrauzzi/stac-eo-mcp/main/docs/demo.gif" width="100%"
|
|
54
|
+
alt="Demo: install with claude mcp add, ask Claude Code about Villa Borghese, get NDVI numbers and a map">
|
|
55
|
+
</p>
|
|
56
|
+
<p align="center"><sub>Install in Claude Code, ask a question, get numbers and a map
|
|
57
|
+
(a real session, replayed faster).</sub></p>
|
|
58
|
+
|
|
59
|
+
## Features
|
|
60
|
+
|
|
61
|
+
- **Two public catalogs**: [Microsoft Planetary Computer](https://planetarycomputer.microsoft.com)
|
|
62
|
+
(assets signed automatically) and [Element84 Earth Search](https://earth-search.aws.element84.com/v1).
|
|
63
|
+
No account or API key needed.
|
|
64
|
+
- **Windowed reads**: only the bbox is read from Cloud-Optimized GeoTIFFs with
|
|
65
|
+
[odc-stac](https://github.com/opendatacube/odc-stac), at 10 m in the scene's UTM grid.
|
|
66
|
+
- **Correct reflectance**: applies the Sentinel-2 processing baseline 04.00 offset (−1000 DN),
|
|
67
|
+
from asset metadata when present, otherwise from the baseline. Both catalogs give identical
|
|
68
|
+
numbers for the same scene.
|
|
69
|
+
- **Cloud masking with SCL**, reported as `valid_pixel_pct`. Tile cloud cover often differs from
|
|
70
|
+
your area (a "9.7% cloudy" tile can be 100% cloudy over your bbox), so this matters.
|
|
71
|
+
- **LLM-friendly output**: statistics, dates and file paths only; never raster arrays. Footprints
|
|
72
|
+
are simplified. Errors say what went wrong *and what to try next*.
|
|
73
|
+
- **Safe by default**: area and scene-count limits stop accidental large downloads.
|
|
74
|
+
|
|
75
|
+
## Tools
|
|
76
|
+
|
|
77
|
+
| Tool | What it does |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `list_collections(catalog?)` | Collections in a catalog, with one-sentence descriptions |
|
|
80
|
+
| `search_scenes(collection, bbox, date_range, max_cloud_cover?, limit?, catalog?)` | Scenes newest first: id, date, cloud cover, footprint, preview link |
|
|
81
|
+
| `get_scene_details(collection, item_id, catalog?)` | Key metadata and the list of assets |
|
|
82
|
+
| `compute_index(item_id, index, bbox, save?, catalog?)` | NDVI / NDWI / NBR on one scene: min, max, mean, std, % valid pixels; optional PNG / GeoTIFF |
|
|
83
|
+
| `time_series(index, bbox, date_range, max_cloud_cover?, catalog?)` | Mean index per acquisition date, cloudy dates skipped with a reason, plus a trend summary |
|
|
84
|
+
|
|
85
|
+
**Resources**: `stac://indices` (formulas, bands, interpretation, cloud mask classes) and
|
|
86
|
+
`stac://catalogs` (catalogs, defaults and limits).
|
|
87
|
+
**Prompt**: `analyze_vegetation(bbox, months=6)`, a guided vegetation analysis.
|
|
88
|
+
|
|
89
|
+
Conventions: `bbox` is `[west, south, east, north]` in WGS84 degrees; `date_range` is
|
|
90
|
+
`"YYYY-MM-DD/YYYY-MM-DD"`.
|
|
91
|
+
|
|
92
|
+
## Install
|
|
93
|
+
|
|
94
|
+
Requires [uv](https://docs.astral.sh/uv/). The server runs locally over stdio; nothing to deploy.
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
uvx stac-eo-mcp # runs the server (it waits for an MCP client on stdin)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Claude Desktop
|
|
101
|
+
|
|
102
|
+
Add to `claude_desktop_config.json` (Settings → Developer → Edit Config):
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"mcpServers": {
|
|
107
|
+
"stac-eo-mcp": {
|
|
108
|
+
"command": "uvx",
|
|
109
|
+
"args": ["stac-eo-mcp"],
|
|
110
|
+
"env": {
|
|
111
|
+
"STAC_EO_MCP_OUTPUT_DIR": "/Users/you/stac-eo-mcp-output"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
If Claude Desktop cannot find `uvx`, use its full path (`which uvx`).
|
|
119
|
+
|
|
120
|
+
### Claude Code
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
claude mcp add stac-eo-mcp -- uvx stac-eo-mcp
|
|
124
|
+
# with options:
|
|
125
|
+
claude mcp add stac-eo-mcp -e STAC_EO_MCP_CATALOG=earth-search -- uvx stac-eo-mcp
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Cursor
|
|
129
|
+
|
|
130
|
+
`.cursor/mcp.json`:
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"mcpServers": {
|
|
135
|
+
"stac-eo-mcp": { "command": "uvx", "args": ["stac-eo-mcp"] }
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Configuration
|
|
141
|
+
|
|
142
|
+
All optional, via environment variables:
|
|
143
|
+
|
|
144
|
+
| Variable | Default | Meaning |
|
|
145
|
+
|---|---|---|
|
|
146
|
+
| `STAC_EO_MCP_CATALOG` | `planetary-computer` | Default catalog (`planetary-computer` or `earth-search`) |
|
|
147
|
+
| `STAC_EO_MCP_OUTPUT_DIR` | `~/stac-eo-mcp-output` | Where PNG / GeoTIFF files are written |
|
|
148
|
+
| `STAC_EO_MCP_MAX_AREA_KM2` | `100` | Largest bbox for pixel reads |
|
|
149
|
+
| `STAC_EO_MCP_MAX_SCENES` | `24` | Most dates read by `time_series` (more are sampled evenly) |
|
|
150
|
+
| `STAC_EO_MCP_MAX_SEARCH_LIMIT` | `50` | Most scenes returned by `search_scenes` |
|
|
151
|
+
| `STAC_EO_MCP_LOG_LEVEL` | `INFO` | Log level; logs go to stderr only |
|
|
152
|
+
|
|
153
|
+
## Example conversation
|
|
154
|
+
|
|
155
|
+
> **You:** How did vegetation in Villa Borghese, Rome (bbox 12.47, 41.905, 12.50, 41.925) change
|
|
156
|
+
> during 2025?
|
|
157
|
+
>
|
|
158
|
+
> **Claude** *calls `time_series(index="ndvi", bbox=[12.47, 41.905, 12.5, 41.925], date_range="2025-01-01/2025-12-31")`*
|
|
159
|
+
>
|
|
160
|
+
> **Claude:** I analysed 24 of the 51 Sentinel-2 dates in 2025 (sampled evenly); 22 had enough
|
|
161
|
+
> clear pixels. Mean NDVI stays between 0.28 and 0.39, typical of a park mixed with city blocks:
|
|
162
|
+
>
|
|
163
|
+
> | Date | Mean NDVI |
|
|
164
|
+
> |---|---|
|
|
165
|
+
> | 2025-01-04 | 0.37 |
|
|
166
|
+
> | 2025-05-01 | 0.35 |
|
|
167
|
+
> | 2025-07-03 | **0.28** (minimum) |
|
|
168
|
+
> | 2025-09-28 | 0.35 |
|
|
169
|
+
> | 2025-11-05 | **0.39** (maximum) |
|
|
170
|
+
>
|
|
171
|
+
> The summer dip and autumn recovery is the usual Mediterranean pattern: dry-season stress in
|
|
172
|
+
> July, regrowth after the autumn rains. 10 July and 16 October were skipped (fully cloudy over
|
|
173
|
+
> the park even though the tiles reported under 30% cloud).
|
|
174
|
+
>
|
|
175
|
+
> **You:** Show me the map for the end of July.
|
|
176
|
+
>
|
|
177
|
+
> **Claude** *calls `search_scenes(...)`, then `compute_index(item_id="S2A_MSIL2A_20250730T100051_R122_T33TTG_20250730T153414", index="ndvi", bbox=[...], save="png")`*
|
|
178
|
+
>
|
|
179
|
+
> **Claude:** 30 July is 91% clear, mean NDVI 0.32 (max 0.90 on the park's tree cover, negative on
|
|
180
|
+
> the Tiber). Map saved to `~/stac-eo-mcp-output/S2A_MSIL2A_20250730T…_ndvi_c75d9e23.png`.
|
|
181
|
+
|
|
182
|
+
Other questions that work well:
|
|
183
|
+
|
|
184
|
+
- *"Find Sentinel-2 scenes under 10% cloud over Lake Trasimeno in August 2025 and compute NDWI on the clearest one."*
|
|
185
|
+
- *"Compare NBR before and after a wildfire"*: two `compute_index` calls; dNBR = NBR<sub>pre</sub> − NBR<sub>post</sub>.
|
|
186
|
+
- *"Which collections does Earth Search have?"*
|
|
187
|
+
|
|
188
|
+
## How it works
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
agent ──MCP/stdio──▶ server.py ──▶ analysis.py ──▶ catalog.py (pystac-client: search, signing, band names)
|
|
192
|
+
│
|
|
193
|
+
├──────▶ raster.py (odc-stac window read, PNG/GeoTIFF)
|
|
194
|
+
└──────▶ indices.py (numpy: reflectance, SCL mask, stats)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
- `models.py` validates inputs (WGS84 bbox, ISO dates, limits) and defines compact outputs.
|
|
198
|
+
- `indices.py` is pure numpy and fully unit-tested on synthetic arrays.
|
|
199
|
+
- Blocking I/O runs in worker threads; `time_series` reads up to 4 scenes in parallel.
|
|
200
|
+
- Where two Sentinel-2 tiles overlap, `time_series` keeps one scene per day (best bbox coverage,
|
|
201
|
+
then lowest cloud cover).
|
|
202
|
+
|
|
203
|
+
## Development
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
git clone https://github.com/bbrauzzi/stac-eo-mcp && cd stac-eo-mcp
|
|
207
|
+
uv sync
|
|
208
|
+
uv run pytest # fast tests (no network)
|
|
209
|
+
uv run pytest -m slow # live tests against both catalogs (~1-2 min)
|
|
210
|
+
uv run ruff check . && uv run ruff format --check .
|
|
211
|
+
npx @modelcontextprotocol/inspector -e STAC_EO_MCP_OUTPUT_DIR=/tmp/out uv run stac-eo-mcp # MCP Inspector (pass env with -e)
|
|
212
|
+
uv run python scripts/make_readme_figures.py # regenerate the README figures
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Limitations
|
|
216
|
+
|
|
217
|
+
- Indices are Sentinel-2 L2A only; search and details work on any collection.
|
|
218
|
+
- Bounding boxes crossing the antimeridian are not supported.
|
|
219
|
+
- The mask keeps snow/ice pixels (SCL class 11); exclude snowy periods for vegetation work.
|
|
220
|
+
|
|
221
|
+
## License
|
|
222
|
+
|
|
223
|
+
MIT. Sentinel-2 data: Copernicus Sentinel data, processed by ESA.
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# stac-eo-mcp
|
|
2
|
+
|
|
3
|
+
[](https://github.com/bbrauzzi/stac-eo-mcp/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/stac-eo-mcp/)
|
|
5
|
+
[](https://pypi.org/project/stac-eo-mcp/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
An [MCP](https://modelcontextprotocol.io) server that lets AI agents (Claude Desktop, Claude Code,
|
|
9
|
+
Cursor) **search satellite imagery in STAC catalogs and measure vegetation, water and burn
|
|
10
|
+
indices** on Sentinel-2 scenes, without downloading whole images.
|
|
11
|
+
|
|
12
|
+
Ask *"How green was Villa Borghese in Rome during 2025?"* and the agent finds the scenes, reads only
|
|
13
|
+
the pixels inside your area, masks clouds and answers with numbers, a map and a trend.
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
<img src="https://raw.githubusercontent.com/bbrauzzi/stac-eo-mcp/main/docs/ndvi_map_rome.png" width="48%" alt="NDVI map of Villa Borghese, Rome, 30 July 2025">
|
|
17
|
+
<img src="https://raw.githubusercontent.com/bbrauzzi/stac-eo-mcp/main/docs/ndvi_timeseries_rome.png" width="50%" alt="NDVI time series over Villa Borghese, 2025">
|
|
18
|
+
</p>
|
|
19
|
+
<p align="center"><sub>Produced by <code>compute_index</code> and <code>time_series</code> on real data.
|
|
20
|
+
White pixels are clouds and shadows masked with the Sentinel-2 SCL layer.</sub></p>
|
|
21
|
+
|
|
22
|
+
<p align="center">
|
|
23
|
+
<img src="https://raw.githubusercontent.com/bbrauzzi/stac-eo-mcp/main/docs/demo.gif" width="100%"
|
|
24
|
+
alt="Demo: install with claude mcp add, ask Claude Code about Villa Borghese, get NDVI numbers and a map">
|
|
25
|
+
</p>
|
|
26
|
+
<p align="center"><sub>Install in Claude Code, ask a question, get numbers and a map
|
|
27
|
+
(a real session, replayed faster).</sub></p>
|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
- **Two public catalogs**: [Microsoft Planetary Computer](https://planetarycomputer.microsoft.com)
|
|
32
|
+
(assets signed automatically) and [Element84 Earth Search](https://earth-search.aws.element84.com/v1).
|
|
33
|
+
No account or API key needed.
|
|
34
|
+
- **Windowed reads**: only the bbox is read from Cloud-Optimized GeoTIFFs with
|
|
35
|
+
[odc-stac](https://github.com/opendatacube/odc-stac), at 10 m in the scene's UTM grid.
|
|
36
|
+
- **Correct reflectance**: applies the Sentinel-2 processing baseline 04.00 offset (−1000 DN),
|
|
37
|
+
from asset metadata when present, otherwise from the baseline. Both catalogs give identical
|
|
38
|
+
numbers for the same scene.
|
|
39
|
+
- **Cloud masking with SCL**, reported as `valid_pixel_pct`. Tile cloud cover often differs from
|
|
40
|
+
your area (a "9.7% cloudy" tile can be 100% cloudy over your bbox), so this matters.
|
|
41
|
+
- **LLM-friendly output**: statistics, dates and file paths only; never raster arrays. Footprints
|
|
42
|
+
are simplified. Errors say what went wrong *and what to try next*.
|
|
43
|
+
- **Safe by default**: area and scene-count limits stop accidental large downloads.
|
|
44
|
+
|
|
45
|
+
## Tools
|
|
46
|
+
|
|
47
|
+
| Tool | What it does |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `list_collections(catalog?)` | Collections in a catalog, with one-sentence descriptions |
|
|
50
|
+
| `search_scenes(collection, bbox, date_range, max_cloud_cover?, limit?, catalog?)` | Scenes newest first: id, date, cloud cover, footprint, preview link |
|
|
51
|
+
| `get_scene_details(collection, item_id, catalog?)` | Key metadata and the list of assets |
|
|
52
|
+
| `compute_index(item_id, index, bbox, save?, catalog?)` | NDVI / NDWI / NBR on one scene: min, max, mean, std, % valid pixels; optional PNG / GeoTIFF |
|
|
53
|
+
| `time_series(index, bbox, date_range, max_cloud_cover?, catalog?)` | Mean index per acquisition date, cloudy dates skipped with a reason, plus a trend summary |
|
|
54
|
+
|
|
55
|
+
**Resources**: `stac://indices` (formulas, bands, interpretation, cloud mask classes) and
|
|
56
|
+
`stac://catalogs` (catalogs, defaults and limits).
|
|
57
|
+
**Prompt**: `analyze_vegetation(bbox, months=6)`, a guided vegetation analysis.
|
|
58
|
+
|
|
59
|
+
Conventions: `bbox` is `[west, south, east, north]` in WGS84 degrees; `date_range` is
|
|
60
|
+
`"YYYY-MM-DD/YYYY-MM-DD"`.
|
|
61
|
+
|
|
62
|
+
## Install
|
|
63
|
+
|
|
64
|
+
Requires [uv](https://docs.astral.sh/uv/). The server runs locally over stdio; nothing to deploy.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
uvx stac-eo-mcp # runs the server (it waits for an MCP client on stdin)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Claude Desktop
|
|
71
|
+
|
|
72
|
+
Add to `claude_desktop_config.json` (Settings → Developer → Edit Config):
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"mcpServers": {
|
|
77
|
+
"stac-eo-mcp": {
|
|
78
|
+
"command": "uvx",
|
|
79
|
+
"args": ["stac-eo-mcp"],
|
|
80
|
+
"env": {
|
|
81
|
+
"STAC_EO_MCP_OUTPUT_DIR": "/Users/you/stac-eo-mcp-output"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
If Claude Desktop cannot find `uvx`, use its full path (`which uvx`).
|
|
89
|
+
|
|
90
|
+
### Claude Code
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
claude mcp add stac-eo-mcp -- uvx stac-eo-mcp
|
|
94
|
+
# with options:
|
|
95
|
+
claude mcp add stac-eo-mcp -e STAC_EO_MCP_CATALOG=earth-search -- uvx stac-eo-mcp
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Cursor
|
|
99
|
+
|
|
100
|
+
`.cursor/mcp.json`:
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"mcpServers": {
|
|
105
|
+
"stac-eo-mcp": { "command": "uvx", "args": ["stac-eo-mcp"] }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Configuration
|
|
111
|
+
|
|
112
|
+
All optional, via environment variables:
|
|
113
|
+
|
|
114
|
+
| Variable | Default | Meaning |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| `STAC_EO_MCP_CATALOG` | `planetary-computer` | Default catalog (`planetary-computer` or `earth-search`) |
|
|
117
|
+
| `STAC_EO_MCP_OUTPUT_DIR` | `~/stac-eo-mcp-output` | Where PNG / GeoTIFF files are written |
|
|
118
|
+
| `STAC_EO_MCP_MAX_AREA_KM2` | `100` | Largest bbox for pixel reads |
|
|
119
|
+
| `STAC_EO_MCP_MAX_SCENES` | `24` | Most dates read by `time_series` (more are sampled evenly) |
|
|
120
|
+
| `STAC_EO_MCP_MAX_SEARCH_LIMIT` | `50` | Most scenes returned by `search_scenes` |
|
|
121
|
+
| `STAC_EO_MCP_LOG_LEVEL` | `INFO` | Log level; logs go to stderr only |
|
|
122
|
+
|
|
123
|
+
## Example conversation
|
|
124
|
+
|
|
125
|
+
> **You:** How did vegetation in Villa Borghese, Rome (bbox 12.47, 41.905, 12.50, 41.925) change
|
|
126
|
+
> during 2025?
|
|
127
|
+
>
|
|
128
|
+
> **Claude** *calls `time_series(index="ndvi", bbox=[12.47, 41.905, 12.5, 41.925], date_range="2025-01-01/2025-12-31")`*
|
|
129
|
+
>
|
|
130
|
+
> **Claude:** I analysed 24 of the 51 Sentinel-2 dates in 2025 (sampled evenly); 22 had enough
|
|
131
|
+
> clear pixels. Mean NDVI stays between 0.28 and 0.39, typical of a park mixed with city blocks:
|
|
132
|
+
>
|
|
133
|
+
> | Date | Mean NDVI |
|
|
134
|
+
> |---|---|
|
|
135
|
+
> | 2025-01-04 | 0.37 |
|
|
136
|
+
> | 2025-05-01 | 0.35 |
|
|
137
|
+
> | 2025-07-03 | **0.28** (minimum) |
|
|
138
|
+
> | 2025-09-28 | 0.35 |
|
|
139
|
+
> | 2025-11-05 | **0.39** (maximum) |
|
|
140
|
+
>
|
|
141
|
+
> The summer dip and autumn recovery is the usual Mediterranean pattern: dry-season stress in
|
|
142
|
+
> July, regrowth after the autumn rains. 10 July and 16 October were skipped (fully cloudy over
|
|
143
|
+
> the park even though the tiles reported under 30% cloud).
|
|
144
|
+
>
|
|
145
|
+
> **You:** Show me the map for the end of July.
|
|
146
|
+
>
|
|
147
|
+
> **Claude** *calls `search_scenes(...)`, then `compute_index(item_id="S2A_MSIL2A_20250730T100051_R122_T33TTG_20250730T153414", index="ndvi", bbox=[...], save="png")`*
|
|
148
|
+
>
|
|
149
|
+
> **Claude:** 30 July is 91% clear, mean NDVI 0.32 (max 0.90 on the park's tree cover, negative on
|
|
150
|
+
> the Tiber). Map saved to `~/stac-eo-mcp-output/S2A_MSIL2A_20250730T…_ndvi_c75d9e23.png`.
|
|
151
|
+
|
|
152
|
+
Other questions that work well:
|
|
153
|
+
|
|
154
|
+
- *"Find Sentinel-2 scenes under 10% cloud over Lake Trasimeno in August 2025 and compute NDWI on the clearest one."*
|
|
155
|
+
- *"Compare NBR before and after a wildfire"*: two `compute_index` calls; dNBR = NBR<sub>pre</sub> − NBR<sub>post</sub>.
|
|
156
|
+
- *"Which collections does Earth Search have?"*
|
|
157
|
+
|
|
158
|
+
## How it works
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
agent ──MCP/stdio──▶ server.py ──▶ analysis.py ──▶ catalog.py (pystac-client: search, signing, band names)
|
|
162
|
+
│
|
|
163
|
+
├──────▶ raster.py (odc-stac window read, PNG/GeoTIFF)
|
|
164
|
+
└──────▶ indices.py (numpy: reflectance, SCL mask, stats)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
- `models.py` validates inputs (WGS84 bbox, ISO dates, limits) and defines compact outputs.
|
|
168
|
+
- `indices.py` is pure numpy and fully unit-tested on synthetic arrays.
|
|
169
|
+
- Blocking I/O runs in worker threads; `time_series` reads up to 4 scenes in parallel.
|
|
170
|
+
- Where two Sentinel-2 tiles overlap, `time_series` keeps one scene per day (best bbox coverage,
|
|
171
|
+
then lowest cloud cover).
|
|
172
|
+
|
|
173
|
+
## Development
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
git clone https://github.com/bbrauzzi/stac-eo-mcp && cd stac-eo-mcp
|
|
177
|
+
uv sync
|
|
178
|
+
uv run pytest # fast tests (no network)
|
|
179
|
+
uv run pytest -m slow # live tests against both catalogs (~1-2 min)
|
|
180
|
+
uv run ruff check . && uv run ruff format --check .
|
|
181
|
+
npx @modelcontextprotocol/inspector -e STAC_EO_MCP_OUTPUT_DIR=/tmp/out uv run stac-eo-mcp # MCP Inspector (pass env with -e)
|
|
182
|
+
uv run python scripts/make_readme_figures.py # regenerate the README figures
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Limitations
|
|
186
|
+
|
|
187
|
+
- Indices are Sentinel-2 L2A only; search and details work on any collection.
|
|
188
|
+
- Bounding boxes crossing the antimeridian are not supported.
|
|
189
|
+
- The mask keeps snow/ice pixels (SCL class 11); exclude snowy periods for vegetation work.
|
|
190
|
+
|
|
191
|
+
## License
|
|
192
|
+
|
|
193
|
+
MIT. Sentinel-2 data: Copernicus Sentinel data, processed by ESA.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "stac-eo-mcp"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "MCP server that lets AI agents search STAC catalogs and compute Sentinel-2 spectral indices (NDVI, NDWI, NBR)."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [{ name = "bbrauzzi", email = "brauzzi@gmail.com" }]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
keywords = ["mcp", "stac", "earth-observation", "sentinel-2", "ndvi", "remote-sensing", "llm", "agents"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"Intended Audience :: Science/Research",
|
|
14
|
+
"Operating System :: OS Independent",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Programming Language :: Python :: 3.13",
|
|
18
|
+
"Topic :: Scientific/Engineering :: GIS",
|
|
19
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"mcp>=2.3,<3",
|
|
23
|
+
"pydantic>=2.7",
|
|
24
|
+
"pydantic-settings>=2.4",
|
|
25
|
+
"pystac-client>=0.9",
|
|
26
|
+
"planetary-computer>=1.0",
|
|
27
|
+
"odc-stac>=0.5",
|
|
28
|
+
"rioxarray>=0.17",
|
|
29
|
+
"matplotlib>=3.9",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/bbrauzzi/stac-eo-mcp"
|
|
34
|
+
Repository = "https://github.com/bbrauzzi/stac-eo-mcp"
|
|
35
|
+
Issues = "https://github.com/bbrauzzi/stac-eo-mcp/issues"
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
stac-eo-mcp = "stac_eo_mcp.server:main"
|
|
39
|
+
|
|
40
|
+
[dependency-groups]
|
|
41
|
+
dev = [
|
|
42
|
+
"pytest>=9.0",
|
|
43
|
+
"ruff>=0.17",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[build-system]
|
|
47
|
+
requires = ["hatchling>=1.27"]
|
|
48
|
+
build-backend = "hatchling.build"
|
|
49
|
+
|
|
50
|
+
[tool.hatch.build.targets.wheel]
|
|
51
|
+
packages = ["src/stac_eo_mcp"]
|
|
52
|
+
|
|
53
|
+
[tool.hatch.build.targets.sdist]
|
|
54
|
+
include = ["src", "tests", "README.md", "LICENSE"]
|
|
55
|
+
|
|
56
|
+
[tool.ruff]
|
|
57
|
+
line-length = 100
|
|
58
|
+
target-version = "py312"
|
|
59
|
+
|
|
60
|
+
[tool.ruff.lint]
|
|
61
|
+
select = ["E", "F", "W", "I", "B", "UP", "SIM", "RUF", "N", "PT"]
|
|
62
|
+
|
|
63
|
+
[tool.pytest.ini_options]
|
|
64
|
+
testpaths = ["tests"]
|
|
65
|
+
addopts = "-ra --strict-markers -m 'not slow'"
|
|
66
|
+
filterwarnings = [
|
|
67
|
+
# Third-party deprecation inside planetary-computer, not our code.
|
|
68
|
+
"ignore:Could not parse bucket/account from href:UserWarning",
|
|
69
|
+
"ignore:Use `@` matmul instead of `\\*` mul operator:PendingDeprecationWarning",
|
|
70
|
+
"ignore:The 'shapely.ops.transform\\(\\)' function is deprecated:DeprecationWarning",
|
|
71
|
+
"ignore:Support for class-based `config` is deprecated:DeprecationWarning:planetary_computer",
|
|
72
|
+
]
|
|
73
|
+
markers = [
|
|
74
|
+
"slow: hits real STAC catalogs over the network (deselect with -m 'not slow')",
|
|
75
|
+
]
|