Skip to content

Repository files navigation

QGIS MCP Bridge for Codex

Field monitoring MVP

This repository also contains a local-first, deterministic Sentinel-2 field-monitoring vertical slice. It preserves the QGIS bridge split: the QGIS 3.20.1 plugin remains Python 3.9.5 and vector-focused; all raster work runs in the Python 3.11+ process.

The one-command demo imports every identifiable field from C:\Users\user\Desktop\Агро\Shape QGIS Migration\Forward 2023.shp when available. It never edits that source. The reviewed source currently contains 51 polygons: 45 have a field code and are imported, while six unlabeled overlapping/reference geometries are excluded with an audit report. If the source is absent, the demo creates one clearly labelled synthetic polygon. Satellite arrays are deterministic offline fixtures and are always displayed as OFFLINE MOCK — НЕ LIVE.

powershell -ExecutionPolicy Bypass -File .\scripts\run_monitoring_demo.ps1

Open http://127.0.0.1:8765/monitoring.

The dashboard intentionally has no external basemap or analytics: viewing a field does not send its map extent to a public tile provider.

The demo executes:

field copy → CRS/geometry validation → stable field_id → scene discovery
→ SCL mask → NDVI/NDRE/NDMI/NDWI/EVI/SAVI → field crop
→ COG/GeoTIFF → zonal statistics → explainable alerts → dashboard/export

Run the worker once without the web server:

powershell -ExecutionPolicy Bypass -File .\scripts\run_monitor_worker.ps1 -Provider mock -StartDate 2026-06-01 -EndDate 2026-06-30 -MaxCloudCover 80

Demo outputs are under runtime\monitoring-demo by default (operational commands use QGIS_MONITOR_RUNTIME):

  • monitoring.sqlite3: farms, fields, scenes, jobs, statistics, alerts and audit log;
  • imports\: SHA-256-preserved input and normalized GeoJSON copy;
  • cogs\<field_id>\<scene_id>\<algorithm_version>\: RGB and index GeoTIFF/COG assets;
  • fixtures\: local demo derivative, never the canonical farm file.

Live CDSE mode

The currently running credentialless live mode uses official CDSE STAC for scene discovery, requires an exact product-URI match in Element 84 Earth Search, and window-reads only the farm AOI from the public Sentinel-2 COG bucket. It does not require CDSE credentials:

powershell -ExecutionPolicy Bypass -File .\scripts\run_monitoring_live.ps1

The command prepares a fresh runtime\monitoring-forward-live by default and idempotently imports the complete reviewed Forward field set before starting the scheduler. To prepare/import only:

powershell -ExecutionPolicy Bypass -File .\scripts\import_forward_farm.ps1

Open http://127.0.0.1:8765/monitoring. This mode is named cdse_cog; the UI and provenance identify both the authoritative CDSE item and the public COG asset. Earth Search is a free public catalogue without a production SLA, so the exact-product check fails closed rather than substituting a different acquisition.

The CDSE adapter performs official STAC catalogue discovery with AOI, date and cloud filters, then retrieves only the AOI pixels through the authenticated Sentinel Hub Process API. Spectral bands and SCL are requested separately so bilinear and nearest-neighbour resampling cannot be mixed. OAuth tokens are cached in memory and secrets are read only from environment variables:

$env:QGIS_MONITOR_PROVIDER = "cdse"
$env:QGIS_MONITOR_RUNTIME = "C:\secure\agro-monitor-runtime"
$env:CDSE_SH_CLIENT_ID = "<OAuth client id>"
$env:CDSE_SH_CLIENT_SECRET = "<secret from a secret store>"
.\.venv\Scripts\python.exe -m codex_qgis_mcp.preview_server --with-scheduler

Create a machine-to-machine OAuth client in the CDSE Sentinel Hub dashboard. Without both credentials the job fails explicitly after catalogue discovery and no raster is marked processed. SciHub is not used. Planet is not enabled because the free trial is not licensed for this internal commercial case. See the dated source matrix.

Web hosting

Native localhost host (currently usable even while the Docker/WSL reboot is pending):

powershell -ExecutionPolicy Bypass -File .\scripts\run_monitoring_live.ps1

Local/private host (the port is bound only to 127.0.0.1):

Copy-Item .env.example .env
# Edit .env: set a long random web password, provider and CDSE OAuth credentials.
docker compose up --build -d

Public HTTPS host requires a server with Docker, a DNS name pointed to it, inbound TCP 80/443, and non-placeholder values in .env. Caddy obtains/renews TLS certificates; the application still enforces Basic Auth:

docker compose -f compose.yaml -f compose.public.yaml up --build -d
docker compose -f compose.yaml -f compose.public.yaml ps

Open https://<QGIS_MONITOR_DOMAIN>/monitoring. Do not expose port 8765 directly to the Internet. The persistent monitoring-data volume contains SQLite, imports and COGs; back it up before upgrades.

Docker Desktop 4.80.0 and WSL 2.7.10.0 are installed on this workstation. The Docker engine cannot start until AMD-V/SVM is enabled in UEFI and Windows is rebooted after the WSL/Virtual Machine Platform installation. Until then, use the native localhost command above. Docker Desktop licensing is free only for personal use, education, non-commercial open source, or qualifying small businesses; verify the organization category before commercial deployment.

Field imports

Use the dashboard or MCP tool monitoring_import_fields. Supported formats are ZIP Shapefile, GeoJSON and GeoPackage. ZIP imports require same-stem .shp, .shx, .dbf and .prj; .cpg absence is warned. Archive traversal, symlinks and expanded size are bounded. CRS is mandatory, geometries are validated/repaired into a separate WGS84 copy, and duplicates are identified by normalized geometry SHA-256. field_id is UUIDv5 over farm identity plus normalized geometry, so renaming a field does not change its identity.

The Forward-specific preparation command additionally hashes all canonical Shapefile components, resolves names from Field_Name with a strict field-code fallback in Culture23, records every excluded source index, and then calls the same generic importer. Current acceptance values are 45 fields, six excluded unlabeled geometries and two repaired identified geometries. See the full-farm specification.

Quality and formulas

The target grid is 10 m. 20 m spectral bands use bilinear resampling; SCL uses nearest-neighbour. SCL classes 0, 1, 3, 7, 8, 9, 10 and 11 are invalid. NoData is NaN internally and -9999 in index GeoTIFFs. Spectral scale/offset comes from each STAC asset; negative reflectance is clamped to zero to match Sentinel Hub harmonizeValues=true. This can saturate normalized indices for atmospherically over-corrected pixels, so EVI/SAVI and the source RGB should be reviewed alongside NDVI. Each observation also has a masked true-colour RGB COG for the web map. Formulas and complete provenance are documented in the monitoring specification.

Alerts use a configurable absolute deviation from the historical median:

$env:QGIS_MONITOR_ALERT_THRESHOLD = "0.05"
$env:QGIS_MONITOR_ALERT_MIN_HISTORY = "2"

Every alert states its rule, observed value, baseline, deviation, threshold and scene URL. It is a review signal, not a diagnosis.

Hermes MCP and cron

Hermes Agent is optional. Put secrets in ~/.hermes/.env or its OAuth store, never in prompts or this repository. Add a least-privilege stdio server entry to ~/.hermes/config.yaml:

mcp_servers:
  agro_monitor:
    command: "C:\\Users\\user\\Desktop\\MCP for QGIS\\.venv\\Scripts\\python.exe"
    args: ["-m", "codex_qgis_mcp.server"]
    env:
      QGIS_MONITOR_PROVIDER: "cdse_cog"
      QGIS_MONITOR_RUNTIME: "C:\\Users\\user\\Desktop\\MCP for QGIS\\runtime\\monitoring-forward-live"
    tools:
      include: [monitoring_status, monitoring_run_job, monitoring_job_status, monitoring_report, monitoring_alerts, monitoring_qgis_qa_manifest]
      resources: false
      prompts: false

Do not expose monitoring_import_fields to unattended cron. A self-contained cron prompt for a reviewed CDSE configuration is:

hermes cron create "0 */6 * * *" "Use only the allowlisted agro_monitor MCP tools. Call monitoring_run_job with provider='cdse_cog', start_date equal to seven calendar days before today, end_date equal to today, and max_cloud_cover=70. Then call monitoring_status and monitoring_alerts. State the newest acquisition timestamp, provider, job state, scene count, and any skip/failure reason. For each alert include field, index, observed value, historical baseline, threshold explanation, and source scene URL. Explicitly say that indices are review signals, not agronomic diagnoses. If there are no new scenes, no failed job, and no alerts, respond only [SILENT]. Never request, print, or infer credentials; never call QGIS mutation tools." --name "agro-sentinel-review" --workdir "C:\Users\user\Desktop\MCP for QGIS" --deliver local
hermes gateway
hermes cron status

Hermes cron runs in fresh sessions, so the prompt is deliberately self-contained. The EO pipeline and alert calculation do not depend on an LLM.

Backup and restore

Stop the preview/worker before a filesystem backup so SQLite and COGs are captured consistently:

powershell -ExecutionPolicy Bypass -File .\scripts\backup_monitoring.ps1
powershell -ExecutionPolicy Bypass -File .\scripts\restore_monitoring.ps1 -Backup ".\runtime\backups\monitoring-YYYYMMDD-HHMMSS.zip"

Restore expands to a new dated directory and never overwrites the active runtime. Point QGIS_MONITOR_RUNTIME at the restored monitoring directory, run /api/monitor/health, compare counts, then switch operational configuration. QGIS layer-mutation backups remain separate under the plugin's runtime\backups\*.gpkg.

Health, tests and operational limits

Invoke-RestMethod http://127.0.0.1:8765/api/monitor/health
.\.venv\Scripts\python.exe -m unittest discover -s tests\server -p "test_*.py"
& 'C:\Program Files\QGIS 3.20.1\bin\python-qgis.bat' -m unittest discover -s tests\qgis_plugin -p "test_*.py"

Current limits:

  • local SQLite is single-host/single-worker MVP storage; move to PostGIS before concurrent workers or multi-user writes;
  • local COGs require capacity monitoring and tested backups;
  • mock AOIs are capped at 512×512 pixels at true 10 m and fail rather than silently resample;
  • CDSE STAC discovery is limited to the first 100 matching items per request in this MVP;
  • cdse_cog requires no credentials but depends on exact Earth Search product coverage and its public-service availability;
  • direct cdse Process API mode requires a registered Sentinel Hub OAuth client and consumes processing units;
  • CDSE quota, product latency, cloud cover and SCL errors can create observation gaps;
  • no “real-time” claim is made: the UI shows acquisition time and the reason a scene was skipped;
  • QGIS 3.44 docs are conceptual; plugin behavior is verified against installed QGIS 3.20.1.

Architecture, security and external-resource decisions are in the complete ASCII architecture, ADR-0002, the threat model and the dated audit.

This project connects Codex to a live QGIS 3.20.1 desktop session through a two-part bridge:

  • qgis_mcp_bridge/: a QGIS desktop plugin running inside QGIS on the bundled Python 3.9.5
  • codex_qgis_mcp/: a separate MCP server and local web preview server running on system Python 3.11+

The split is intentional. The current Python MCP SDK requires Python >= 3.10, while QGIS 3.20.1 ships with Python 3.9.5.

What v1 supports

  • Live QGIS session only
  • Any loaded editable vector layer
  • Scope targeting by all features, selected features, explicit feature ids, or QGIS expression
  • Auto-backup to GeoPackage before mutating operations
  • Area field calculation
  • Attribute update / rename workflows
  • Feature deletion

Setup

1. Install the QGIS plugin

powershell -ExecutionPolicy Bypass -File .\scripts\install_qgis_plugin.ps1

This copies [qgis_mcp_bridge](C:\Users\user\Desktop\MCP for QGIS\qgis_mcp_bridge) into:

%APPDATA%\QGIS\QGIS3\profiles\default\python\plugins\qgis_mcp_bridge

2. Create the server environment

powershell -ExecutionPolicy Bypass -File .\scripts\setup_server_venv.ps1

3. Start QGIS and load the plugin

Open QGIS 3.20.1, enable QGIS MCP Bridge in Plugin Manager, and keep that QGIS session open.

When the plugin starts, it writes the live bridge connection file to:

%APPDATA%\QGIS\QGIS3\profiles\default\python\plugins\qgis_mcp_bridge\runtime\connection.json

4. Run the MCP server for Codex

powershell -ExecutionPolicy Bypass -File .\scripts\run_mcp_server.ps1

Equivalent direct launch:

.\.venv\Scripts\python.exe -m codex_qgis_mcp.server

5. Run the local web preview

powershell -ExecutionPolicy Bypass -File .\scripts\run_web_preview.ps1

Equivalent direct launch:

.\.venv\Scripts\python.exe -m codex_qgis_mcp.preview_server

The preview server binds to 127.0.0.1:8765 by default. Open:

http://127.0.0.1:8765/

The preview server now persists QGIS and Excel snapshots in SQLite so the UI can fall back to the latest saved data when either live source is unavailable.

Codex MCP config example

{
  "mcpServers": {
    "qgis": {
      "command": "C:\\Users\\user\\Desktop\\MCP for QGIS\\.venv\\Scripts\\python.exe",
      "args": ["-m", "codex_qgis_mcp.server"]
    }
  }
}

If needed, override the connection file path:

$env:QGIS_MCP_CONNECTION_FILE = "C:\Users\user\AppData\Roaming\QGIS\QGIS3\profiles\default\python\plugins\qgis_mcp_bridge\runtime\connection.json"

Exposed MCP tools

  • qgis_session_status
  • qgis_list_layers
  • qgis_describe_layer
  • qgis_preview_scope
  • qgis_add_field
  • qgis_set_category_color
  • qgis_calculate_area_field
  • qgis_update_attribute
  • qgis_delete_features

Example tasks for Codex

Fill an area field

Use qgis_calculate_area_field on layer "parcels" for all features, write the result into field "area_m2", create the field if it does not exist, and use unit "m2".

Rename values in a field

Use qgis_update_attribute on layer "buildings", field "name", scope expression "\"type\" = 'school'", mode "prefix", value "School: ".

Delete a subset

Use qgis_delete_features on layer "roads" for selected features.

Tests

QGIS-side tests:

& 'C:\Program Files\QGIS 3.20.1\bin\python-qgis.bat' -m unittest discover -s tests\qgis_plugin -p "test_*.py"

Server-side tests:

.\.venv\Scripts\python.exe -m unittest discover -s tests\server -p "test_*.py"

Local web preview

The web preview is a read-only hybrid viewer:

  • QGIS renders the current map as a PNG snapshot so labels match the desktop session.
  • The browser overlays GeoJSON features for click inspection only.
  • Synchronization is manual through Refresh from QGIS.
  • A separate Business Plan panel renders the summary block from a configured Excel workbook above the map.
  • Successful QGIS bootstrap, QGIS map snapshots, and Excel payloads are stored in SQLite and reused as the latest fallback when the live QGIS bridge or workbook file cannot be reached.

By default the snapshot database is stored at:

runtime\preview\snapshots.sqlite3

Optional override:

$env:QGIS_MCP_SNAPSHOT_DB = "C:\path\to\snapshots.sqlite3"

HTTP endpoints exposed by the preview server:

  • GET /
  • GET /healthz
  • GET /api/status
  • GET /api/bootstrap
  • GET /api/business-plan
  • POST /api/snapshot
  • GET /api/snapshots/{snapshot_id}/map.png

Business plan workbook configuration

The preview server reads the workbook path from QGIS_MCP_BUSINESS_PLAN_FILE.

Optional worksheet override:

$env:QGIS_MCP_BUSINESS_PLAN_SHEET = "bp 26"

You can also pass both values through CLI arguments:

.\.venv\Scripts\python.exe -m codex_qgis_mcp.preview_server --business-plan-file "C:\path\to\plan.xlsx" --business-plan-sheet "bp 26"

scripts\run_web_preview.ps1 auto-detects the requested БП ЧЕРНІГІВ 2026 23.03.26 СРЕДН.УРОЖ.xlsx workbook on the local desktop and sets QGIS_MCP_BUSINESS_PLAN_SHEET=bp 26 before launch when those values are not already provided.

Operational notes

  • Backups are written as .gpkg snapshots before every successful mutating operation.
  • Recovery is manual through the returned backup_path.
  • The bridge listens on 127.0.0.1 only and requires a session token from connection.json.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages