QGIS Skill
Geospatial analysis and GIS operations via the nkarasiak/qgis-mcp plugin and FastMCP server.
When to Use This Skill
- Calculate distances between geographic points
- Create buffer zones around features (proximity analysis)
- Transform coordinates between CRS (GPS to Web Mercator)
- Load and manipulate geospatial layers (Shapefile, GeoJSON, GeoPackage)
- Perform geoprocessing operations (intersect, union, difference, clip)
- Export map images for reports or web display
- Query features with spatial filters
- Style layers with categorized or graduated symbology
- Run QGIS processing algorithms programmatically
- Manage project variables, settings, bookmarks
- Render maps and take canvas screenshots
When Not To Use
- For non-geographic image processing (resize, crop, filter) -- use the imagemagick skill instead
- For 3D modelling and scene rendering -- use the blender skill instead
- For generic diagrams and flowcharts -- use the mermaid-diagrams skill instead
- For data analysis without a spatial component -- use the jupyter-notebooks or pytorch-ml skills instead
Architecture
This is the deployed topology (verified against /etc/supervisord.conf and
supervisorctl status qgis-mcp), which mirrors blender's own GPU-sidecar
proxy pattern (tools/blender-mcp-proxy.js):
Claude Code (MCP Client)
|
| MCP Protocol (stdio)
v
FastMCP Server (uv run src/qgis_mcp/server.py) -- registered in mcp.json
|
| TCP Socket (length-prefixed framing, port 9877)
v
qgis-mcp supervisor proxy (/opt/agentbox/scripts/qgis_mcp_standalone.py)
| thin TCP-to-TCP bridge: local 0.0.0.0:9877 -> gui-tools-service:9877
v
QGIS Desktop on the gui-tools-service GPU sidecar, with qgis_mcp_plugin loaded
QGIS itself runs on the gui-tools-service GPU sidecar, not on a local
Display :1 — nix-built QGIS in the main image cannot reach the nvidia driver
libraries injected into /usr/lib, the same constraint that put Blender's
interactive path on the same sidecar. [program:qgis-mcp] is a supervised,
always-running local proxy (autostart=true); it only bridges the socket. If
the sidecar is down, the proxy accepts the TCP connection and then closes it
with no reply — bring the sidecar up first with ./agentbox.sh gui-tools up.
The nkarasiak/qgis-mcp plugin uses length-prefixed binary framing (4-byte big-endian uint32 header followed by JSON payload), not newline-delimited JSON. The FastMCP server handles this protocol automatically.
Plugin Source
Cloned at: /home/devuser/workspace/qgis-mcp
Symlinked to QGIS at: ~/.local/share/QGIS/QGIS3/profiles/default/python/plugins/qgis_mcp_plugin
Auto-Install / Verification (legacy git-clone shim)
This is the client-side MCP registration mcp.json currently uses
(mcp-server/server.py, which runs the ensure_repo()/ensure_plugin_symlink()
steps below before exec-ing into the FastMCP server). It is a legacy, fragile
bootstrap — it clones from GitHub and patches source files at runtime rather
than shipping pre-baked — kept only until it is replaced with a pre-baked
stdio-to-TCP bridge in the shape of blender's tools/mcp-blender-client.js.
Before using QGIS tools, verify the plugin is installed:
# Check plugin symlink exists
ls -la ~/.local/share/QGIS/QGIS3/profiles/default/python/plugins/qgis_mcp_plugin
# If missing, re-create it:
ln -s /home/devuser/workspace/qgis-mcp/qgis_mcp_plugin \
~/.local/share/QGIS/QGIS3/profiles/default/python/plugins/qgis_mcp_plugin
# If repo not cloned yet:
git clone https://github.com/nkarasiak/qgis-mcp /home/devuser/workspace/qgis-mcp
ln -s /home/devuser/workspace/qgis-mcp/qgis_mcp_plugin \
~/.local/share/QGIS/QGIS3/profiles/default/python/plugins/qgis_mcp_plugin
Adding as MCP Server to Claude Code
Legacy form (same git-clone shim as above; the deployed mcp.json entry is
queen-owned and currently wraps this via mcp-server/server.py):
claude mcp add --transport stdio qgis -- uv run --project /home/devuser/workspace/qgis-mcp src/qgis_mcp/server.py
Or add to .mcp.json at project root:
{
"mcpServers": {
"qgis": {
"command": "uv",
"args": ["run", "--project", "/home/devuser/workspace/qgis-mcp", "src/qgis_mcp/server.py"],
"env": {
"QGIS_MCP_PORT": "9877"
}
}
}
}
Tools (51+)
| Category | Tools |
|---|---|
| Project | load_project, create_new_project, save_project, get_project_info |
| Layers | get_layers, add_vector_layer, add_raster_layer, remove_layer, find_layer, create_memory_layer, set_layer_visibility, zoom_to_layer, get_layer_extent, set_layer_property |
| Features | get_layer_features, add_features, update_features, delete_features, select_features, get_selection, clear_selection, get_field_statistics |
| Styling | set_layer_style (single, categorized, graduated) |
| Rendering | render_map, get_canvas_screenshot, get_canvas_extent, set_canvas_extent |
| Processing | execute_processing, list_processing_algorithms, get_algorithm_help |
| Layouts | list_layouts, export_layout |
| Layer tree | get_layer_tree, create_layer_group, move_layer_to_group |
| Plugins | list_plugins, get_plugin_info, reload_plugin |
| System | ping, diagnose, get_qgis_info, get_raster_info, get_message_log, execute_code, batch_commands, validate_expression, get_project_variables, set_project_variable, get_setting, set_setting, transform_coordinates |
Examples
# Ping to check connection
ping()
# Load a GeoJSON layer
add_vector_layer(path="/data/cities.geojson", name="Cities")
# Run a processing algorithm
execute_processing(
algorithm="native:buffer",
parameters={"INPUT": "Cities", "DISTANCE": 10000, "OUTPUT": "memory:"}
)
# Render the map
render_map(width=1920, height=1080)
# Transform coordinates
transform_coordinates(
x=-3.3245, y=54.3889,
source_crs="EPSG:4326", target_crs="EPSG:27700"
)
Environment Variables
| Variable | Default | Description |
|---|---|---|
QGIS_MCP_HOST |
localhost |
QGIS plugin TCP host |
QGIS_MCP_PORT |
9877 |
QGIS plugin TCP port |
QGIS_MCP_TRANSPORT |
stdio |
MCP transport mode |
QGIS_MCP_LOG_FILE |
~/.local/share/qgis-mcp/server.log |
Server log file |
QGIS_MCP_LOG_LEVEL |
INFO |
Log level |
QGIS_MCP_TOOL_MODE |
granular |
granular (51 tools) or compound (~19 grouped) |
mcp.json's live qgis registration (queen-owned) already sets this same
QGIS_MCP_HOST / QGIS_MCP_PORT pair, so the two are consistent.
Compound Tool Mode
To reduce schema overhead, use compound mode (~19 grouped tools instead of 51):
QGIS_MCP_TOOL_MODE=compound uv run --project /home/devuser/workspace/qgis-mcp src/qgis_mcp/server.py
Troubleshooting
Connection refused:
# Check the local supervisor proxy is running (there is no [program:qgis])
supervisorctl status qgis-mcp
# Check the gui-tools GPU sidecar (QGIS itself) is up
./agentbox.sh gui-tools up
# Verify plugin is loaded and server started
# In QGIS: Plugins > Manage Plugins > search "QGIS MCP" > check enabled
# Click MCP toolbar button to start the server
# Test TCP connection
python3 -c "import socket; s=socket.socket(); s.connect(('localhost',9877)); print('OK'); s.close()"
Protocol mismatch (old plugin vs new):
The new plugin uses length-prefixed framing (4-byte header), not newline-delimited JSON.
Always use the FastMCP server (src/qgis_mcp/server.py) which handles framing automatically.
Do NOT send raw newline-delimited JSON to port 9877 -- it will fail with the new plugin.