Skip to content

Bind MCP server to loopback#208

Closed
fa1k3 wants to merge 1 commit into
aaronsb:mainfrom
fa1k3:fix/listen-loopback
Closed

Bind MCP server to loopback#208
fa1k3 wants to merge 1 commit into
aaronsb:mainfrom
fa1k3:fix/listen-loopback

Conversation

@fa1k3
Copy link
Copy Markdown
Contributor

@fa1k3 fa1k3 commented May 20, 2026

Summary

  • bind both MCP HTTP server entry points to 127.0.0.1 instead of Node's default wildcard interface
  • update startup logs to show the explicit loopback address
  • add regression coverage that asserts the explicit loopback bind is used

Why

This keeps the local Obsidian MCP HTTP endpoint scoped to the local machine by default.

Tests

  • npm test -- --runInBand tests/listen-loopback.test.ts
  • npm run lint (passes with existing warnings only)
  • npm run build
  • npm test -- --runInBand --forceExit

@aaronsb
Copy link
Copy Markdown
Owner

aaronsb commented May 24, 2026

Hi @fa1k3 — thank you for this PR. The security instinct here is exactly right, and digging into it surfaced that the gap is broader than the bind axis alone (cleartext over LAN, self-signed-on-LAN trust model, and the agent-visible side of the problem all interact). Rather than land the hardcoded 127.0.0.1 and lose the legitimate "Obsidian on machine A, MCP client on machine B" escape hatch the plugin's HTTPS path is built for, I wrote it up as ADR-107 (network exposure modes as a classified state machine) and implemented it in #211.

The design preserves the spirit of your patch:

  • Default bind is 127.0.0.1, exactly as you proposed
  • The dangerous-by-default http + 0.0.0.0 state is now explicitly classified 🔴 JAIL and surfaces a Notice + Debug.error + an MCP initialize.instructions warning so the LLM client also sees it on its first turn
  • The migration nudges existing users to the new default with a one-time Notice

What it adds over the hardcoded approach:

  • A bindMode setting (Loopback / All interfaces / Custom) so users who legitimately need LAN access can opt in with eyes open instead of needing a code change
  • A live verdict badge in the settings UI that re-classifies on every change, with an inline red caution under "All interfaces"
  • The classification is a pure 9-cell table that handles the protocol × bind × cert combinations together — https + LAN + self-signed (encrypted but TOFU) is treated differently from http + LAN (cleartext), where a single-axis warning would conflate them

Closing this in favor of #211. Credit and inspiration noted in the ADR's "Alternatives Considered" section and in the commit messages. Genuinely appreciate the contribution — please keep them coming.

— Aaron

@aaronsb aaronsb closed this May 24, 2026
aaronsb added a commit to fa1k3/obsidian-mcp-plugin that referenced this pull request May 25, 2026
Models the combined (protocol × bind × cert) state as a 9-cell table
with three verdicts (🟢 OK / 🟡 WARN / 🔴 JAIL). Server always starts;
🔴 surfaces a Notice + log + MCP `initialize` instructions warning so
the LLM client also sees it. New settings: `bindMode` discriminated
enum + `customBindHost`. Migration flips the implicit `0.0.0.0` to a
real `127.0.0.1` default, making ADR-103's "loopback by default"
premise actually true. Inspired by community PR aaronsb#208 (fa1k3) —
preserves the security instinct while keeping the LAN/remote escape
hatch the hardcoded approach would have removed.
aaronsb added a commit to fa1k3/obsidian-mcp-plugin that referenced this pull request May 25, 2026
Replaces the implicit 0.0.0.0 Node-default bind with a deliberate,
classified network exposure model.

Server: MCPHttpServer.start() now resolves the listen host from
settings (bindMode + customBindHost), passes it to listen(), and
classifies the combined (protocol × bind × certSource) state. On 🔴
fires a 15s Notice + Debug.error; on 🟡 logs Debug.warn; on 🟢 silent.
The verdict-derived agent-visible warning is pushed into MCPServerPool
which conditionally injects it into the MCP initialize.instructions
field for every session created after bind, so an LLM client sees the
warning on its first turn against the server. node-mcp-server.ts (the
unused fallback) is hardcoded to loopback for consistency.

Settings: MCPPluginSettings gains bindMode ('loopback' | 'all' |
'custom') default 'loopback', customBindHost default '', and a
one-shot hasShownBindMigrationNotice flag. Existing installs with a
data.json that predates bindMode see a one-time 20s Notice on next
load pointing them at the new setting; fresh installs are silent.

UI: a new "Network binding" section between server config and the
HTTPS section, with a live colored verdict badge that re-classifies
on every change, the bind dropdown (Loopback / All interfaces /
Custom), an inline red caution under "All interfaces" whose wording
adapts to whether HTTPS is enabled, and a custom-host text field
that normalizes loopback/wildcard aliases on blur (typing 127.0.0.1
collapses the dropdown to Loopback; 0.0.0.0 collapses to All).

Tests: 7 integration assertions verifying the source wires listen()
to the resolved host, pushes verdict-derived instructions into the
pool, conditionally injects on 🔴 only, and that the pool spread-
pattern omits the field on ok/warn.

Inspired by PR aaronsb#208 (fa1k3) — preserves the security instinct while
keeping the LAN/remote escape hatch the hardcoded approach removed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants