mcp-geoes
# IGN-IGME-MCP
**Ask an AI agent about Spanish geology and get an answer with a map.**
This is an [MCP](https://modelcontextprotocol.io/) server that puts the public
cartography of Spain — the **IGME**'s geology and the **IGN**'s base mapping and
orthophotos — behind a single set of tools an agent can use, and composes them
into one image when a question needs both.
> One question, two publishers, one answer:
>
> *"¿Hay fallas cuaternarias cerca de este punto? Enséñame un mapa sobre la
> ortofoto."*
>
> The faults come from the IGME over ArcGIS REST, the orthophoto from the IGN
> over OGC WMS, and they arrive as a single aligned image with a legend saying
> which is which.
Read-only, always. Every operation is a GET; there is no editing path anywhere
in the codebase.
---
## Why it exists
Spain publishes an enormous amount of high-quality geospatial data for free —
and almost none of it is reachable from an AI assistant. Three problems stand in
the way, and this project is the answer to each:
**The data lives behind two incompatible protocol families.** The IGME runs
ArcGIS Server; the IGN publishes OGC WMS and WMTS. An agent should not have to
know, so the tools are the same for both — and a source that cannot do something
says so and names the alternative, instead of returning an empty result that
reads like "there is nothing there".
**Real questions cross publishers.** *"Are there active faults nearby, and what
does the ground look like?"* is one question and two organisations. Splitting it
across two servers pushes the join into the agent, which is where scale
confusion and misattribution creep in.
**A map without provenance is a liability.** A geological contact read from a
1:1,000,000 synthesis and one from a 1:50,000 sheet are not interchangeable, and
an agent that cannot tell them apart will use the first to answer about a
parcel. Every answer here carries its service, publisher and nominal scale.
New to WMS, WMTS or ArcGIS REST? → **[`docs/theory.md`](docs/theory.md)**
---
## What it can do
| | |
|---|---|
| **Find a place** | turn "Suances" into a map envelope — a municipality's real boundary, not a guessed square |
| **Say what exists** | enumerate every source, from both publishers, before assuming one |
| **Answer at a point** | which geological unit, which lithology, which flight date — with the scale it was read at |
| **Query by attributes** | SQL-style filtering with transparent paging, where the source supports it |
| **Draw maps** | one source framed on a place, or several stacked and aligned to the pixel with a combined legend |
| **Describe itself** | what each source publishes, what it can do, and what its fields mean |
| **Refuse usefully** | when a source cannot answer, it says why and what can |
See it in use → **[`docs/examples.md`](docs/examples.md)**
---
## Quick start
Requires Python ≥ 3.11 and [uv](https://docs.astral.sh/uv/).
```bash
git clone <this repository> && cd IGN-IGME-MCP
uv sync --all-extras
uv run pytest # the whole suite, offline
uv run mcp-geoes --list-profiles
uv run mcp-geoes --profile general --validate
```
Then connect a client. The configuration files ship with the repository, with
all five profiles already declared — open it and start the server you want:
| Client | File |
|---|---|
| VS Code | [`.vscode/mcp.json`](.vscode/mcp.json) |
| Claude Code | [`.mcp.json`](.mcp.json) and [`.claude/`](.claude) |
| Claude Desktop | [`examples/clients/claude_desktop_config.json`](examples/clients/claude_desktop_config.json) |
Anything else that speaks MCP runs the same command:
```bash
uv run mcp-geoes --profile geologia
```
Wiring, hosted transport and multi-profile caveats →
**[`examples/mcp_clients.md`](examples/mcp_clients.md)**
Then ask it *"¿qué cartografía tienes de Suances?"* or *"¿qué unidad geológica
hay ahí?"*.
**Five profiles, one process each** — the tool list an agent sees is what keeps
its choices accurate. The split is by **domain, never by publisher**: real
questions cross the IGME and the IGN in the same sentence.
| Profile | For | Tools |
|---|---|---|
| `general` | "what do you have for this place?" — both publishers, short answer | 19 |
| `cartografia_ign` | the IGN as subject: orthophoto, MTN sheets, historical series, relief | 25 |
| `geologia` | the IGME's geology, with the IGN's cartography to read it against | 27 |
| `riesgos` | active faults, seismicity, ground movements, neotectonics | 21 |
| `agua` | hydrogeology and permeability over the IGN's hydrography | 21 |
Counts include the thirteen generic tools, which always register.
What each profile declares, which one to pick, and how to write another →
**[`docs/profiles.md`](docs/profiles.md)**
Prefer to see it work before wiring up a client?
```bash
uv run python scripts/demo_tools.py --out working/ # the tools, live
uv run python scripts/demo_compose.py --out working/ # a composed map
uv run python scripts/demo_discovery.py --place Suances --out working/
```
Full instructions, container and hosting → **[`docs/deployment.md`](docs/deployment.md)**
---
## How it fits together
```
AI agent ──MCP──▶ mcp_geoes ──▶ domain profile (YAML, Spanish)
│
├──▶ arcgis_client ──▶ IGME (ArcGIS REST)
├──▶ ogc_client ──▶ IGN (WMS · WMTS)
└──▶ map_compose ──▶ one image, one legend
```
Three ideas carry the design:
**Capabilities are declared, never assumed.** Each source states what it can
answer. Asking for anything else raises an error naming the alternative — never
a plausible empty result.
**Questions start from places, not coordinates.** `resolve_place` turns a name
into an envelope, and every map tool takes `place=` instead of `bbox=`.
**Composition happens in EPSG:3857.** Layers from different publishers can only
be stacked if all were rendered for the same envelope in the same projection.
Tool signatures still speak lon/lat; the conversion is done for you.
**Profiles add knowledge, never code.** A YAML file names its sources across
both publishers, their semantics, its closed vocabularies and its convenience
tools. A new domain — hydrogeology, geological risk, mining — is a new file.
Why each of those, and what was rejected → **[`docs/architecture.md`](docs/architecture.md)**
· Requirements and their tests → **[`docs/specs.md`](docs/specs.md)**
---
## Documentation
| Document | For |
|---|---|
| [`docs/theory.md`](docs/theory.md) | the GIS concepts, if you are new to them, with pointers to the standards |
| [`docs/licensing.md`](docs/licensing.md) | **what you must write when you publish** — the IGN's and IGME's attribution terms |
| [`docs/profiles.md`](docs/profiles.md) | what a profile declares, the five shipped ones, and how to add one |
| [`docs/examples.md`](docs/examples.md) | what you can ask, what comes back, and what it refuses |
| [`docs/architecture.md`](docs/architecture.md) | the design decisions and why they were taken |
| [`docs/specs.md`](docs/specs.md) | numbered requirements, acceptance criteria, milestones |
| [`docs/deployment.md`](docs/deployment.md) | build, test, run, containerise, host, and keep it current |
| [`examples/mcp_clients.md`](examples/mcp_clients.md) | client configuration, copy-paste |
Each package also has its own README: [`geo_core`](packages/geo_core/),
[`geo_http`](packages/geo_http/), [`arcgis_client`](packages/arcgis_client/),
[`ogc_client`](packages/ogc_client/), [`map_compose`](packages/map_compose/),
[`mcp_geoes`](packages/mcp_geoes/).
---
## Layout
```
IGN-IGME-MCP/
├── packages/
│ ├── geo_core/ # backend contract, model, geometry, logging
│ ├── geo_http/ # shared transport: retries, cache, timing
│ ├── arcgis_client/ # ArcGIS Server REST → IGME
│ ├── ogc_client/ # OGC WMS, WMTS and WFS → IGN, regional SDIs
│ ├── map_compose/ # composition engine and combined legend
│ └── mcp_geoes/ # the MCP server and the profile loader
├── profiles/ # domain profiles (YAML + prompt), in Spanish
├── docs/ # theory · profiles · examples · architecture · specs · deployment
├── scripts/ # demos, live validation, catalog audit
├── docker/ # container healthcheck
└── tests/
```
---
## Troubleshooting
Three log levels on one axis, because troubleshooting a chain of
agent → server → two origins means asking three questions in order:
| Level | Answers | Emits |
|---|---|---|
| `INFO` | what did the agent ask for, and did it work? | one line per tool call: tool, backend, duration, status |
| `DEBUG` | what did we ask the origins? | one line per upstream request: URL, status, bytes, cache, duration |
| `TRACE` | what exactly went over the wire? | full URL, every parameter, truncated payload |
Every record carries a `call_id` that propagates from the tool call down to the
HTTP request, so one `grep` reconstructs a whole interaction. Pass the agent's
own trace id as `trace_id` and the two timelines join.
```bash
uv run mcp-geoes --profile geologia --log-level TRACE --log-format json
```
Logs go to **stderr**: under the stdio transport, stdout carries the MCP
protocol itself.
---
## Language
The code, its comments and its documentation are in **English**, because layers
0–2 are reusable well beyond Spain. The **domain profiles are in Spanish** —
tool names, field semantics, prompts, vocabularies — because the data is Spanish
(`UNIDAD`, `LITOLOGIA`, "Triásico"), users ask in Spanish, and a translation step
between question, tool and data is exactly where models introduce mapping
errors.
---
## Licence and attribution
The **software** is [MIT](LICENSE).
The **data is not**. It belongs to the **Instituto Geológico y Minero de España
(IGME, CSIC)** and the **Instituto Geográfico Nacional (IGN)**, who publish it
freely on the condition that you cite the source. That condition travels with
the data: when you put one of these maps in a report, a site or a product, the
obligation to attribute is yours.
This server gives you everything you need to meet it — publisher, nominal scale,
edition date and the official product page ride along with every answer — but it
cannot meet it for you, because it does not know where your result ends up.
The IGN requires the literal form `PNOA 2023 CC-BY 4.0 scne.es`, prefixed with
"Obra derivada de" if you modified anything — and composing counts. The IGME
requires `Origen de los datos: © Instituto Geológico y Minero de España (IGME)`.
**Read [`docs/licensing.md`](docs/licensing.md) before you publish anything.**
It has both formulas, what counts as a derived work, and a checklist.
This project is not affiliated with, endorsed by, or maintained by either
institute.
TDQS
Scored across 8 tools
Each tool targets a distinct operation: listing services, describing services or layers, getting legends, querying attributes, identifying by point, and rendering maps. The overlap between query_layer and identify_at_point is minimal and well-differentiated by interaction style.
All tool names follow a clear verb_noun snake_case pattern (list_services, describe_layer, get_legend, export_map). Even identify_at_point fits the pattern with a prepositional phrase, and no mixed conventions appear.
Eight tools is well-scoped for a geospatial service client, covering discovery, metadata, query, legend retrieval, and map rendering without redundancy or bloat.
The tool surface covers the full geospatial workflow: catalog browsing, metadata inspection, query, point identification, legend access, single-source export, and multi-source composition. No obvious dead ends or missing core operations.