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.
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .coverage
10
+ stac-eo-mcp-output/
11
+ .playwright-mcp/
@@ -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
+ [![CI](https://github.com/bbrauzzi/stac-eo-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/bbrauzzi/stac-eo-mcp/actions/workflows/ci.yml)
34
+ [![PyPI](https://img.shields.io/pypi/v/stac-eo-mcp)](https://pypi.org/project/stac-eo-mcp/)
35
+ [![Python](https://img.shields.io/pypi/pyversions/stac-eo-mcp)](https://pypi.org/project/stac-eo-mcp/)
36
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](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
+ [![CI](https://github.com/bbrauzzi/stac-eo-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/bbrauzzi/stac-eo-mcp/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/stac-eo-mcp)](https://pypi.org/project/stac-eo-mcp/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/stac-eo-mcp)](https://pypi.org/project/stac-eo-mcp/)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](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
+ ]
@@ -0,0 +1,3 @@
1
+ """MCP server for searching STAC catalogs and analysing Earth observation data."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from stac_eo_mcp.server import main
2
+
3
+ if __name__ == "__main__":
4
+ main()