Skip to content

feat: optional auth-gated LAN binding for the MCP server#188

Open
mvanhorn wants to merge 3 commits into
palmier-io:mainfrom
mvanhorn:fix/122-mcp-server-lan-binding
Open

feat: optional auth-gated LAN binding for the MCP server#188
mvanhorn wants to merge 3 commits into
palmier-io:mainfrom
mvanhorn:fix/122-mcp-server-lan-binding

Conversation

@mvanhorn

@mvanhorn mvanhorn commented Jun 26, 2026

Copy link
Copy Markdown

Summary

Adds an optional, auth-gated LAN binding for the MCP server. The default stays loopback-only; users can opt in to expose it on the local network, in which case a bearer token is required.

Why this matters

#122 asks to reach the MCP server from another machine on the local network (an agent running off-box). The maintainer confirmed loopback is an intentional security default and asked for the use case, which the reporter gave (an agent on another machine on the LAN). This change keeps the secure default and makes LAN access an explicit, off-by-default opt-in protected by a bearer token, so behavior is unchanged when the toggle is off.

What changed

  • MCPHTTPServer.swift: bind to a configurable host (default 127.0.0.1); for non-loopback hosts, require a bearer token via a new MCPBearerTokenValidator (constant-time compare) and return 401 on a missing/incorrect token. Host strings are normalized and validated, falling back to loopback on anything malformed.
  • MCPService.swift: a bindHost preference and an auto-generated, securely-random bearer token persisted in UserDefaults, plus derived connection host/URL; the token is only required when binding non-loopback.
  • AppState.swift: setMCPHost / regenerateMCPToken that restart the server when it is enabled.
  • AgentPane.swift: an "Allow access from local network" toggle with the connection URL and a copyable token.
  • MCPHTTPServerTests.swift: tests for host normalization (loopback / localhost / valid IPv4 / malformed) and the loopback-vs-LAN bearer-token requirement.

Verification

The added MCPHTTPServerTests cover the pure host-normalization and token-gating logic. Local swift test could not run to completion in this environment because the SPM build hangs fetching the Sentry binary xcframework artifacts; CI exercises the full suite.

Closes #122

AI was used for assistance.


Note

High Risk
Opt-in exposure of the MCP server on the LAN and bearer tokens stored in UserDefaults affect remote access to editor tooling; mitigations are explicit opt-in and required auth on non-loopback binds.

Overview
Adds an opt-in local network mode for the MCP HTTP server while keeping loopback-only as the default.

MCPHTTPServer now binds to a configurable host (normalized/validated IPv4 or localhost, invalid values fall back to 127.0.0.1). For non-loopback binds it enforces Authorization: Bearer via MCPBearerTokenValidator (constant-time compare, 401 + WWW-Authenticate) on MCP and OAuth discovery routes, swaps localhost origin checks for bearer validation, and advertises a sensible host when binding 0.0.0.0.

MCPService persists bind host and an auto-generated bearer token in UserDefaults, wires them into server startup, exposes connection URL/host helpers, and makes stop() async with guards against stale start tasks.

AppState restarts MCP when LAN toggle or token regeneration changes settings; Agent settings add the LAN toggle, copyable connection URL, token display/regenerate, and updated “running on” address.

MCPHTTPServerTests cover host normalization and loopback vs LAN token gating.

Reviewed by Cursor Bugbot for commit 204caaa. Bugbot is set up for automated code reviews on this repo. Configure here.

Default stays loopback-only. Add an off-by-default opt-in to bind the MCP
server on a configurable host; when binding non-loopback, require a bearer
token (constant-time compare, 401 on mismatch). Host strings are normalized
and validated, an auto-generated token is stored in UserDefaults, and the
Agent settings pane exposes the toggle, connection URL, and copyable token.

Closes palmier-io#122
Comment thread Sources/PalmierPro/App/AppState.swift Outdated
Comment thread Sources/PalmierPro/Agent/MCP/MCPHTTPServer.swift Outdated
@mvanhorn

mvanhorn commented Jul 9, 2026

Copy link
Copy Markdown
Author

Addressed both cursor findings: restartMCPServiceIfNeeded now waits for the old listener's stop to complete before starting the replacement (no more bind race on the port), and the /.well-known/oauth-protected-resource response advertises the reachable machine hostname instead of the raw 0.0.0.0 bind address in LAN mode. All Swift sources compile locally (only .metal kernels fail here from a missing local Metal toolchain).

Comment thread Sources/PalmierPro/Agent/MCP/MCPHTTPServer.swift
… binds

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mvanhorn

Copy link
Copy Markdown
Author

Bugbot's finding addressed in 204caaa: the /.well-known/oauth-protected-resource handler now runs the same accessRejection bearer check as /mcp before serving metadata, so the endpoint is no longer unauthenticated on non-loopback binds. The earlier two findings (restart race, advertised host) were resolved in previous pushes.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 204caaa. Configure here.

guard let self, self.httpServer === httpServer else { return }
Log.mcp.error("http server failed to start: \(error.localizedDescription)")
self?.isRunning = false
self.isRunning = false

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Start task revives stopped listener

High Severity

MCPService.start() schedules a MainActor task that calls httpServer.start() before checking whether that server instance is still the active one. If stop() or a restart clears httpServer and stops the listener first, the pending task can still bind a listener on the discarded instance, leaving an orphaned MCP HTTP server (including LAN exposure) while the app starts or tracks a different instance.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 204caaa. Configure here.

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.

[FEATURE REQUEST] - Expose MCP server to local network

1 participant