SSH calls made through the MCP fail with:
ssh: connect to host 192.168.0.11 port 22: No route to host
...even though ssh hass from Terminal works fine, ping works, and the port is reachable.
The failures hit only hosts on the Mac's own LAN subnet (e.g. 192.168.0.x). Hosts reached through a gateway (192.168.37.x via pfSense) work, because that traffic leaves via the default route and doesn't trip the local-network check.
Since macOS Sonoma, every process that wants to talk to devices on its own LAN needs the
Local Network privacy entitlement (System Settings → Privacy & Security → Local Network).
Processes launched by launchd as LaunchAgents cannot show UI prompts, so the grant must either
be pre-existing or triggered by a foreground launch.
Denied access returns EHOSTUNREACH (rendered as "No route to host") — not "Permission denied",
which is why this is easy to misdiagnose as a networking problem.
Toggling node ON in Local Network settings works, but it grants LAN access to every Node.js
process on the Mac, not just ssh-mcp-server. That's more permission than we want.
macOS TCC attributes Local Network grants by signed binary (cdhash) and, for LaunchAgents, by the
plist's Label. By launching the MCP server via a private copy of the Node binary with its own
ad-hoc signature, we get a distinct TCC identity that appears in the Privacy UI as
ssh-mcp-server rather than node.
-
Create
~/bin/and copy the real Node binary (resolving the Homebrew symlink):mkdir -p ~/bin cp "$(readlink -f /opt/homebrew/bin/node)" ~/bin/ssh-mcp-server
-
Homebrew's
nodeis a thin launcher that loadslibnode.141.dylibvia an@loader_path/../librpath, which won't resolve from~/bin/. Add an absolute rpath to the Cellar lib dir:install_name_tool -add_rpath \ /opt/homebrew/Cellar/node/<version>/lib \ ~/bin/ssh-mcp-server
install_name_toolinvalidates the code signature, so re-sign ad-hoc:codesign --force --sign - ~/bin/ssh-mcp-server -
Verify the copy still runs:
~/bin/ssh-mcp-server --version -
Point the LaunchAgent at the new binary. Edit
~/Library/LaunchAgents/com.idletoaster.ssh-mcp-server.plist:<key>ProgramArguments</key> <array> <string>/Users/yuriy/bin/ssh-mcp-server</string> <string>/Users/yuriy/git/ssh-mcp-server/index.js</string> </array>
-
Reload the agent:
launchctl unload ~/Library/LaunchAgents/com.idletoaster.ssh-mcp-server.plist launchctl load ~/Library/LaunchAgents/com.idletoaster.ssh-mcp-server.plist
-
Confirm the running process is the renamed binary:
ps aux | grep ssh-mcp-server | grep -v grep # → /Users/yuriy/bin/ssh-mcp-server /Users/yuriy/git/ssh-mcp-server/index.js
-
Trigger an SSH call through MCP. If macOS hasn't seen this binary before, a Local Network prompt should appear for ssh-mcp-server — click Allow.
In practice, because the LaunchAgent
Label(com.idletoaster.ssh-mcp-server) is unchanged, macOS kept the pre-existing grant and no prompt was needed. -
Open System Settings → Privacy & Security → Local Network. Confirm
ssh-mcp-serveris listed and enabled. The broadnodeentry can be toggled OFF (use the MCP to test a LAN host likehassafterward to verify isolation).
- Homebrew node upgrades do NOT propagate to
~/bin/ssh-mcp-server. It's a frozen copy. When the server is upgraded (or if something breaks), re-run steps 1–3 and reload the LaunchAgent. - The hardcoded rpath (
/opt/homebrew/Cellar/node/<version>/lib) breaks when the Cellar version directory changes on upgrade. Re-copy orinstall_name_tool -rpath <old> <new>. - Ad-hoc signing is sufficient for user-scope LaunchAgents; no notarization needed.
| Symptom | Meaning |
|---|---|
ssh: connect to host X port 22: No route to host via MCP, but Terminal SSH works |
macOS Local Network privacy gate |
| Fails for 192.168.0.x, works for 192.168.37.x | Same subnet = local net check; routed = no check |
No entry for ssh-mcp-server in Privacy UI |
Binary has never attempted local network from a prompt-capable context |
| Error is "Permission denied" not "No route to host" | NOT this bug — check SSH keys / config |
~/Library/LaunchAgents/com.idletoaster.ssh-mcp-server.plist(ProgramArguments[0])~/bin/ssh-mcp-server(new, ad-hoc signed copy of node)