Skip to main content
Glama

QgisRemoteMCP — QGIS Desktop as an MCP Server

Give any AI assistant a complete GIS workstation — data loading, spatial analysis, cartography, and multi-format export — all running in a live QGIS Desktop instance.

QgisRemoteMCP is an MCP server that exposes a full QGIS Desktop through Docker. The AI loads data, runs analysis, produces maps — while users interact with the same instance in their browser via noVNC.

MCP Streamable HTTP QGIS 3.40+ Python 3.12 License: MIT Status: Production Multi-user: Beta

Project status: Production-ready for single-user local deployments. Multi-user mode (per-user isolated containers with GPU passthrough) is functional but in beta. Developed at Cerema Méditerranée — published to share the approach and invite contributions.


What it does

You say to the AI

What happens

"Analyse le risque inondation à Nîmes"

Loads flood zones + buildings, computes exposure, exports interactive HTML with water height slider

"Carte de densité du bâti à Montpellier"

Downloads 10,000 buildings as local GPKG, creates hex grid, graduated symbology, PDF A3 export

"Prépare un relevé terrain pour Sète"

Loads data, styles layers, exports QField-ready ZIP with editable Observations layer (camera, dropdowns)

"Analyse l'évolution foncière sur la côte"

DVF transactions 2020-2024, coastal distance bands, temporal web map with animated playback

Everything runs inside Docker — the AI calls MCP tools, QGIS does the work, the user sees results live in their browser. No local QGIS installation needed.


Related MCP server: QGIS MCP

Quick start

1. Run

git clone https://github.com/nic01asFr/QgisRemoteMCP.git
cd QgisRemoteMCP
cp .env.example .env
docker compose up -d --build

2. Verify

curl http://localhost:8100/health
# → {"status":"ok","bridge":true,"server":"QgisRemoteMCP","multi_user":false}

3. Connect Claude Desktop

Edit claude_desktop_config.json:

{
  "mcpServers": {
    "qgis": {
      "type": "http",
      "url": "http://localhost:8100/mcp"
    }
  }
}

4. Connect Claude Code

The repo includes a .mcp.json — Claude Code picks it up automatically when you open the project directory.


Smart data loading

The core innovation: a structured pipeline that replaces unreliable live WFS connections with fast local GeoPackage files.

Problem

Live WFS

Smart Loading

Pagination

IGN silently truncates at 5000

ogr2ogr handles all pages automatically

Spatial index

None (in-memory)

R-tree in GeoPackage

Processing speed

60-250x slower

Fast local file

Network during analysis

HTTP per feature

Zero network

CRS confusion

Mixed 4326/3857/2154

Standardized EPSG:2154

1. set_study_zone("Montpellier")         → geocode, store bbox, zoom
2. smart_load("osm_xyz")                 → basemap (streaming)
3. smart_load("bdtopo_batiments")        → 10,000 buildings as local GPKG
4. run_processing / execute_python       → analyse (fast, no network)
5. export_pdf / export_web_map           → deliver

Downloads are cached in /data/cache/ with bbox hash — same area = instant reload for 24h.

Performance (Montpellier, ~10 km bbox)

Operation

Features

Time

Download buildings

10,000

~30s

Download roads

5,000

6.8s

Cache reload (2nd call)

10,000

instant

Buffer 50m

10,000

1.8s

Density grid 500m

440 cells

0.3s


Available data sources

All sources are free (IGN open data since July 2021). No API key needed. 47 pre-configured in datasources.json.

Vector (WFS → local GPKG)

ID

Name

Key attributes

bdtopo_batiments

Buildings

nature, usage, height, floors, materials

bdtopo_routes

Roads

nature, importance, width, lanes, speed

bdtopo_hydrographie

Rivers

name, class, width

bdtopo_communes

Communes

name, INSEE code, population

admin_express_communes

Communes (Admin Express)

name, code, population

rpg

Agricultural parcels

crop type, area

+ 10 more

BD TOPO vegetation, railways, POI, activity zones...

Basemaps & imagery (streaming)

ID

Name

osm_xyz

OpenStreetMap

ign_planign

Plan IGN v2

ign_ortho_wmts

IGN orthophotos

ign_cadastre

Cadastral parcels

corine_land_cover

Land cover 2018

ign_dem

Hillshade from high-resolution DEM (display)

ign_dem_altitudes

Raw elevation values, 32-bit TIFF (compute only, not renderable)

+ 10 more

Esri, Stamen, CartoDB, infrared, SCAN 25...

APIs

ban_geocode (address search), geo_api_communes (commune info), dvf_api (property transactions), panoramax (street imagery), ign_altimetrie (elevation).


Recipes

Pre-built workflow templates — from data loading to styled map export in one command.

ID

Name

Output

densite_bati

Building density

Hex grid + graduated symbology + PDF

urbanisme_general

Urban overview

Buildings, roads, vegetation + categorized styles

risque_inondation

Flood risk

Flood zones + building exposure + interactive web map

occupation_sol

Land cover

Corine Land Cover + categorized symbology

pression_fonciere_cotiere

Coastal land pressure

DVF 2020-2024 + coastal bands + temporal web map

# Automated — all steps in one shot
run_recipe(id="risque_inondation", zone="Nimes")

# Manual — follow steps one by one
get_recipe(id="densite_bati", zone="Montpellier")

Export formats

Format

Tool

Output

PDF

export_pdf

Print-ready layout (A3 landscape, A4 portrait) with title, legend, scalebar

Web map

export_web_map

Leaflet HTML with embedded GeoJSON, layer toggle, popups

Flood map

export_flood_map

Interactive HTML — water height slider, building exposure stats, animation

Temporal map

export_temporal_map

Interactive HTML — year slider, trend arrows, animated playback

QField

export_qfield

Portable ZIP for mobile (.qgz + GPKGs + editable Observations layer)

Grist

export_grist

Collaborative document with typed columns, map widget, form pages

Layer

export_layer

GPKG, GeoJSON, Shapefile, or CSV

Grist export — universal HTML converter

export_grist has two modes:

  1. From QGIS project — exports loaded layers as Grist tables with map widget

  2. From any HTML file — takes any Leaflet HTML containing GeoJSON (flood maps, temporal maps, qgis2web) and creates a Grist document with data in tables and the original interactive map as a custom widget

Auto-detected column types: Choice (colored dropdowns), Date (epoch timestamps), Ref (cross-table references). Form-like tables get a Grist Form page.


MCP tools (46)

Smart loading

Tool

Description

set_study_zone

Define study area (commune, address, bbox). Geocodes, stores bbox, zooms.

get_study_zone

Get current zone (name, bbox in 4326 + 2154).

smart_load

Load data by catalog ID. WFS → local GPKG. Rasters stream.

list_datasources

Browse data catalog (filter by category or search).

add_from_catalog

Add source by catalog ID.

Core

Tool

Description

execute_python

Run PyQGIS code with helpers module, iface, project, processing.

get_screenshot

Capture QGIS canvas (JPEG ≤1MB). Auto-included after modifying tools.

get_project_info

Project state: layers, CRS, layouts, extents.

run_processing

Execute any of ~730 Processing algorithms (native, qgis, gdal, grass, 3d).

search_algorithms

Find algorithms by keyword.

restart_qgis_engine

Respawn the QGIS process when the bridge is deadlocked. Last resort — 5-15 s of unavailability.

Async execution

Anything expected to run past 30 s — a heavy native:difference, an extractbylocation over 50 000 features, a wide smart_load — should be submitted rather than awaited. The bridge streams heartbeats, so a frozen Qt main thread is told apart from a dead bridge without waiting for a timeout to expire.

Tool

Description

execute_async

Submit a bridge action for background execution, returns a job_id immediately.

poll_job

Job status, heartbeat_age_s, qt_lag_ms, probably_frozen, and the result once done.

cancel_job

Best-effort cancellation.

Data & layers

Tool

Description

add_layer

Add vector/raster/WFS/WMS by URI. Point clouds too — pass provider=copc|ept|vpc and the layer is built as a point cloud, with point_count returned.

remove_layer

Remove a layer.

get_features

Query features with attribute/spatial filters.

zoom_to

Navigate to extent, layer, or point.

Styling & layout

Tool

Description

set_layer_style

Single color, categorized, or graduated symbology.

set_layer_visibility

Show/hide layers.

apply_layout_template

Apply print layout template (A3/A4).

list_layout_templates

List available templates.

Recipes

Tool

Description

list_recipes

Browse workflow recipes.

get_recipe

Get recipe with parameter substitution.

run_recipe

Execute all steps automatically.

Export

Tool

Description

export_pdf

Print layout → PDF.

export_web_map

Visible layers → Leaflet HTML.

export_flood_map

Flood analysis → interactive HTML.

export_temporal_map

Time series → interactive HTML.

export_qfield

QField-ready ZIP (.qgz + GPKGs + Observations).

export_grist

Grist document from project or HTML.

export_layer

Layer → GPKG/GeoJSON/Shapefile/CSV.

publish_artifact

Publish a deliverable (storymap, dataset, recipe, PDF) to S3 through the hub, returns a stable public URL.

Files

Tool

Description

upload_file

Upload via multipart POST (any size), URL fetch, or base64.

download_file

Download from /data/ (URL for user, inline for small files).

list_files

List files in /data/.

delete_file

Delete file from /data/.

download_project

Save as .qgz.

GUI interaction

Tool

Description

qgis_desktop_ui

Open interactive QGIS view in conversation (MCP App).

mouse_click / mouse_scroll / mouse_drag / key_press

Direct GUI interaction.

Project management

Tool

Description

new_project / open_project / save_project

Project lifecycle.


MCP resources & prompts

Resources (skill documents)

URI

Content

skill://smart-loading

Smart loading pipeline, CRS handling, caching

skill://pyqgis

PyQGIS scripting patterns & API usage

skill://processing

Processing algorithms guide (native, GDAL, GRASS)

skill://cartography

Symbology, labels, layouts, PDF export

skill://helpers

Python helpers (geocode, add_wfs, zoom_to, overpass_query...)

skill://data-sources

French national datasets reference

skill://recipes

Workflow recipes reference

skill://external-services

Vision services integration

skill://solar

Solar cadastre pipeline (DSM, r.sun, observatory)

skill://file-exchange

bridge_put / bridge_get file transfer — design, pending implementation

skill://qgis-status

Live QGIS instance status

Prompts

Prompt

Description

analyse_territoire

Territory analysis template (zone + question)

workflow_donnees

Guided theme-based workflow (urbanisme, environnement, transport, agriculture, risques)


Multi-user mode (beta)

Each authenticated user gets an isolated QGIS container with its own project, data, and noVNC session.

Enable

# .env
MULTI_USER_MODE=true
JWT_SECRET=your-secret-key
IDLE_TIMEOUT_MINUTES=30

Uncomment the Docker socket volume in docker-compose.yml.

How it works

Agent A (Bearer: qgis_xxx)  →  Gateway (:8100)  →  Container-A (172.22.0.3:8080)
Agent B (Bearer: qgis_yyy)  →  Gateway (:8100)  →  Container-B (172.22.0.4:8080)
  • POST /api/auth/register → get API key (qgis_...)

  • All MCP calls with Authorization: Bearer qgis_xxx → routed to user's container

  • Containers auto-start on first call, auto-stop after idle timeout

  • Per-user data isolation (/data/users/<user_id>/)

  • GPU automatically passed to workers when available (NVIDIA Container Toolkit)

Endpoints (multi-user)

Method

Route

Purpose

POST

/api/auth/register

Register user, get API key

POST

/api/auth/login

Login, get token

GET

/api/session

Current user's container info

GET

/api/sessions

List all active sessions


GPU support

GPU is automatically detected at startup and passed through to all containers for compute workloads (GDAL CUDA, PyTorch, heavy raster processing). QGIS rendering stays on CPU (Xvfb limitation).

Requirements:


Architecture

┌──────────────────────────────────────────────────────────────┐
│                 QgisRemoteMCP Container                      │
│                                                              │
│  supervisord                                                 │
│  ├── Xvfb :99              (virtual display 1920x1080)      │
│  ├── fluxbox               (window manager)                 │
│  ├── QGIS Desktop ◄──────────────────┐                     │
│  │   └── qgis_bridge.py  (startup)   │ UNIX socket         │
│  ├── x11vnc → noVNC       (:6080)    │                     │
│  ├── api_server.py         (:8080) ──┘                     │
│  ├── main_mcp.py           (:8100)                          │
│  └── stream_server.py      (:8081)                          │
│                                                              │
│  /data/          user files, projects, exports              │
│  /data/cache/    smart_load GPKG cache (24h)                │
│  /app/skills/    MCP resource documents                     │
└──────────────────────────────────────────────────────────────┘

Communication flow

MCP Client (Claude Desktop, Claude Code, any MCP client)
  │ JSON-RPC over Streamable HTTP (:8100)
  ▼
main_mcp.py (MCP Server, 46 tools)
  │ HTTP → api_server.py (:8080) → UNIX socket
  ▼
qgis_bridge.py (runs inside QGIS, Qt main thread)
  │ PyQGIS API (iface, QgsProject, processing)
  ▼
QGIS Desktop (Xvfb :99) → x11vnc → noVNC (:6080) → browser

Workflow context

Every mutating tool response includes a _context with:

  • phase: setup / analysis / cartography / export (auto-detected)

  • study_zone: current zone name

  • layers: loaded layers with feature counts

  • hint: suggested next action

This guides the AI through structured workflows without hard restrictions.


Customization — skills, templates, recipes, data sources

QgisRemoteMCP is designed to be specialized for specific domains. All customization is file-based — no code changes needed.

Skills (MCP Resources)

Skills are Markdown documents in skills/ that teach the AI how to work. The LLM reads them on demand via skill:// URIs.

skills/
├── smart_loading.md     # Data loading pipeline, CRS, caching
├── pyqgis.md            # PyQGIS scripting patterns
├── processing.md        # Processing algorithms (native, GDAL, GRASS)
├── cartography.md       # Symbology, labels, layouts, PDF export
├── helpers.md           # Ready-made Python helpers reference
├── data_sources.md      # French national datasets
├── recipes.md           # Workflow recipes reference
└── external_services.md # Vision services (Moondream, SAMGeo3, DepthPro)

To add a skill: create skills/my_domain.md, then register it in main_mcp.py:

  • Add to RESOURCES[] with a skill://my-domain URI

  • Add to the _SKILL_MAP dict

Example: a skills/environnement.md skill could document environmental analysis patterns (species habitats, noise mapping, air quality), making the AI an environmental GIS specialist.

Print layout templates (.qpt)

QGIS print layout templates in templates/:

Template

Format

Content

a3_landscape.qpt

A3 landscape

Title, subtitle, legend, scalebar, north arrow, data sources

a4_portrait.qpt

A4 portrait

Same elements, portrait layout

Templates use QGIS expressions for dynamic labels: [% @title %], [% @subtitle %], [% @data_sources %]. Variables are set by apply_layout_template().

To add a template: create a .qpt file in QGIS Layout Manager → Export as Template. Place it in templates/. It's automatically available via list_layout_templates and apply_layout_template.

Web map templates (Leaflet HTML)

Three specialized Leaflet templates in templates/web/:

Template

Use case

Key features

leaflet_template.html

General map

Layer toggle, popups, legend, basemap selector

leaflet_flood_template.html

Flood analysis

Water height slider, building exposure stats, animation

leaflet_temporal_template.html

Time series

Year slider, per-band stats, trend arrows, playback

Templates are plain HTML/JS with placeholder markers (__LAYERS_DATA__, __TITLE__) replaced at export time by the bridge.

To create a custom template: copy an existing one, modify the HTML/JS, and register it in qgis_bridge.py as a new export action.

Recipes (JSON workflows)

Recipes are step-by-step GIS workflows in recipes/:

{
  "id": "densite_bati",
  "name": "Carte de densité du bâti",
  "description": "Hex grid density analysis...",
  "tags": ["urbanisme", "densité"],
  "parameters": {
    "zone": {"type": "string", "required": true}
  },
  "steps": [
    {"action": "set_study_zone", "params": {"target": "{zone}"}},
    {"action": "smart_load", "params": {"id": "bdtopo_batiments"}},
    {"action": "run_processing", "params": {"algorithm": "native:creategrid", ...}},
    {"action": "set_layer_style", "params": {...}}
  ]
}

To add a recipe: create a JSON file in recipes/. It's automatically available via list_recipes, get_recipe, and run_recipe. Parameters like {zone} are substituted at runtime.

Data sources catalog

datasources.json contains 47 pre-configured French data sources. Each entry:

{
  "id": "bdtopo_batiments",
  "name": "Bâtiments (BD TOPO)",
  "category": "topography",
  "type": "wfs",
  "url": "https://data.geopf.fr/wfs/ows",
  "typename": "BDTOPO_V3:batiment",
  "description": "Buildings with usage, height, materials"
}

To add a source: add an entry to datasources.json. Supported types: wfs (downloaded as GPKG via smart_load), wms, wmts, xyz (streaming), api (custom).

Specialization patterns

To create a domain-specific QGIS MCP server:

  1. Add domain skillsskills/urbanisme.md, skills/hydrologie.md, etc.

  2. Add domain data sources — extend datasources.json with relevant WFS/WMS

  3. Create domain recipes — automated workflows for common analyses

  4. Design layout templates — branded .qpt with your organization's logo and style

  5. Customize web templates — adapt Leaflet HTML templates for your visualization needs

The same Docker image serves all specializations — everything is configured through mounted files.


Project structure

QgisRemoteMCP/
├── main_mcp.py              # MCP Server (46 tools, 12 resources, 3 prompts)
├── qgis_app.html            # MCP App (interactive QGIS in conversation)
├── datasources.json         # 47 pre-configured French data sources
├── src/
│   ├── qgis_bridge.py       # Runs inside QGIS (47 actions, Qt main thread)
│   ├── qgis_helpers.py      # Python helpers (geocode, smart loading, etc.)
│   ├── api_server.py        # FastAPI REST API (file upload/download, commands)
│   ├── stream_server.py     # MJPEG stream
│   ├── auth.py              # Authentication (multi-user mode)
│   └── container_manager.py # Per-user Docker containers (multi-user mode)
├── skills/                  # MCP Resources (AI skill documents)
├── recipes/                 # Workflow recipes (JSON)
├── templates/               # Print layouts (.qpt) + web map templates (Leaflet)
├── Dockerfile
├── docker-compose.yml
├── supervisord.conf
├── entrypoint.sh
├── requirements.txt
├── .env.example
├── CLAUDE.md                # AI assistant instructions
├── CONTRIBUTING.md
└── LICENSE                  # MIT

Development

Source files are mounted as volumes in dev — edit locally, restart to apply:

docker compose restart qgisremotemcp
docker compose logs -f qgisremotemcp

QGIS bridge changes require a full restart (loaded at QGIS startup via PYQGIS_STARTUP).

Testing

# Health
curl http://localhost:8100/health

# Execute Python
curl -X POST http://localhost:8081/api/execute \
  -H "Content-Type: application/json" \
  -d '{"code": "result[\"version\"] = Qgis.version()"}'

# Run a recipe
curl -X POST http://localhost:8100/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"run_recipe","arguments":{"id":"risque_inondation","zone":"Nimes"}}}'

What works, what doesn't

Stable (single-user, Docker)

  • Full MCP server: 46 tools, 11 resources, 3 prompts, SSE streaming

  • Smart data loading pipeline: WFS → GPKG with pagination, R-tree, caching

  • All export formats: PDF, Leaflet (standard/flood/temporal), QField, Grist

  • PyQGIS scripting with helpers module (geocode, overpass, smart loading)

  • ~730 Processing algorithms (native, qgis, GDAL, GRASS, 3d) — SAGA n'est pas installe, et QGIS a retire son provider du coeur depuis la 3.30

  • MCP App: interactive QGIS in conversation (noVNC + file management)

  • Workflow recipes: 5 pre-built analysis templates

  • Print layouts: A3/A4 with dynamic labels

Beta — functional, needs hardening

  • Multi-user mode: per-user Docker containers with auth, session isolation, and idle cleanup. Tested with 2 concurrent users. Not yet tested at scale or in production.

  • GPU passthrough: auto-detected and passed to workers for CUDA/compute. Works with NVIDIA Container Toolkit. Xvfb rendering stays CPU.

  • Grist HTML→Grist converter: works with all tested Leaflet HTML files but edge cases may exist with unusual GeoJSON structures.

Known limitations

  • QGIS rendering uses Mesa llvmpipe (CPU) — Xvfb cannot use GPU for OpenGL

  • Single-file MCP server (main_mcp.py, ~2300 lines) — intentional for deployment simplicity

  • In-memory sessions in multi-user mode — no persistence across gateway restarts

  • No automated tests yet

  • French-focused data sources (IGN, BD TOPO) — extend datasources.json for other countries


Compatible MCP clients

Tested with:


Tech stack

  • QGIS 3.40+ · PyQGIS · Processing · GDAL/OGR 3.8

  • Python 3.12 · FastAPI · uvicorn · httpx · MCP SDK

  • Docker · supervisord · Xvfb · fluxbox · x11vnc · noVNC

  • MCP transport: Streamable HTTP (spec 2025-03-26)

  • No database: all state in QGIS project + filesystem


Contributing

We welcome contributions — bug reports, feature ideas, or merge requests.

Particularly interested in

  • Additional data sources — extend datasources.json for non-French datasets

  • New recipes — workflow templates for common GIS analyses

  • Testing — automated tests, CI/CD pipeline

  • Multi-user hardening — HTTPS, rate limiting, session persistence

  • Documentation — usage guides, video demos, tutorials

How to contribute

  1. Fork the repo

  2. Create a feature branch (git checkout -b feat/my-feature)

  3. Test with a real QGIS container (docker compose up -d)

  4. Submit a merge request

See CONTRIBUTING.md for detailed guidelines.


External vision services (optional)

Service

Default URL

Purpose

Moondream

http://localhost:8001

Image captioning, VQA

SAMGeo3

http://localhost:8002

Geospatial segmentation

DepthPro

http://localhost:8003

Monocular depth estimation

Configure via MOONDREAM_URL, SAMGEO3_URL, DEPTHPRO_URL in .env.


Credits


License

MIT — Nicolas LAVAL, Cerema Méditerranée

Maintenance

ActivityActive
ResponsivenessSlow

Related MCP Connectors

Related MCP Servers

  • A
    license
    B
    quality
    D
    maintenance
    A Model Context Protocol server that connects Claude AI to QGIS, enabling direct interaction with the GIS software for project creation, layer manipulation, code execution, and processing algorithms through natural language prompts.
    15
    10
    Creative Commons Zero v1.0 Universal
  • A
    license
    B
    quality
    A
    maintenance
    An MCP server that enables AI assistants to directly control QGIS for tasks like layer management, feature editing, and map rendering. It provides a suite of 50 tools to execute processing algorithms and manage GIS projects through natural language commands.
    118
    281
    GPL 2.0
  • F
    license
    B
    quality
    D
    maintenance
    QGISMCP connects QGIS to Claude AI through the Model Context Protocol (MCP), allowing Claude to directly interact with and control QGIS for project creation, layer loading, code execution, and more.
    15
    1
    -
  • A
    license
    Not graded
    quality
    A
    maintenance
    An MCP server that connects AI agents to cloud-native geospatial data via STAC metadata and DuckDB with H3 spatial indexing, enabling zero-configuration SQL queries on terabyte-scale datasets over S3.
    23
    BSD 3-Clause