Qgisstreammcp — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Qgisstreammcp (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
QGIS as a service for your AI assistant — Deploy once, use QGIS from anywhere. No local installation required, no GIS expertise needed.
Run a full QGIS Desktop inside Docker and expose it as an MCP server. Your AI assistant gets 42 tools covering data loading, spatial analysis, cartography and multi-format export. You don't need QGIS installed — the AI handles it entirely on your behalf, from any device, at any time.
Administrators deploy the container once (on a server, a NAS, or locally). Users connect their AI client and immediately have QGIS capabilities available in their conversations.
<p align="center"> <img src="docs/architecture.png" alt="Architecture" width="700"> </p>
set_study_zone("Montpellier") then smart_load("bdtopo_batiments"). Downloads WFS as local GeoPackage with automatic pagination, R-tree spatial index, CRS transform. Processing runs 60-250x faster than live WFS.run_recipe("risque_inondation", zone="Nimes").git clone https://github.com/nic01asFr/QgisStreamMCP.git
cd QgisStreamMCP
docker compose up -d --build| Port | Service | URL |
|---|---|---|
| 6080 | noVNC (QGIS in browser) | http://localhost:6080 |
| 8100 | MCP Server (Streamable HTTP) | http://localhost:8100/mcp |
| 8080 | REST API | http://localhost:8080/docs |
| 8081 | MJPEG stream | http://localhost:8081/stream |
Add to claude_desktop_config.json:
{
"mcpServers": {
"qgis": {
"url": "http://localhost:8100/mcp"
}
}
}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 (network + no index) | Fast local file |
| Network during analysis | HTTP requests per feature | Zero network |
| CRS confusion | Mixed 4326/3857/2154 | Standardized EPSG:2154 |
1. set_study_zone(target="Montpellier") # Geocode, store bbox, zoom
2. smart_load(id="osm_xyz") # Basemap (streaming)
3. smart_load(id="bdtopo_batiments") # Buildings (download GPKG)
4. smart_load(id="bdtopo_routes") # Roads (download GPKG)
5. get_screenshot # Verify
6. run_processing / execute_python # Analyze
7. export_pdf / export_web_map / ... # Deliver (PDF, HTML, QField, Grist)| 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 |
| Dissolve by usage | 10,000 | 2.2s |
| Density grid 500m | 440 cells | 0.3s |
Downloads are cached in /data/cache/ with bbox hash. Same area = instant reload for 24h.
All sources are free (IGN open data since July 2021). No API key needed.
| 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_vegetation | Vegetation zones | nature |
bdtopo_voie_ferree | Railways | nature, nb_voies |
bdtopo_hydro_surfaces | Water bodies | nature, name |
bdtopo_communes | Communes (BD TOPO) | name, code INSEE, population |
admin_express_communes | Communes (Admin Express) | name, code, population |
admin_express_departements | Departments | name, code |
admin_express_regions | Regions | name, code |
rpg | Agricultural parcels | crop type, area |
bdtopo_poi | Points of interest | nature |
bdtopo_lieu_dit | Place names | name |
bdtopo_surface_activite | Activity zones | nature |
bdtopo_equipement_transport | Transport facilities | nature |
| ID | Name |
|---|---|
osm_xyz | OpenStreetMap |
ign_planign | Plan IGN v2 |
ign_scan25 | Cartes topo IGN 1:25000 |
cartodb_positron | Light basemap |
cartodb_dark | Dark basemap |
esri_world_topo | Esri World Topographic |
stamen_terrain | Stamen Terrain (relief) |
| ID | Name |
|---|---|
ign_ortho_wmts | IGN orthophotos (WMTS, fast) |
ign_ortho_wms | IGN orthophotos (WMS) |
ign_ortho_irc | IGN infrared photos |
esri_world_imagery | Esri satellite |
| ID | Name |
|---|---|
ign_cadastre | Cadastral parcels |
ign_dem | High-resolution DEM |
corine_land_cover | Land cover 2018 |
ban_geocode | Address geocoding API |
geo_api_communes | Commune info API |
dvf_api | Property transactions API |
panoramax | Street-level imagery API |
ign_altimetrie | Elevation API |
Pre-built workflow templates that automate complete analyses — from data loading to styled map export.
| ID | Name | Description |
|---|---|---|
densite_bati | Building Density | Hex grid density analysis with graduated symbology |
urbanisme_general | Urban Overview | Buildings, roads, vegetation, hydrology with categorized styles |
risque_inondation | Flood Risk | Flood zones, building exposure, buffer analysis + interactive web map |
occupation_sol | Land Cover | Corine Land Cover with categorized symbology |
pression_fonciere_cotiere | Coastal Land Pressure | DVF transactions 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")
→ Returns step-by-step instructions to execute individuallyPrint-ready PDF via QGIS print layouts with pre-built templates (A3 landscape, A4 portrait). Includes title, legend, scalebar, north arrow, and data sources.
apply_layout_template(template="a3_landscape", title="Flood Risk — Nimes")
export_pdf(layout="a3_landscape")Leaflet HTML files with embedded GeoJSON data. Three specialized templates:
| Template | Use case | Features |
|---|---|---|
| Standard | General map | Layer toggle, popup, legend, basemap selector |
| Flood | Flood risk analysis | Water height slider, building exposure stats, animation |
| Temporal | Time series | Year slider, per-band statistics, trend arrows, animated playback |
Portable ZIP ready for QField mobile data collection:
.qgz project with relative GPKG sourcesexport_qfield(project_name="terrain_survey")
→ /data/terrain_survey_qfield.zipConverts QGIS project layers or any HTML map into a Grist collaborative document (.grist):
Detected column types: Choice (colored dropdowns), Date (epoch timestamps), Ref (cross-table references). Form-like tables automatically get a Grist Form page.
# From QGIS project
export_grist(title="Urban Analysis")
# From any HTML with GeoJSON
export_grist(html_path="/data/flood_map_nimes.html")| Tool | Description |
|---|---|
set_study_zone | Define study area (commune, address, bbox). Geocodes, stores bbox, zooms canvas. |
get_study_zone | Get current study zone (name, bbox in 4326 + 2154). |
smart_load | Load data by catalog ID. WFS → local GPKG with spatial index. Rasters stream. |
| Tool | Description |
|---|---|
execute_python | Run PyQGIS code with full access to iface, project, processing, helpers module. |
get_screenshot | Capture QGIS canvas as PNG. Auto-included after modifying tools. |
get_project_info | Current project state (layers, CRS, layouts, extents). |
run_processing | Execute any of 1000+ Processing algorithms. |
search_algorithms | Find Processing algorithms by keyword. |
zoom_to | Navigate to extent, layer, or point. |
| Tool | Description |
|---|---|
add_layer | Add vector/raster/WFS/WMS by URI. |
remove_layer | Remove a layer. |
get_features | Query features with attribute/spatial filters. |
list_datasources | Browse the pre-configured data catalog. |
add_from_catalog | Add a source by catalog ID. |
| Tool | Description |
|---|---|
set_layer_style | Apply single color, categorized, or graduated symbology. |
set_layer_visibility | Show/hide layers. |
apply_layout_template | Apply a print layout template (A3 landscape, A4 portrait). |
list_layout_templates | List available layout templates. |
| Tool | Description |
|---|---|
list_recipes | Browse available workflow recipes. |
get_recipe | Get recipe details with parameter substitution. |
run_recipe | Execute a complete recipe automatically (all steps in one shot). |
| Tool | Description |
|---|---|
export_pdf | Export print layout to PDF. |
export_web_map | Export visible layers as interactive Leaflet HTML. |
export_flood_map | Interactive flood analysis HTML (water height slider, building exposure). |
export_temporal_map | Interactive temporal analysis HTML (year slider, animated playback). |
export_qfield | QField-ready ZIP package (.qgz + GPKGs + editable Observations layer). |
export_grist | Grist document from project layers or from any HTML with GeoJSON. |
export_layer | Export vector layer to GPKG, GeoJSON, Shapefile, CSV. |
| Tool | Description |
|---|---|
upload_file | Upload file (shapefile, GeoJSON, GPKG, CSV, TIFF, project). |
download_file | Download file from /data/. |
list_files | List files in /data/. |
delete_file | Delete file from /data/. |
download_project | Save project as .qgz. |
| Tool | Description |
|---|---|
qgis_desktop_ui | Open interactive QGIS MCP App in conversation. |
mouse_click / mouse_scroll / mouse_drag / key_press | Direct GUI interaction via xdotool. |
| Tool | Description |
|---|---|
new_project | Create empty project. |
open_project | Open a .qgz project. |
save_project | Save current project. |
Reference documents that guide the AI assistant's expertise:
| Resource URI | Content |
|---|---|
skill://smart-loading | Smart Loading Pipeline — set_study_zone + smart_load, CRS handling, caching |
skill://pyqgis | PyQGIS scripting patterns & API usage |
skill://processing | Processing algorithms guide (native, GDAL, GRASS) |
skill://cartography | Symbology, labels, print layouts, PDF export |
skill://helpers | Ready-made Python helpers (geocode, add_wfs, zoom_to, create_point_layer...) |
skill://data-sources | French national datasets reference |
skill://recipes | Workflow recipes reference |
skill://external-services | Calling external HTTP services from PyQGIS scripts |
skill://qgis-status | Live QGIS instance status |
| Prompt | Description |
|---|---|
analyse_territoire | Template for territory analysis (zone + question). |
workflow_donnees | Guided workflow — theme-based loading (urbanisme, environnement, transport, agriculture, risques) with step-by-step instructions. |
An interactive QGIS view embedded directly in the Claude conversation:
User: "Analyse l'urbanisation autour de Montpellier"
AI: [set_study_zone("Montpellier")]
→ Geocodes, stores bbox, zooms canvas
[smart_load("osm_xyz")] → OpenStreetMap basemap
[smart_load("bdtopo_batiments")] → 10,000 buildings as GPKG
[smart_load("bdtopo_routes")] → 5,000 road segments
[execute_python → density grid] → 500m hex grid, graduated symbology
[apply_layout_template("a3_landscape", title="Densité bâtie — Montpellier")]
[export_pdf] → /data/densite_montpellier.pdfUser: "Analyse le risque inondation à Nîmes"
AI: [run_recipe("risque_inondation", zone="Nimes")]
→ Executes all steps automatically:
1. set_study_zone("Nimes")
2. smart_load basemap + buildings + flood zones
3. Buffer analysis (50m, 100m, 200m from flood zones)
4. Building exposure classification
5. Graduated symbology
→ Returns screenshot + statistics
[export_flood_map(include_fields=["nature","usage","height"])]
→ Interactive HTML with water height slider
[export_grist(html_path="/data/flood_map_nimes.html")]
→ Grist document with editable tables + embedded map widgetUser: "Prépare un relevé terrain pour la commune de Sète"
AI: [set_study_zone("Sète")]
[smart_load("bdtopo_batiments")]
[smart_load("bdtopo_routes")]
[set_layer_style("Batiments", type="categorized", field="usage")]
[export_qfield(project_name="releve_sete")]
→ ZIP with .qgz + GPKGs + Observations layer (camera, dropdowns, date picker)
→ Ready to load on QField mobile appPyQGIS scripts (execute_python) can call any HTTP service accessible from the container — local or remote. This covers ML inference APIs, custom backends, satellite processors, elevation services, or any internal API.
Pass service URLs via environment variables in .env:
MY_SERVICE_URL=http://host.docker.internal:8001Then use them in scripts:
import os, urllib.request, json
url = os.environ.get("MY_SERVICE_URL")
# call your service...host.docker.internal resolves to the host machine, allowing access to services running outside the container.
┌─────────────────────────────────────────────────────────────┐
│ QgisStreamMCP 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) │
│ /data/cache/ (smart_load GPKG cache, 24h) │
│ /app/skills/ (MCP skill documents) │
│ /app/datasources.json (30+ pre-configured sources) │
└──────────┬──────────────────────────────────────────────────┘
│ HTTP (optional)
▼
External services (any HTTP API reachable from the container)Claude Desktop / MCP Client
│ JSON-RPC over Streamable HTTP (:8100)
▼
main_mcp.py (MCP Server)
│ UNIX socket /tmp/qgis_bridge.sock
▼
qgis_bridge.py (runs inside QGIS, main thread)
│ PyQGIS API (iface, QgsProject, processing)
▼
QGIS Desktop (Xvfb display :99)
│ X11
▼
x11vnc → websockify → noVNC (:6080)
│ WebSocket
▼
User's browser# Source files are mounted as volumes — edit locally
# Restart to apply changes:
docker compose restart qgisstreammcp
# View logs
docker compose logs -f qgisstreammcp
# Test API
curl http://localhost:8080/health
curl -X POST http://localhost:8080/api/execute \
-H "Content-Type: application/json" \
-d '{"code": "result[\"v\"] = Qgis.version()"}'
# Test smart loading
curl -X POST http://localhost:8080/api/execute \
-H "Content-Type: application/json" \
-d '{"code": "result.update(helpers.set_study_zone(\"Montpellier\"))", "timeout": 30}'QgisStreamMCP/
├── main_mcp.py # MCP Server (40 tools, 10 resources, 3 prompts)
├── datasources.json # 30+ pre-configured data sources catalog
├── qgis_app.html # MCP App (interactive QGIS in conversation)
├── src/
│ ├── qgis_bridge.py # Runs inside QGIS (UNIX socket bridge, 45 actions)
│ ├── qgis_helpers.py # Python helpers (geocode, smart loading, etc.)
│ ├── api_server.py # FastAPI REST API
│ └── stream_server.py # MJPEG stream
├── skills/ # MCP Resources (AI skill documents)
│ ├── smart_loading.md
│ ├── pyqgis.md
│ ├── processing.md
│ ├── cartography.md
│ ├── helpers.md
│ ├── data_sources.md
│ └── external_services.md
├── recipes/ # Workflow recipes (JSON)
│ ├── densite_bati.json
│ ├── urbanisme_general.json
│ ├── risque_inondation.json
│ ├── occupation_sol.json
│ └── pression_fonciere_cotiere.json
├── templates/ # Print layout templates (.qpt)
│ ├── a3_landscape.qpt
│ ├── a4_portrait.qpt
│ └── web/ # Leaflet HTML templates
│ ├── leaflet_template.html
│ ├── leaflet_flood_template.html
│ └── leaflet_temporal_template.html
├── projects/ # QGIS project files (persisted)
├── docs/ # Architecture diagrams
├── Dockerfile
├── docker-compose.yml
├── supervisord.conf
├── entrypoint.sh
├── requirements.txt
└── CLAUDE.mdMIT
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.