Skip to content

Latest commit

 

History

History
124 lines (88 loc) · 4.68 KB

File metadata and controls

124 lines (88 loc) · 4.68 KB

macOS Local Network Permission for ssh-mcp-server

Symptom

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.

Root cause

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.

Previous (too broad) fix

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.

Scoped fix: give the server its own binary identity

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.

Steps performed

  1. 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
  2. Homebrew's node is a thin launcher that loads libnode.141.dylib via an @loader_path/../lib rpath, 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_tool invalidates the code signature, so re-sign ad-hoc:

    codesign --force --sign - ~/bin/ssh-mcp-server
  3. Verify the copy still runs:

    ~/bin/ssh-mcp-server --version
  4. 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>
  5. Reload the agent:

    launchctl unload ~/Library/LaunchAgents/com.idletoaster.ssh-mcp-server.plist
    launchctl load   ~/Library/LaunchAgents/com.idletoaster.ssh-mcp-server.plist
  6. 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
  7. 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.

  8. Open System Settings → Privacy & Security → Local Network. Confirm ssh-mcp-server is listed and enabled. The broad node entry can be toggled OFF (use the MCP to test a LAN host like hass afterward to verify isolation).

Maintenance

  • 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 or install_name_tool -rpath <old> <new>.
  • Ad-hoc signing is sufficient for user-scope LaunchAgents; no notarization needed.

Diagnostic quick reference

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

Files touched

  • ~/Library/LaunchAgents/com.idletoaster.ssh-mcp-server.plist (ProgramArguments[0])
  • ~/bin/ssh-mcp-server (new, ad-hoc signed copy of node)