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,writeandeditwork on text files on the Windows machine.executeruns a.pyfile, or a shell command, and returnsexit_code,stdoutandstderr.- 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.
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.
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.
| Result | Value |
|---|---|
| Wellbores with water depth > 350 m, within 5 statute miles of an in-service pipeline | 167 |
| Wells that hold these wellbores | 125 |
| Mean water depth | 405.81 m |
| Median water depth | 370.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.
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 arcpyput 680 KB of "Windows fatal exception" text instderr, on a good run. arcpy handles these exceptions itself, butfaulthandlerreports each one. Nowfaulthandlerwrites 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 withO_BINARY. echo cafécame back ascaf�.echouses the code page of thecmdthat started it, sochcp 65001on the same line had no effect. The server now runschcpin onecmdand the command in a secondcmd.- 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.1and 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