geo-osiris-mcp
# geo-osiris-mcp
Thin, agent-facing MCP adapter over the OSIRIS Geo Hub admin API
(`/api/v1/admin/*`). Geo Hub remains the only business-logic authority:
validation, duplicate detection, provenance, freshness, and publication rules
all stay there. This server only validates agent input, translates it into Geo
Hub requests, and returns compact results.
**Phase 6D scope:** one image supports either local stdio or authenticated
Streamable HTTP. Read mode exposes 14 read tools; staging adds 8 controlled
staging tools, including `request_import_publication`. Neither mode exposes
approve, publish, commit, archive, delete, cancel, or source sync.
## Requirements
- Python 3.12
- Network access to a Geo Hub instance reachable from this process
## Configuration
| Variable | Required | Default | Purpose |
| --- | --- | --- | --- |
| `GEO_API_URL` | yes | — | Geo Hub base URL |
| `GEO_READ_TOKEN` | read mode | — | Geo API `geo.read` bearer; server-side only |
| `GEO_STAGE_TOKEN` | staging mode | — | Geo API `geo.read` + `geo.stage` bearer; server-side only |
| `GEO_HUB_MCP_MODE` | no | `read` | `read` or `staging`; anything else aborts startup |
| `GEO_MCP_TRANSPORT` | no | `stdio` | `stdio` or authenticated Streamable HTTP (`http`) |
| `GEO_MCP_HOST`, `GEO_MCP_PORT` | HTTP only | — | Listener bind address and port |
| `GEO_MCP_PATH` | no | `/mcp` | Streamable HTTP path |
| `GEO_MCP_ALLOWED_HOSTS` | HTTP only | — | Exact allowed Host headers, comma-separated |
| `GEO_MCP_CLIENT_TOKENS_FILE` | HTTP only | — | Read-only JSON file containing client-token SHA-256 hashes |
See `.env.example`. Unknown or empty `GEO_HUB_MCP_MODE` fails closed (exit 2).
## Install and run
```sh
python3.12 -m venv .venv
. .venv/bin/activate
pip install -e ".[dev]"
GEO_API_URL=http://localhost:8000 GEO_READ_TOKEN=... geo-osiris-mcp
```
`stdio` remains the default for local development and the existing Docker-exec
workflow. HTTP mode requires an exact host allowlist and hashed client
credentials. Generate a client token outside the container, give its plaintext
only to one Hermes host, and put only its SHA-256 in `deploy/client-tokens.json`:
```sh
python -c 'import hashlib,secrets;t=secrets.token_urlsafe(32);print(t);print(hashlib.sha256(t.encode()).hexdigest())'
```
`deploy/client-tokens.json` is gitignored. Hermes-Zima and Hermes-Mac must use
separate credentials; do not place them in profile text, source control, or
container environment variables.
## Tools
**Read (14 tools, both modes):** `list_layers`, `get_layer`, `list_features`,
`get_feature`, `list_imports`, `get_import`, `list_import_rows`, `list_sources`,
`get_source`, `inspect_layer`, `summarize_import`, `infer_csv_mapping`,
`compare_dataset_to_layer`, `health`.
**Staging (8 tools, `GEO_HUB_MCP_MODE=staging` only):** `create_managed_layer`,
`update_layer`, `create_feature`, `patch_feature`, `stage_csv_import`,
`stage_geojson_import`, `resolve_import_row`, `request_import_publication`.
All collection tools return `{items, next_cursor, has_more}` with opaque
cursors. `summarize_import.commit_ready` is informational only; there is no
commit tool. Staging tools force `mode=managed` on layer creation, reject
`published`/`archived` feature statuses before any HTTP request, and never
auto-resolve duplicate candidates. Import staging accepts inline content only
(bounded to the Geo Hub limits); Geo Hub owns parsing, conversion, validation,
and candidate generation.
### Orchestration
`infer_csv_mapping` is deterministic and local: it inspects headers and sample
rows, matches latitude/longitude/external_id aliases, and suggests a mapping
with per-field confidence and notes. It never calls a model and never stages
anything. Ambiguous or missing coordinates return `null` with an explanation.
`compare_dataset_to_layer` is conservative:
- `stage=false` (advisory, **zero writes**) inspects the layer and dataset
shape and returns `authoritative: false` with advisory labels only
(`advisory_existing_external_id`, `advisory_identity_match`). It does not
claim duplicates or validity — Geo Hub has not evaluated the dataset.
- `stage=true` (staging mode only) creates **exactly one** staging import and
returns `authoritative: true` with a bounded candidate preview (max 10 rows).
It never resolves candidates, commits, cancels, or publishes.
`summarize_import` is the single authoritative implementation of
`commit_ready`; orchestration reuses it rather than reimplementing readiness.
## Docker deployment
`deploy/compose.yml` retains the idle `geo-osiris-mcp` stdio container and adds
two server-fixed HTTP services for staging validation:
- `geo-osiris-mcp-read`: read mode, `GEO_READ_TOKEN` only,
`127.0.0.1:8765/mcp`.
- `geo-osiris-mcp-stage`: staging mode, `GEO_STAGE_TOKEN` only,
`127.0.0.1:8766/mcp`.
Both HTTP services read the same hash-only `deploy/client-tokens.json` file.
Clients cannot choose the mode, and neither service receives approval,
publication, admin, database, Docker-socket, or host-filesystem credentials.
The stdio container is a trusted local-development compatibility shim and holds
both read and stage backend credentials so its separately launched process can
select the matching credential. Do not deploy that shim in production.
```sh
docker compose -f deploy/compose.yml --env-file <env-file> up -d --build
```
The staging services use `GEO_API_URL=http://geo-api:8000` on the external
`osiris-staging_osiris-staging` Docker network. Production must use immutable
GHCR digests, bind ports only to the private LAN/VPN interface, and place host
firewall rules in front of those ports. Do not use Nginx Proxy Manager for MCP.
## Hermes integration
Stdio remains available for local development:
```yaml
mcp_servers:
geo-osiris:
command: docker
args:
- exec
- -i
- -e
- GEO_HUB_MCP_MODE=read # staging profile uses =staging
- geo-osiris-mcp
- python
- -m
- geo_osiris_mcp.server
enabled: true
```
For private-network HTTP, configure separate profile-scoped bearer secrets and
remote MCP endpoints, for example default → `http://<private-host>:8765/mcp`
and geo-curator → `http://<private-host>:8766/mcp`. The `Authorization: Bearer`
value is the client MCP token, never `GEO_READ_TOKEN` or `GEO_STAGE_TOKEN`.
## Secrets
`GEO_READ_TOKEN` and `GEO_STAGE_TOKEN` are injected server-side into their
respective containers. The agent must not read, print, log, echo, or commit
them. Do not put backend credentials in Hermes config, the README, or chat.
## Staging acceptance example
Against local staging (`amateur-radio-repeaters-es`: 2 features, managed,
duplicate detection `callsign` + 300 m):
1. Read profile: `inspect_layer amateur-radio-repeaters-es` → `feature_count 2`,
`mode managed`, `identity_properties ["callsign"]`, `coordinate_radius_m 300`.
2. Staging profile: `infer_csv_mapping` → `create_managed_layer` →
`stage_csv_import` (2-row CSV) → `summarize_import` → `total 2, valid 2,
invalid 0, commit_ready true`; the import stays `validated` and no feature is
published.
3. Publication boundary: asking to commit/publish must fail — the tool does not
exist.
## Tests and lint
```sh
ruff check .
ruff format --check .
pytest
```
TDQS
Scored across 14 tools
Most tools are clearly separated by resource and action (list_*, get_*, infer_*, compare_*). A few operational summaries like inspect_layer and summarize_import overlap somewhat with their get_* counterparts, but the descriptions provide enough distinction to avoid serious misselection.
Tool names overwhelmingly follow a consistent verb_noun snake_case pattern such as list_layers, get_feature, and list_import_rows. The only minor deviation is health, which is a single noun rather than a verb_noun form, but it is a conventional health-check name and does not create confusion.
Fourteen tools is well within the ideal range for a domain-specific server. Each tool covers a distinct resource or operation in the Geo Hub/import workflow, and none feel redundant or excessive.
The tool surface is strong for read-only inspection, listing, CSV inference, and advisory comparison. However, there are notable gaps around the import lifecycle: no tool commits, publishes, or archives an import, and there is no create/update/delete coverage for layers or sources, leaving the workflow incomplete for agents that need to act rather than only inspect.