claude-code - 💡(How to fix) Fix Claude Desktop on macOS: spawned MCP servers can't reach LAN (EHOSTUNREACH) [1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
anthropics/claude-code#55169Fetched 2026-05-01 05:44:27
View on GitHub
Comments
0
Participants
1
Timeline
1
Reactions
0
Author
Participants
Timeline (top)
labeled ×1

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.

Error Message

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 }

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).

Fix Action

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 to https://<lan-host>:443 with the right SNI and rewriting the Host: 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.

Code Example

{
  "mcpServers": {
    "lan-mcp": {
      "command": "/usr/local/bin/npx",
      "args": ["mcp-remote", "https://<lan-host>/mcp?..."],
      "env": { "NODE_TLS_REJECT_UNAUTHORIZED": "0" }
    }
  }
}

---

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
}

---

# 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
RAW_BUFFERClick to expand / collapse

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 via npx 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:

ProbeResult
ping <lan-host>0% loss, low-ms
curl -sk https://<lan-host>/healthHTTP 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 to https://<lan-host>:443 with the right SNI and rewriting the Host: 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)

  1. 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.
  2. 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.
  3. Surface a clearer error. Right now the user sees a Node EHOSTUNREACH buried 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.

extent analysis

TL;DR

The most likely fix for the issue of Claude Desktop being unable to reach self-hosted MCP servers on the local network is to implement a loopback HTTP→HTTPS reverse proxy outside Claude Desktop's process tree.

Guidance

  • Identify the root cause of the issue: the disclaimer wrapper severs the network policy chain, causing the kernel to deny connections to private-range IPs.
  • Consider implementing a loopback proxy as a workaround, as described in the issue.
  • If the disclaim behavior is intentional, document the limitation and recommend the loopback-proxy workaround in the MCP setup docs.
  • Surface a clearer error message to users when they encounter this issue, providing guidance on setting up a loopback proxy.

Example

A small Python HTTP reverse proxy can be used to forward requests from 127.0.0.1:<port> to https://<lan-host>:443. This can be run under launchd as a user agent.

Notes

The issue is specific to Claude Desktop on macOS and does not affect Claude Code (CLI). The loopback proxy workaround bypasses the NECP gate by allowing the disclaimed child to reach 127.0.0.1.

Recommendation

Apply the loopback proxy workaround, as it provides a functional solution to the issue and allows users to access self-hosted MCP servers on their local network.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING