ArcGIS MCP Bridge

Created By
muend4 days ago
Secure local-first MCP server exposing 100 ArcGIS Pro / ArcPy geoprocessing tools to LLM agents. Two-process architecture, PathGuard filesystem sandboxing, and confirmation gates for destructive operations. Real geoprocessing requires local Windows + licensed ArcGIS Pro; cloud/container runners support discovery, installation metadata, and health_check pipeline introspection without ArcGIS.
Overview

PyPI - Version PyPI - Downloads Python 3.11+ License Tools arcgis-mcp-bridge MCP server smithery badge

arcgis-mcp-bridge

100 declarative geoprocessing tools. Two isolated processes. One security floor.

A secure, local-first, asynchronous MCP server exposing ArcGIS Pro's ArcPy engine to Claude Desktop and other MCP hosts over stdio JSON-RPC.

Technical write-up: https://dev.to/muend/building-a-secure-mcp-bridge-for-arcgis-pro-and-arcpy-511g

📦 Installation

Install the official release of arcgis-mcp-bridge directly from PyPI:

# Traditional installation
pip install arcgis-mcp-bridge
# Modern, lightning-fast alternative
uv pip install arcgis-mcp-bridge
Catalog100 tools · 10 verticals
Tests81 unit tests · 81/81 passing · arcpy mocked
Static analysisRuff clean · Mypy strict clean
TransportJSON-RPC 2.0 over stdio
LicenseApache-2.0

Why arcgis-mcp-bridge?

Featurearcgis-mcp-bridgegeo2004/MCP-ArcGISPronicogis (C#/.NET)
Tools100~15~10
Dependency SyncDeterministic (uv.lock)Imperative (requirements.txt)Native Nuget
Transportstdio JSON-RPCfile-based IPCNamed Pipes
Security ArchitectureDocumented PathGuard sandboxNone specified / default host accessNone specified / default host access
arcpy IsolationTwo-process architectureSingle process executionAdd-In in-process execution
CI (Offline Verification)✅ Supported❌ Not available❌ Not available
LicenseApache-2.0MITMIT

Highlight: Sketch → GIS Pipeline

Hand-drawn parcel boundary → photo → geodatabase feature class. ORB+RANSAC image registration, HSV ink segmentation, direct GDB commit. No manual digitizing required.

Demo coming soon. To preview the sketch-to-GIS pipeline:

  1. Draw a polygon on paper and photograph it.
  2. Ask Claude: "Use extract_sketch_to_gis to register this photo against my basemap and commit the result to my GDB."
  3. The feature class appears in ArcGIS Pro — no manual digitizing.

00 — Example Prompts

After health_check succeeds, talk to Claude naturally:

"Buffer all parcels in my GDB by 50 meters and save to scratch."
"List all feature classes in C:\GIS\city.gdb starting with 'road_'."
"Dissolve the neighborhoods layer by district_id."
"Run kernel density on crime_points with a 500-meter search radius."
"Calculate slope and aspect from the DEM at C:\GIS\dem.tif."
"Find the 3 nearest facilities to each incident in my network dataset."
"Check geometry on all feature classes in my GDB and repair errors."

01 — Core Architecture & Philosophy

Claude Desktop / Cursor
        |  JSON-RPC over stdio
        v
Layer A · MCP Protocol Host
        |  NDJSON subprocess bridge
        v
Layer B · ArcPy Worker
        |
        v
ArcGIS Pro / ArcPy Runtime

Layer A — Async Event-Driven Server (arcgis_mcp/server.py). FastMCP on the bridge interpreter. Owns the stdio channel, validates every request against frozen Pydantic v2 contracts, dispatches work via asyncio.create_subprocess_exec — the event loop never blocks on a geoprocessing call and never holds a thread lock. Layer A contains zero module-level arcpy or cv2 imports (verified by grep in the audit gate); it cannot crash on Esri's native code because it never touches it.

Layer B — Subprocess ArcPy Isolation Worker (arcgis_mcp/worker.py). Spawned per job on the licensed ArcGIS Pro interpreter (ARCPY_PYTHON_PATH). The only place import arcpy is legal; cv2 loads lazily inside the one vision tool that needs it. Worker stdout is rebound to stderr at startup — the single sanctioned stdout write is the final NDJSON result frame, so native ArcObjects chatter can never corrupt the JSON-RPC channel. A native crash terminates the worker, not the server: the parent converts a non-zero exit into a structured error frame.

Declarative registry (arcgis_mcp/registry.py). Each tool is one ToolSpec(name, category, description, input_model, worker_fn, destructive). One generic proxy factory materializes all 100 MCP endpoints in Layer A; one generic run_tool dispatcher serves them in Layer B. Adding tool #101 touches two files — never the runtime loops.

Every failure crossing the process boundary is classified: validation · security · license · geoprocessing (with the full arcpy.GetMessages() stack) · internal.


02 — The 100-Tool Census Matrix

#VerticalToolsKey capabilities
1map_layer_management10.aprx maps, layer order/visibility/symbology, camera, save
2data_management22FC/GDB lifecycle, fields, Describe, Excel/GeoJSON/CSV exchange
3geometry_analysis23Overlays, dissolve/merge, selections, joins, proximity, fishnet
4coordinate_reference_projection4WKID-driven define/project for vector + raster, CRS lookup
5raster_operations15Map algebra, zonal stats, DEM slope/aspect/hillshade, hydrology
6vision_analytics1Sketch-to-GIS: ORB+RANSAC registration → HSV ink → GDB commit
7export_layout9PDF/PNG plots, DPI control, map frames, text/legend, page size
8editing_topology7Repair/check geometry, append, dedupe, diff, topology validation
9network_analysis4Service areas, routing, OD cost matrix, closest facility
10spatial_statistics5Mean center, ellipse, kernel density, Gi* hot spots, Moran's I
Total100

Esri extension licenses (Spatial, Network) are checked out through one shared context manager and checked back in inside finally — a crash can never leave a seat locked. Unavailable licenses return a structured frame, not a process drop.

Destructive Mutation Safety Floor

Ten state-mutating tools refuse to run without an explicit confirm: true payload token. The gate fires in the dispatcher before the 10–30 s arcpy import is paid, and the registry refuses to even register a destructive spec whose contract lacks a confirm field:

append_features        calculate_field        define_projection
delete_dataset         delete_field           delete_identical
extract_sketch_to_gis  near_analysis          remove_layer_from_map
repair_geometry

calculate_field carries an additional expression-channel floor: the default expression_type is ARCADE (Esri's sandboxed expression language), and PYTHON3 — which executes code inside the worker — is rejected at the Layer-A contract boundary unless confirm: true is explicitly supplied. raster_calculator expressions are constrained to a pure map-algebra grammar (identifiers, numbers, operators; no quotes, no dunder access) by a contract validator.


03 — Automated Quality Gate & Testing

Scope, stated plainly: the automated gate currently consists of 81 unit tests spanning the PathGuard boundary, the Pydantic contracts, the generic registry path-guard and registration invariants, the worker's error-boundary mapping, and Settings environment validation. It exercises the catalog's structural contracts and every security-critical seam — it does not claim multi-scenario validation of the 100 geoprocessing tools themselves, which execute against a licensed ArcGIS runtime that no CI runner has.

In-memory test architecture. tests/conftest.py injects MagicMock proxies into sys.modules["arcpy"] and sys.modules["arcpy.sa"] (with CheckExtension answering "Available") before any package import resolves. The entire suite executes in well under a second, with no ArcGIS installation, no license checkout, and no Esri runtime — locally and in CI identically.

Static analysis. Ruff enforces canonical formatting plus E/W/F/I/B/RUF at 88 columns against a py311 floor. Mypy runs strict = true with the Pydantic plugin across all 31 source files.

make format          # ruff format + import sorting (mutates)
make lint            # ruff check, mutates nothing
make type-check      # mypy --strict over arcgis_mcp/
make security-audit  # live registry inspection: path roles + confirm gates
make verify-all      # lint + type-check + security-audit, one gate
python -m pytest     # 81/81

04 — Security Framework (PathGuard Sandbox)

Every filesystem argument in every contract declares its role — "read", "write", or "read_list" — in the model's path_fields mapping. One shared enforcement function applies those declarations in both processes: Layer A pre-checks before a worker is ever spawned; Layer B re-validates because it never trusts its parent.

Two boundary controls:

  • validate_read(raw: str) — fully resolves the path (symlinks, .., relative segments collapsed before any comparison) and requires containment inside a configured allowed_roots directory. Existence is enforced via a deepest-existing-prefix strategy: the targeted path or its filesystem-resolvable geodatabase prefix must exist. This makes GDB-internal datasets (…\city.gdb\roads) first-class — the .gdb container is validated on the filesystem, while the logical tail is constrained to plain dataset names only arcpy can resolve.
  • validate_write(raw: str, *, overwrite: bool) — same resolution and containment, plus ArcGIS-legal dataset naming and overwrite discipline: an existing target is never replaced unless the request explicitly sets overwrite: true.

Any escape pattern — traversal sequences, UNC shares, NUL bytes, reserved device names, out-of-root targets — raises PathSecurityError immediately: the request is answered with a structured security frame and no subprocess is ever orchestrated for it.


05 — Environment Variables

VariableRequiredPurpose
ARCPY_PYTHON_PATHyesLayer B interpreter: licensed arcpy and Pydantic resolvable (use arcgis-mcp-env)
ARCGIS_MCP_ALLOWED_ROOTSno;-separated PathGuard boundary roots; defaults to ~/Documents/ArcGIS/Projects if unset
ARCGIS_MCP_SCRATCH_GDBnoDefault output workspace; must already exist (startup fails fast if missing)
ARCGIS_MCP_LOG_FILE / _LOG_LEVEL / _TOOL_TIMEOUTnoLogging + per-job ceiling
ARCGIS_MCP_MAX_WORKERSnoConcurrent arcpy worker ceiling (default 2) — protects license seats and RAM

Claude Desktop Configuration

ARCPY_PYTHON_PATH is required — it is the licensed worker interpreter reported by arcgis-mcp-setup.

{
  "mcpServers": {
    "arcgis-mcp-bridge": {
      "command": "arcgis-mcp-server",
      "env": {
        "ARCPY_PYTHON_PATH": "C:\\...\\envs\\arcgis-mcp-env\\python.exe",
        "ARCGIS_MCP_ALLOWED_ROOTS": "C:\\GIS\\Data;C:\\Workspace",
        "ARCGIS_MCP_MAX_WORKERS": "2"
      }
    }
  }
}

After restart, call health_check first — it proves the full server→worker pipeline without importing arcpy.


06 — Compatibility

ArcGIS ProPython (arcgispro-py3)Status
3.13.9✅ Tested
3.23.9✅ Tested
3.33.11✅ Tested — reference platform
3.43.11⚠ Community-reported, not CI-verified

Windows only. ArcPy is Windows-exclusive. Layer A runs on any platform for development (MagicMock injection), but Layer B requires a licensed ArcGIS Pro installation on Windows.


Apache License 2.0. See LICENSE.

Server Config

{
  "mcpServers": {
    "arcgis-mcp-bridge": {
      "command": "arcgis-mcp-server",
      "env": {
        "ARCPY_PYTHON_PATH": "C:\\path\\to\\arcgis-mcp-env\\python.exe",
        "ARCGIS_MCP_ALLOWED_ROOTS": "C:\\GIS\\Data",
        "ARCGIS_MCP_MAX_WORKERS": "2"
      }
    }
  }
}
Project Info
Created At
4 days ago
Updated At
4 days ago
Author Name
muend
Star
-
Language
-
License
-
Category

Recommend Servers

View All