mcp-rwee: Four Tools, and arcpy Runs on Another Machine

In my post arcpi: Not Everything Needs an MCP to Be Agentic, I let an agent call the ArcGIS REST API directly, with no MCP server in front of it. This time I have the opposite problem. My data and arcpy are on a Windows machine with ArcGIS Pro. Claude Code runs on my Mac. arcpy has no REST API, and it does not run on a Mac. So here, a small MCP server earns its keep.

The server is mcp-rwee. The name is its tool list: read, write, edit and execute. The client writes a Python script on the Windows machine, runs it there and answers from the printed output. In this post, I explain how it works, what one real question looked like, and what Windows taught me that macOS did not.

Four tools and a prompt file

The server has exactly four tools and one prompt. There is no buffer tool, no select_by_location tool and no list_layers tool. arcpy already has all of these. The client writes the script that calls them.

  • read, write and edit work on text files on the Windows machine.
  • execute runs a .py file, or a shell command, and returns exit_code, stdout and stderr.
  • The prompt file gives the client its task. For arcpy, it tells the client to write a script, run it, read the error if there is one, correct the script and run it again.

This is the same idea as Code Mode, which Philipp Schmid describes in Why Agents Are Soon Only Going to Have 1 Tool. A script can read thousands of features, and the model sees only what the script prints. The server keeps the last 100,000 bytes of that output. One difference is important: the script does not run in a sandbox. It runs with the rights of the Windows user that started the server. I come back to this below.

Two processes, not one

arcpy is native code. A crash in native code does not raise a Python exception, so a try statement cannot stop it. Only a process boundary can stop it. For this reason, the server and the scripts run in two different processes.

The Claude client sends tool calls with a token to server.py on the Windows machine. server.py sends jobs to worker.py through pipes. A dashed red line between them marks where a crash stops. The worker writes output to temp files, and the server reads the end of each file.
The two processes. A crash in the worker stops at the red line. The server stays up.

server.py is a FastMCP HTTP server. It checks the token and does the file tools. worker.py runs each script on its main thread with runpy. The worker is standard library only, so FastMCP and its dependencies do not load beside arcpy.

The worker stays alive between scripts, so arcpy stays imported. On my test VM, the first script took 4.51 seconds, and most of that was import arcpy. The same script again took 0.02 seconds. After a crash or a timeout, the server stops the worker, and the next script starts a new one.

Script output does not go through the job pipe. The worker sends file descriptors 1 and 2 to a temp file for each run. Output from native code and from child processes goes there too, and a script cannot corrupt the channel between the processes. The server reads only the end of each file, so a script that prints without end cannot fill the server's memory.

Errors are part of the loop

Most first scripts fail. A dataset name is wrong, or an argument has the wrong type. That is fine, because the error comes back as text.

Sequence diagram. The client writes test.py and executes it. The worker returns exit code 1 with ERROR 000732, dataset does not exist. The client edits the script and executes it again. The worker returns exit code 0, and the client answers from stdout.
One question with one correction. The client reads the traceback, edits the script and runs it again.

A Python exception gives exit_code 1 and the traceback in stderr, and the worker stays alive. A native crash gives crashed true, the output so far and the Python stack. A timeout gives timed_out true. In each case, the client gets enough text to decide what to do next.

A real question

I gave Claude a real ArcGIS Pro project, NorthSea.aprx, and this question: calculate the number of wells with depth greater than 350 m and within 5 miles of an in-service pipeline, and return the statistics. The project is on the Windows machine. The Mac has no copy. (The Claude Code session started before I registered the server, so it called the same four tools through a small FastMCP client. The server got the same calls.)

Claude made five runs of four scripts. The first script found the maps, layers, fields and coordinate systems. The second script found that water_depth is the only depth field. The third script calculated the answer with three methods: SelectLayerByLocation with a geodesic distance, GenerateNearTable with a geodesic distance, and a NumPy distance to densified pipeline segments. The NumPy method failed with ValueError: Invalid input for distance, expecting float. The traceback came back, and the server continued to run. The fourth run used the same script with the densify call corrected, and all three methods selected the same 167 object IDs.

Map in ED50 degrees, latitude 52 to 72 north. 167 selected wellbores are orange dots near blue in-service pipelines. Other wellbores deeper than 350 m are grey circles.
The 167 selected wellbores (orange), the other wellbores in water deeper than 350 m (grey), and the pipelines.
ResultValue
Wellbores with water depth > 350 m, within 5 statute miles of an in-service pipeline167
Wells that hold these wellbores125
Mean water depth405.81 m
Median water depth370.00 m

Now the part that I like best. The last run checked the answer instead of trusting it. The first output showed a gap of 10,970.80 m between two of the methods. That gap is large only for wellbores that are far from a pipeline, because the NumPy method uses a flat local scale. Under 10 km, the largest gap is 6.24 m. The two nearest wellbores outside the limit are 10.48 m past it, so the gap does not change the count.

The checks also found the limits of the answer. water_depth is the depth of the water, not the depth of the wellbore in the rock. 2,258 wellbores have no depth value, and most of them are Dutch or Danish. 119 of these are within the distance limit. The script cannot test them, so they are not in the 167.

Getting to the Windows machine

The server listens on 127.0.0.1 only. A non-local host without a TLS certificate is refused, because plain HTTP would expose the token. From the Mac, I use an SSH tunnel.

On the Mac, the MCP client sends HTTP with the token to ssh -L on 127.0.0.1:8765. An encrypted SSH tunnel on port 22 goes to sshd on the Windows machine. sshd forwards to server.py on 127.0.0.1:8765, which sends jobs to worker.py, which reads NorthSea.gdb.
The path of one tool call. Only the red line goes across the network.

The script remote.sh does the setup in one command. It starts the server over SSH with a new token, opens the tunnel and registers the server in Claude Code for the current folder. The token goes to the server through SSH standard input, so it is not in a remote file or a remote command line. remote.sh stop removes all three.

What Windows taught me

All tests passed on macOS. Then I ran them on Windows with real arcpy, and Windows found faults that macOS did not show.

  • Each first import arcpy put 680 KB of "Windows fatal exception" text in stderr, on a good run. arcpy handles these exceptions itself, but faulthandler reports each one. Now faulthandler writes to its own file, and the server reads that file only after a crash.
  • Output lines ended with \r\r\n. The capture file was in text mode, so Windows changed each newline a second time. The worker now opens the files with O_BINARY.
  • echo cafĂ© came back as caf�. echo uses the code page of the cmd that started it, so chcp 65001 on the same line had no effect. The server now runs chcp in one cmd and the command in a second cmd.
  • After a hard stop of the server, the worker continued to run. A hard stop does not run exit handlers. Now a thread in the worker reads the job pipe. When the pipe closes, the worker stops itself and its child processes.

The test suite has 97 tests. All 97 passed on a Windows 11 on Arm VM and on a Windows machine with x64 hardware, both with ArcGIS Pro 3.7.3.

The honest part

execute is remote code execution, by design. A person who has the token can run code as the server's Windows user. The worker protects the server against a crash. It is not a security boundary. My advice is short:

  • Keep the server on 127.0.0.1 and use the SSH tunnel.
  • Use a copy of important data, because each script can change data.
  • Stop the server when you finish. Closing the SSH session does not stop it, and its token continues to work.

A dedicated low-privilege Windows user is the next step, but I have not tested it yet. The tunnel stayed open through a quiet period of 90 seconds. I have not tested a longer quiet period behind a proxy.

Try it

The code is on GitHub at mraad/mcp-rwee, under the MIT license. The README has the quick start. The manual in docs/index.html has the install procedure, every test result and the complete NorthSea run. Download it and open it in a browser.

arcpi showed that an agent does not always need an MCP server. mcp-rwee shows when it does: the library is on another machine, and it has no API. Four tools are enough, because the client writes the rest. More to come.

Comments

Popular posts from this blog

How to use Esri Flex API on Android and iPhone

Augmented Reality, iPhone and ArcGIS Server

Weighted Map Feature Clustering With Attributes