arcgis-mcp-bridge

by muend

Not rated
GitHub

About

Secure, local-first MCP server exposing ArcGIS Pro's ArcPy engine over stdio JSON-RPC.

Details

Author
muend
Categories
Other

Option 1: Unified PyPI Environment — Recommended on Windows

Use the same interpreter for the MCP server and ArcPy worker:

{ "mcpServers": { "arcgis-mcp-bridge": { "command": "C:\\...\\envs\\arcgis-mcp-env\\python.exe", "args": [ "-m", "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_SCRATCH_GDB": "C:\\GIS\\Data\\scratch.gdb", "ARCGIS_MCP_MAX_WORKERS": "2" } } } }

ThecommandandARCPY_PYTHON_PATHvalues should be identical in this configuration. Use thepython_exevalue returned by the setup command.

Option 2: Local Git Development Environment

Use the repository.venvfor Layer A and the provisionedarcgis-mcp-envfor Layer B:

{ "mcpServers": { "arcgis-mcp-bridge": { "command": "C:\\path\\to\\arcgis-mcp-bridge\\.venv\\Scripts\\python.exe", "args": [ "-m", "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_SCRATCH_GDB": "C:\\GIS\\Data\\scratch.gdb", "ARCGIS_MCP_MAX_WORKERS": "2" } } } }

This split-environment configuration assumes that the worker was provisioned from the repository with one of the Path B setup commands above.PYTHONPATHis not required whenuv sync --lockedhas installed the project into the repository.venv.

A globally resolvedarcgis-mcp-servercommand can work, but it creates a split-environment deployment. It is not recommended for first-time Windows setup unless the worker environment has been provisioned and verified separately.

After restarting the MCP host, callhealth_checkfirst. It verifies the server-to-worker IPC path and reports the selected worker interpreter without importing ArcPy. Then run a read-only ArcGIS tool or the ArcPy preflight command above to validate the licensed runtime.

If the MCP server starts but every ArcGIS tool fails, inspect the server log for the worker traceback. Common environment-related causes include:

ModuleNotFoundError: No module named 'arcgis_mcp' ModuleNotFoundError: No module named 'pydantic_core._pydantic_core'

These errors usually mean that the worker is using a different Python installation, the bridge was not installed into the worker interpreter, or the worker contains an incomplete or incompatible Pydantic installation. The recommended fix is the unified-environment configuration documented above.

For the recommended unified configuration, confirm that both values are identical:

"command": "C:\\...\\envs\\arcgis-mcp-env\\python.exe"
"ARCPY_PYTHON_PATH": "C:\\...\\envs\\arcgis-mcp-env\\python.exe"

arcgis-mcp-bridgerequires Windows, a licensed ArcGIS Pro installation, and Python 3.11 or newer for the bridge package.

Install the bootstrap package usingonepackage manager:

# Option A — pip py -m pip install --upgrade arcgis-mcp-bridge # Option B — uv uv pip install --upgrade arcgis-mcp-bridge

Then clone ArcGIS Pro's Python environment:

# The final JSON report contains the target python_exe path. py -m arcgis_mcp.setup_env

If the installed console command is available onPATH,arcgis-mcp-setupis equivalent topy -m arcgis_mcp.setup_env.

Important for Windows systems with multiple Python installations:the setup is not complete untilarcgis-mcp-bridgeis installed into the reportedarcgis-mcp-env\python.exe. Use that same interpreter for both the MCP servercommandandARCPY_PYTHON_PATH. This prevents worker failures caused by packages or native extensions being loaded from another Python environment.

See05 — Installationfor the complete setup and configuration.

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:Building a Secure MCP Bridge for ArcGIS Pro and ArcPy

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:
- Draw a polygon on paper and photograph it.
- Ask Claude:"Use extract_sketch_to_gis to register this photo against my basemap and commit the result to my GDB."
- The feature class appears in ArcGIS Pro — no manual digitizing.

Afterhealth_checksucceeds, 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."
flowchart TD A[Claude Desktop / Cursor] -->|JSON-RPC over stdio| B[Layer A · MCP Protocol Host] B -->|NDJSON subprocess bridge| C[Layer B · ArcPy Worker] C --> D[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 viaasyncio.create_subprocess_exec— the event loop never blocks on a geoprocessing call and never holds a thread lock. Layer A containszero module-levelarcpyorcv2imports(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 placeimport arcpyis legal;cv2loads 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 oneToolSpec(name, category, description, input_model, worker_fn, destructive). One generic proxy factory materializes all 100 catalog MCP endpoints in Layer A; one genericrun_tooldispatcher serves them in Layer B. The catalog is exposed alongside three core endpoints:health_check,list_layers, andexecute_spatial_tool. Adding catalog tool #101 touches two files — never the runtime loops.

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

Esri extension licenses (Spatial,Network) are managed through one shared context manager and checked back in viafinallyon normal Python exception paths. Worker-process isolation contains native failures to the current job, while unavailable licenses return a structured error frame instead of terminating the MCP server.

Ten state-mutating tools refuse to run without an explicitconfirm: truepayload token. The gate fires in the dispatcherbeforethe 10–30 sarcpyimport is paid, and the registry refuses to even register a destructive spec whose contract lacks aconfirmfield:

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_fieldcarries an additional expression-channel floor: the defaultexpression_typeisARCADE(Esri's sandboxed expression language), andPYTHON3— which executes code inside the worker — is rejected at the Layer-A contract boundary unlessconfirm: trueis explicitly supplied.raster_calculatorexpressions 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

Licensed-runtime evidence is reported separately in thebenchmarks/method card. Its committed result uses a real ArcGIS Pro worker and a dedicated scratch GDB; it is not pooled with the mocked unit-test count or presented as validation of all 100 geoprocessing tools.

Scope, stated plainly:the automated gate currently consists of86 unit testsspanning the PathGuard boundary, the Pydantic contracts, the generic registry path-guard and registration invariants, the worker's error-boundary mapping, andSettingsenvironment 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.pyinjectsMagicMockproxies intosys.modules["arcpy"]andsys.modules["arcpy.sa"](withCheckExtensionanswering"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.

- tests/test_security.py&tests/test_pathguard.py— the PathGuard boundary firewall, exercised against real directories via pytest'stmp_pathfixture: valid reads/writes inside the sandbox pass; traversal (..-segments), UNC, relative, NUL-byte, reserved-device, over-length and out-of-root paths are rejected; write discipline (ArcGIS dataset-name rules, overwrite opt-in) is enforced.
- tests/test_contracts.py— Pydantic contract enforcement: per-tool parameter specs, cross-field validators,frozen/extra="forbid", and theok-xor-errorinvariant on the IPC envelope.
- tests/test_registry.py&tests/test_registry_guard.py— registry stream integrity plus genericapply_path_guardenforcement andregisterinvariants — every schema must be aToolInputsubclass, everypath_fieldsentry must reference a valid role, duplicate names are rejected, and every destructive spec must carry itsconfirmgate.
- tests/test_worker.pyprocess_frameerror-boundary mapping: every failure class (validation, security, license, geoprocessing, internal) maps to its distinctWorkerError.kind.
- tests/test_config.pySettings.from_environmentvalidation: required variables, directory/file checks, integer bounds, and the fail-fast on a missing scratch geodatabase.

The side-effect importimport arcgis_mcp.toolsin the registry test is what populates the catalog; it is# noqa-pinned so no linter ever strips it again.

Static analysis.Ruff enforces canonical formatting plusE/W/F/I/B/RUFat 88 columns against apy311floor (code must parse on the oldest supported interpreter — Layer B). Turkish comments are first-class: the dotlessı/İare registered underallowed-confusables, so prose is configured around, never rewritten. Mypy runsstrict = truewith 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 # 86/86

04 — Security Framework (PathGuard Sandbox)

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

- validate_read(raw: str)— fully resolves the path (symlinks,.., relative segments collapsedbeforeany comparison) and requires containment inside a configuredallowed_rootsdirectory. Existence is enforced via adeepest-existing-prefixresolution strategy: the targeted path or its filesystem-resolvable geodatabase prefix must exist. This is what makes GDB-internal datasets (…\city.gdb\roads) first-class — the.gdbcontainer 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 the overwrite discipline: an existing target is never replaced unless the request explicitly setsoverwrite: true.

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

Choose the onboarding path that matches your use case.

- Windows with a licensed ArcGIS Pro installation
- Python 3.11 or newer forarcgis-mcp-bridge
- An existing writable directory forARCGIS_MCP_ALLOWED_ROOTS
- An existing file geodatabase forARCGIS_MCP_SCRATCH_GDB, unless<first allowed root>\scratch.gdbalready exists

Path A: Pure PyPI Installation — Recommended for Windows Users

This is the simplest and most reliable setup for Claude Desktop and other MCP hosts on Windows. The recommended configuration uses the samearcgis-mcp-env\python.exefor both Layer A (the MCP server) and Layer B (the ArcPy worker).

Choose one bootstrap installation command:

# Option A — pip py -m pip install --upgrade arcgis-mcp-bridge # Option B — uv uv pip install --upgrade arcgis-mcp-bridge

Then clone ArcGIS Pro's Python environment:

# The final JSON report contains the target python_exe path. py -m arcgis_mcp.setup_env

If the installed console command is available onPATH,arcgis-mcp-setupis equivalent topy -m arcgis_mcp.setup_env.

Copy thepython_exevalue from the JSON report and assign it below:

$ArcGISMcpPython = "C:\...\envs\arcgis-mcp-env\python.exe"
# Standard installation & $ArcGISMcpPython -m pip install --upgrade arcgis-mcp-bridge # OR: include the optional OpenCV-based sketch-to-GIS extension & $ArcGISMcpPython -m pip install --upgrade "arcgis-mcp-bridge[vision]"

Do not run both commands; the second command already installs the standard package together with thevisionextra.

Use$ArcGISMcpPythonas both the MCP server interpreter andARCPY_PYTHON_PATH. This preventsarcgis_mcp, Pydantic,pydantic-core, and other native dependencies from being resolved from a different Python installation.

Path B: Git Clone & Deterministic Development — GIS Contributors

This path keeps Layer A in a hermetic development environment while running ArcPy work in a separately cloned, licensedarcgis-mcp-envworker.

# 1. Clone the repository. git clone https://github.com/muend/arcgis-mcp-bridge.git cd arcgis-mcp-bridge # 2. Create the isolated development environment. # Do not use --system-site-packages: Layer A must remain independent of arcpy. uv venv --python "C:\Program Files\ArcGIS\Pro\bin\Python\envs\arcgispro-py3\python.exe" # 3. Synchronize the committed dependency resolution. uv sync --locked
# Standard worker uv run python -m arcgis_mcp.setup_env --install-runtime-deps --project-root . # OR: worker with the optional OpenCV-based sketch-to-GIS extension uv run python -m arcgis_mcp.setup_env --with-vision --project-root .

--with-visionimplies runtime-dependency installation, so the two commands should not be run consecutively.

The setup command is idempotent, accepts--env-name(default:arcgis-mcp-env) and--dry-run, and emits a JSON report. SetARCGIS_CONDA_EXEif ArcGIS Pro'sconda.exeis not available onPATH.

The interpreter referenced byARCPY_PYTHON_PATHmust be able to import the complete worker stack:

For a first-time Windows installation, use the samearcgis-mcp-env\python.exefor the servercommandandARCPY_PYTHON_PATH. Separate server and worker environments remain supported for development, but the worker interpreter must contain its own compatible installation ofarcgis-mcp-bridgeand all runtime dependencies.

Run this preflight check before configuring the MCP host:

$ArcGISMcpPython = "C:\...\envs\arcgis-mcp-env\python.exe" & $ArcGISMcpPython -c "import sys, arcgis_mcp, pydantic, pydantic_core; print(sys.executable); print('Bridge runtime OK')" & $ArcGISMcpPython -c "import arcpy; print('ArcPy', arcpy.GetInstallInfo().get('Version'))"

ARCPY_PYTHON_PATHis required in every configuration and must point to the licensed interpreter reported by the setup command.

Replace every placeholder path below with an existing path on your machine. The scratch geodatabase must already exist.

Option 1: Unified PyPI Environment — Recommended on Windows

Use the same interpreter for the MCP server and ArcPy worker:

{ "mcpServers": { "arcgis-mcp-bridge": { "command": "C:\\...\\envs\\arcgis-mcp-env\\python.exe", "args": [ "-m", "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_SCRATCH_GDB": "C:\\GIS\\Data\\scratch.gdb", "ARCGIS_MCP_MAX_WORKERS": "2" } } } }

ThecommandandARCPY_PYTHON_PATHvalues should be identical in this configuration. Use thepython_exevalue returned by the setup command.

Option 2: Local Git Development Environment

Use the repository.venvfor Layer A and the provisionedarcgis-mcp-envfor Layer B:

{ "mcpServers": { "arcgis-mcp-bridge": { "command": "C:\\path\\to\\arcgis-mcp-bridge\\.venv\\Scripts\\python.exe", "args": [ "-m", "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_SCRATCH_GDB": "C:\\GIS\\Data\\scratch.gdb", "ARCGIS_MCP_MAX_WORKERS": "2" } } } }

This split-environment configuration assumes that the worker was provisioned from the repository with one of the Path B setup commands above.PYTHONPATHis not required whenuv sync --lockedhas installed the project into the repository.venv.

A globally resolvedarcgis-mcp-servercommand can work, but it creates a split-environment deployment. It is not recommended for first-time Windows setup unless the worker environment has been provisioned and verified separately.

After restarting the MCP host, callhealth_checkfirst. It verifies the server-to-worker IPC path and reports the selected worker interpreter without importing ArcPy. Then run a read-only ArcGIS tool or the ArcPy preflight command above to validate the licensed runtime.

If the MCP server starts but every ArcGIS tool fails, inspect the server log for the worker traceback. Common environment-related causes include:

ModuleNotFoundError: No module named 'arcgis_mcp' ModuleNotFoundError: No module named 'pydantic_core._pydantic_core'

These errors usually mean that the worker is using a different Python installation, the bridge was not installed into the worker interpreter, or the worker contains an incomplete or incompatible Pydantic installation. The recommended fix is the unified-environment configuration documented above.

For the recommended unified configuration, confirm that both values are identical:

"command": "C:\\...\\envs\\arcgis-mcp-env\\python.exe"
"ARCPY_PYTHON_PATH": "C:\\...\\envs\\arcgis-mcp-env\\python.exe"
No reviews yet — be the first

Sign in to leave a review

Use Google, GitHub, or an email account so ratings stay tied to real people.

Email sign in

No reviews posted yet.