Claude Desktop on macOS: spawned MCP servers can't reach LAN (EHOSTUNREACH)
Filing here because no public Claude Desktop repo exists. This bug is in Claude.app (the macOS desktop GUI), not in Claude Code (the CLI). Please redirect to the right tracker if one exists internally — I just couldn't find one. Posting publicly so the next person hitting this exact failure mode can find a diagnosis. Claude Code (this repo) is unaffected on the same Mac with the same MCP config; details below.
---
Summary
On macOS, Claude Desktop launches every external MCP command through Claude.app/Contents/Helpers/disclaimer, which calls responsibility_spawnattrs_setdisclaim() on the spawned child. The disclaimed child is no longer attributable to Claude Desktop's TCC bundle id and has no bundle id of its own, so the kernel rejects any connect() to a private-range IP (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) with EHOSTUNREACH before any packet leaves the host. The macOS Local Network privacy gate never prompts for this case (raw unicast connect() doesn't trigger it — only Bonjour/multicast SDK calls do), so users have no path to grant permission via System Settings.
Net effect: any self-hosted MCP server on the user's LAN is unreachable from Claude Desktop on macOS, even when DNS resolves, the host is pingable, curl works from Terminal, and the same MCP config works in Claude Code (CLI) on the same Mac.
Environment
- Claude Desktop: 1.5354.0
- Bundle id:
com.anthropic.claudefordesktop - macOS: 15.7.5 (Sequoia)
- Node: v25.9.0 / npx: 11.12.1
- MCP server under test: a self-hosted HTTPS endpoint on the user's LAN, fronted by Caddy with
tls internal, addressed vianpx mcp-remote https://<lan-host>/mcp?... - The same MCP block works fine in Claude Code on the same Mac.
Reproduction
In ~/Library/Application Support/Claude/claude_desktop_config.json, configure any MCP server reachable only at a LAN IP, e.g.:
{
"mcpServers": {
"lan-mcp": {
"command": "/usr/local/bin/npx",
"args": ["mcp-remote", "https://<lan-host>/mcp?..."],
"env": { "NODE_TLS_REJECT_UNAUTHORIZED": "0" }
}
}
}
Launch Claude Desktop. ~/Library/Logs/Claude/mcp-server-<name>.log:
Connecting to remote server: https://<lan-host>/mcp?...
Connection error: [TypeError: fetch failed] {
[cause]: Error: connect EHOSTUNREACH <lan-ip>:443 - Local (<local-ip>:<port>)
code: 'EHOSTUNREACH', errno: -65, syscall: 'connect',
address: '<lan-ip>', port: 443
}
This happens on every launch. Concurrently from the same Mac, in Terminal:
| Probe | Result |
|---|---|
| ping <lan-host> | 0% loss, low-ms |
| curl -sk https://<lan-host>/health | HTTP 200 |
| npx -y mcp-remote 'https://<lan-host>/...' | Reaches TLS handshake |
| Same npx mcp-remote via Claude Code MCP config | ✅ Works |
Minimal isolation of the cause
Same npx invocation, same args, same shell, only differing by the disclaimer wrapper:
# Terminal: reaches TLS handshake
$ NODE_TLS_REJECT_UNAUTHORIZED=0 /usr/local/bin/npx -y mcp-remote \
'https://<lan-host>/mcp?...' --transport http-only
[…] Connecting to remote server: https://<lan-host>/mcp?…
[…] (proceeds — ultimately hits TLS / handshake)
# Same shell, wrapped in Claude Desktop's disclaimer: EHOSTUNREACH
$ NODE_TLS_REJECT_UNAUTHORIZED=0 \
/Applications/Claude.app/Contents/Helpers/disclaimer \
/usr/local/bin/npx -y mcp-remote \
'https://<lan-host>/mcp?...' --transport http-only
[…] Connection error: connect EHOSTUNREACH <lan-ip>:443
code: 'EHOSTUNREACH', errno: -65
The disclaimer binary's strings confirm responsibility_spawnattrs_setdisclaim is the only relevant API call, so the wrapper is what severs the network policy chain.
Root cause
responsibility_spawnattrs_setdisclaim_v2() (called by the disclaimer helper) sets the spawned process to disclaim its parent's responsibility for TCC purposes. On Sequoia, the kernel's NECP (Network Extension Control Policy) consults the responsible bundle id when deciding whether a process may connect to a private-range IP. With responsibility severed and no own bundle id (Node CLI, no Info.plist), the policy denies the connection synchronously with EHOSTUNREACH.
The Local Network Privacy panel in System Settings only auto-populates apps that invoke Bonjour SDK calls (NSNetServiceBrowser etc.). Plain unicast connect() to a LAN IP does not surface a prompt and produces no TCC entry — so tccutil reset SystemPolicyNetwork com.anthropic.claudefordesktop returns Failed to reset because there is nothing to reset, and there is no user-side way to grant the permission either.
Loopback (127.0.0.1) is not gated by NECP, which is why a localhost-bound forwarder works (see workaround below).
Why Claude Code is unaffected
claude (CLI) is launched from Terminal.app, which has been granted Local Network access, and CC's MCP child inherits Terminal's responsibility chain (no disclaimer wrapper involved). Same Mac, same MCP config block — connects successfully every time.
Workaround
A loopback HTTP→HTTPS reverse proxy outside Claude Desktop's process tree:
- A small Python (stdlib only) HTTP reverse proxy on
127.0.0.1:<port>, forwarding tohttps://<lan-host>:443with the right SNI and rewriting theHost:header (the upstream Caddy was virtual-hosting on Host). - Run under launchd as a user agent (
~/Library/LaunchAgents/<label>.plist,RunAtLoad=true,KeepAlive=true). - Claude Desktop config rewritten to
http://localhost:<port>/mcp?...with--allow-http.
After this change Claude Desktop initializes the MCP session cleanly: tools/list, prompts/list, resources/list all return as expected.
This works because (1) the disclaimed child can reach 127.0.0.1 (loopback isn't NECP-gated), so the EHOSTUNREACH is bypassed, and (2) the proxy itself runs from launchd's user agent context and has full LAN access.
Suggested fixes (in order of preference)
- Don't disclaim MCP server children. If the security goal is to isolate user-installed MCP commands from Claude Desktop's TCC grants, a narrower mechanism (e.g. a custom seatbelt profile, or a process group with its own bundle id) would isolate them without breaking LAN access for self-hosted MCP servers — which appears to be a common pattern.
- Document the limitation. If the disclaim behavior is intentional, document that LAN MCP endpoints aren't reachable from Claude Desktop on macOS and recommend the loopback-proxy workaround in the MCP setup docs.
- Surface a clearer error. Right now the user sees a Node
EHOSTUNREACHburied in~/Library/Logs/Claude/mcp-server-<name>.log. The MCP manager could detect this specific error against a private IP and surface a tailored message ("This MCP server is on your local network. Claude Desktop on macOS cannot reach LAN MCP servers directly — set up a loopback proxy. [docs link]").
---
Happy to provide additional logs / packet capture / try fixes if a Desktop-team contact wants to follow up.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗