Summary
macos-mcp exhibits a steady, linear physical footprint leak when AX-tree-reading tools are exercised in long-running sessions. Growth rate is roughly 10–17 MB/min of activity, and the process never releases memory after the calling client stops invoking tools. On a 32 GB host left running over a couple of days, the process climbed to ~16.5 GB physical footprint and contributed to a system-level OOM. With an external watchdog reaping the process at 400 MB, we observe repeated respawns under normal use; during active AX-tree-read loops, the process can reach the threshold in roughly 18–22 minutes.
The evidence points most strongly at AX observer references (AXObserverCreate / AXObserverAddNotification) may be created during these calls and not consistently paired with AXObserverRemoveNotification / explicit release, allowing AXUIElement retentions to accumulate inside pyobjc.
Environment
- macos-mcp
v2.14.4 (manifest 0.1.0)
- macOS Tahoe 26.x on Apple silicon
- Hosted as a local MCP server by an MCP client (Claude Desktop), which respawns the process on transport close
Symptom timeline
Watchdog-driven respawn log over a 48h window. The watchdog sends SIGTERM when the process’s footprint crosses 400 MB.
2026-05-25T05:45:09.633Z transport closed (footprint = 430 MB at kill)
2026-05-26T05:36:30.930Z transport closed (footprint = 432 MB at kill)
2026-05-26T06:46:36.707Z transport closed (footprint = 444 MB at kill)
Each fresh process starts at ~50–100 MB. Footprint climbs roughly linearly with tool-call activity. After calls stop, footprint does not decay, which is the tell that the retained objects are not just transient working memory.
Why footprint and not RSS. An earlier version of our watchdog used ps -o rss= as its threshold metric and silently failed against this leak class: the kernel was aggressively swapping/compressing the leaked dirty pages, so ps-reported RSS oscillated in the 33–301 MB range for hours while the actual physical footprint grew to 16.5 GB. We switched to footprint -f bytes <pid> (resident + swapped + compressed). Any generic memory monitor reading RSS will under-count this leak; footprint is the metric that surfaces it cleanly.
Suspected leak site
A sample(1) call-graph capture taken during steady-state growth pointed at:
macos_mcp/ax/events.py:363 _update_observers
with frames into:
_AXObserverAddNotificationAndCheckRemote
AXObserverCreate
- a pyobjc retention loop around
AXUIElement references
The shape is consistent with observers being attached during AX-tree reads (snapshot / scrape / similar) and—we suspect—the matching AXObserverRemoveNotification (or CFRelease / pyobjc __del__) not running when the wrapping Python object goes out of scope. Once an observer is attached to an AXUIElement, the element gets retained by the AX framework until the observer is explicitly removed; GC’ing the Python wrapper alone won’t release it.
Reproduction
- Connect
macos-mcp to any MCP client.
- Call Snapshot repeatedly at a steady cadence for about 20 minutes against Finder and Safari. In our run, this produced roughly 10–17 MB/min footprint growth.
- Loop over
Snapshot (or any tool that walks the accessibility tree) against a couple of running apps — e.g. Finder + Safari — for ~20 minutes.
- Watch footprint:
footprint -f bytes <pid>
It should climb roughly linearly with each call (~10–17 MB/min on our box).
- Stop calling tools. Footprint does not drop — suspected observers remain attached, and AXUIElement retentions appear to stay live.
A single 20-minute loop is enough to reproduce the growth; the OOM-class outcome only shows up if nothing reaps the process.
What we’re asking
- Confirm whether
_update_observers in ax/events.py is the right site, or point at the actual offender if the sample misled us.
- Patch — most likely a matching
AXObserverRemoveNotification in the dealloc path for whatever wraps the observer, or a weakref / explicit-release pattern so AXUIElement retentions get freed when the Python wrapper drops. If observers are intentionally long-lived, a bounded cache with eviction would also solve it.
- Regression-test — please add a small long-run AX-tree-read test or diagnostic that verifies observer count / footprint remains bounded after repeated Snapshot-style calls.
- Release — once patched, please cut a version that downstream MCP-client extension auto-updaters pick up. Happy to validate the candidate on our environment before release if that’s useful — the repro above produces a clean signal in under 30 minutes.
Notes
The leak is severe in long-running sessions, but the surface area looks small — possibly a missing release/removal path. Filing this with as much detail as we have so it’s cheap to triage on your side.
Supporting artifacts
2026-05-20 macos-mcp memory leak sample.txt — full sample(1) output of the leaking process captured at peak (16.5 GB physical footprint). Contains the call graph with the _AXObserverAddNotificationAndCheckRemote / AXObserverCreate hot path that points at events.py:363.
2026-05-20 macos-mcp vmmap summary.txt — vmmap -summary of the same process. Relevant region table: MALLOC_SMALL at 16.4 GB virtual / 16.3 GB swapped across 4,187 regions, with ~49 million live allocations in the default malloc zone. Consistent with many small allocations remaining live; per-call attribution would need allocation-stack evidence.
2026-05-26 claude desktop mcp-server-macos log excerpt.txt — last 48h of transport closed lines from the MCP client’s per-server log; time-correlates with the watchdog kill events.
2026-05-21 macos-mcp watchdog v2 deployment verification.txt — the v1→v2 diff of our watchdog script plus real KILL pid=… footprint=… log lines proving the metric switch from ps -o rss= to footprint -f bytes works. Includes the rationale for why RSS-based monitoring missed this leak.
2026-05-26 cursortouch macos-mcp manifest snapshot.json.txt — version metadata for the installed extension at the time of report
cc: @claude-dashnet
2026-05-26.claude.desktop.mcp-server-macos.log.excerpt.txt
2026-05-21.macos-mcp.watchdog.v2.deployment.verification.txt
2026-05-20.macos-mcp.vmmap.summary.txt
2026-05-20.macos-mcp.memory.leak.sample.txt
2026-05-26.cursortouch.macos-mcp.manifest.snapshot.json.txt
Summary
macos-mcpexhibits a steady, linear physical footprint leak when AX-tree-reading tools are exercised in long-running sessions. Growth rate is roughly 10–17 MB/min of activity, and the process never releases memory after the calling client stops invoking tools. On a 32 GB host left running over a couple of days, the process climbed to ~16.5 GB physical footprint and contributed to a system-level OOM. With an external watchdog reaping the process at 400 MB, we observe repeated respawns under normal use; during active AX-tree-read loops, the process can reach the threshold in roughly 18–22 minutes.The evidence points most strongly at AX observer references (
AXObserverCreate/AXObserverAddNotification) may be created during these calls and not consistently paired withAXObserverRemoveNotification/ explicit release, allowingAXUIElementretentions to accumulate inside pyobjc.Environment
v2.14.4(manifest0.1.0)Symptom timeline
Watchdog-driven respawn log over a 48h window. The watchdog sends
SIGTERMwhen the process’sfootprintcrosses 400 MB.Each fresh process starts at ~50–100 MB. Footprint climbs roughly linearly with tool-call activity. After calls stop, footprint does not decay, which is the tell that the retained objects are not just transient working memory.
Why footprint and not RSS. An earlier version of our watchdog used
ps -o rss=as its threshold metric and silently failed against this leak class: the kernel was aggressively swapping/compressing the leaked dirty pages, so ps-reported RSS oscillated in the 33–301 MB range for hours while the actual physical footprint grew to 16.5 GB. We switched tofootprint -f bytes <pid>(resident + swapped + compressed). Any generic memory monitor reading RSS will under-count this leak;footprintis the metric that surfaces it cleanly.Suspected leak site
A sample(1) call-graph capture taken during steady-state growth pointed at:
with frames into:
_AXObserverAddNotificationAndCheckRemoteAXObserverCreateAXUIElementreferencesThe shape is consistent with observers being attached during AX-tree reads (snapshot / scrape / similar) and—we suspect—the matching
AXObserverRemoveNotification(orCFRelease/ pyobjc__del__) not running when the wrapping Python object goes out of scope. Once an observer is attached to anAXUIElement, the element gets retained by the AX framework until the observer is explicitly removed; GC’ing the Python wrapper alone won’t release it.Reproduction
macos-mcpto any MCP client.Snapshot(or any tool that walks the accessibility tree) against a couple of running apps — e.g. Finder + Safari — for ~20 minutes.footprint -f bytes <pid>It should climb roughly linearly with each call (~10–17 MB/min on our box).
A single 20-minute loop is enough to reproduce the growth; the OOM-class outcome only shows up if nothing reaps the process.
What we’re asking
_update_observersinax/events.pyis the right site, or point at the actual offender if the sample misled us.AXObserverRemoveNotificationin the dealloc path for whatever wraps the observer, or aweakref/ explicit-release pattern soAXUIElementretentions get freed when the Python wrapper drops. If observers are intentionally long-lived, a bounded cache with eviction would also solve it.Notes
The leak is severe in long-running sessions, but the surface area looks small — possibly a missing release/removal path. Filing this with as much detail as we have so it’s cheap to triage on your side.
Supporting artifacts
2026-05-20 macos-mcp memory leak sample.txt— fullsample(1)output of the leaking process captured at peak (16.5 GB physical footprint). Contains the call graph with the_AXObserverAddNotificationAndCheckRemote/AXObserverCreatehot path that points atevents.py:363.2026-05-20 macos-mcp vmmap summary.txt—vmmap -summaryof the same process. Relevant region table:MALLOC_SMALLat 16.4 GB virtual / 16.3 GB swapped across 4,187 regions, with ~49 million live allocations in the default malloc zone. Consistent with many small allocations remaining live; per-call attribution would need allocation-stack evidence.2026-05-26 claude desktop mcp-server-macos log excerpt.txt— last 48h oftransport closedlines from the MCP client’s per-server log; time-correlates with the watchdog kill events.2026-05-21 macos-mcp watchdog v2 deployment verification.txt— the v1→v2 diff of our watchdog script plus realKILL pid=… footprint=…log lines proving the metric switch fromps -o rss=tofootprint -f bytesworks. Includes the rationale for why RSS-based monitoring missed this leak.2026-05-26 cursortouch macos-mcp manifest snapshot.json.txt— version metadata for the installed extension at the time of reportcc: @claude-dashnet
2026-05-26.claude.desktop.mcp-server-macos.log.excerpt.txt
2026-05-21.macos-mcp.watchdog.v2.deployment.verification.txt
2026-05-20.macos-mcp.vmmap.summary.txt
2026-05-20.macos-mcp.memory.leak.sample.txt
2026-05-26.cursortouch.macos-mcp.manifest.snapshot.json.txt