Skip to content

Comments

Add --host flag to MCP HTTP server#232

Open
kleddbot wants to merge 1 commit intotobi:mainfrom
kleddbot:fix/mcp-http-host-flag
Open

Add --host flag to MCP HTTP server#232
kleddbot wants to merge 1 commit intotobi:mainfrom
kleddbot:fix/mcp-http-host-flag

Conversation

@kleddbot
Copy link

@kleddbot kleddbot commented Feb 20, 2026

feat(mcp): add --host flag to HTTP server bind address

Problem

On macOS, Node.js resolves localhost to ::1 (IPv6 loopback) via the system resolver. Docker containers using Colima or similar runtimes route host.docker.internal to IPv4 127.0.0.1. This mismatch causes silent connection failures when agents or downstream tools (e.g., OpenClaw) try to reach the QMD daemon from inside a container.

There was no way to explicitly control the bind address — QMD always bound to localhost, making the failure unrecoverable without external workarounds.

Solution

Add a --host flag to qmd mcp --http that lets users specify the bind address explicitly.

qmd mcp --http --host 127.0.0.1          # explicit IPv4 bind
qmd mcp --http --host 127.0.0.1 --daemon  # same, as background daemon
qmd mcp --http --host '[::1]'            # explicit IPv6 (brackets stripped automatically)
qmd mcp --http                           # unchanged default: localhost

Changes

src/mcp.ts

  • startMcpHttpServer() accepts optional host parameter
  • Normalizes bracketed IPv6 input ([::1]::1) for socket bind
  • Re-brackets IPv6 addresses in URL construction (::1[::1]) per HTTP spec
  • Moves listeningPort assignment inside listen() callback — ensures internal forwarding URLs use the resolved port when using --port 0
  • All internal URL construction uses configured host instead of hardcoded localhost

src/qmd.ts

  • Adds host to parseArgs options
  • Validates --host input: rejects values containing :// or / (catches --host http://127.0.0.1)
  • Normalizes bracketed IPv6 at CLI boundary before passing to library
  • Forwards --host to daemon child process via spawnArgs
  • Updates daemon startup message to reflect configured host
  • Updates EADDRINUSE error message to include bound host

Documentation

  • CLAUDE.md: Updated CLI reference
  • README.md: Added --host example with Docker/Colima context
  • skills/qmd/references/mcp-setup.md: Updated HTTP mode examples

Tests (test/mcp.test.ts)

Seven new tests in two groups:

Host binding (5 tests):

  • Default host: server reachable via localhost
  • Explicit IPv4 bind + address verification via AddressInfo
  • MCP protocol handshake on custom host
  • Bracketed IPv6 input normalized correctly ([::1]::1 at socket level)

Daemon --host forwarding (2 tests):

  • Daemon spawn forwards --host to child process (verified via health endpoint polling)
  • Invalid --host value (URL) exits with code 1 and descriptive error

All tests use isolated state directories (XDG_CACHE_HOME, QMD_CONFIG_DIR) to prevent interference with real user daemons.

Test plan

  • tsc — no type errors
  • vitest test/mcp.test.ts — 63/63 pass (7 new)
  • vitest test/cli.test.ts — 66/66 pass (existing daemon tests unaffected)
  • vitest test/ — 495 passed tests, 0 failed tests; 11 passed files, 1 failed file (structured-search.test.ts import error, pre-existing on main too)
  • Manual: foreground --host 127.0.0.1, foreground default, daemon --host 127.0.0.1, help text

Backward Compatibility

Fully backward compatible. Without --host, behavior is identical to before (binds to localhost). No changes to stdio MCP transport, CLI commands, or search functionality.

Usage with Docker / OpenClaw

# Recommended for Docker/Colima environments
qmd mcp --http --host 127.0.0.1 --daemon

# Container reaches QMD via:
# http://host.docker.internal:8181/mcp

Follow-up Items

These are minor refinements identified during review. None block this merge.

  • Extract formatHostForUrl() helper — Bracket-stripping and re-bracketing logic appears in 3 sites (mcp.ts, qmd.ts CLI normalization, qmd.ts display formatting). Extract a shared helper to single-source the normalization contract.
  • Stricter IP validation with net.isIP() — For inputs that look like numeric IPs, use net.isIP() to catch structurally invalid addresses like 192.168.1.999 before they fail with cryptic EADDRNOTAVAIL at bind time. Must not reject valid hostnames (e.g., host.docker.internal) — only validate when the input is clearly intended as a numeric address.
  • Comment test port strategy — The daemon integration test uses a random port in a static range (19876 + random) instead of --port 0 because the daemon spawns as a detached process and the test needs a known port to poll. Add an inline comment explaining this constraint for future maintainers.

Co-Authored-By: Claude Opus 4.6 noreply@anthropic.com
Co-Authored-By: ChatGPT 5.3-Codex noreply@openai.com

Closes #227

Allow users to specify the bind address for the MCP HTTP server,
solving IPv6/IPv4 mismatch when Docker containers (Colima) route
host.docker.internal to 127.0.0.1 while Node resolves localhost to ::1.

- Add host option to startMcpHttpServer() with IPv6-safe URL formatting
- Add --host to CLI parseArgs, daemon spawn forwarding, and help text
- Validate --host input (reject URLs) and normalize bracketed IPv6
- Set listeningPort inside listen() callback for correct --port 0 handling
- Update forwarding URLs to use configured host instead of hardcoded localhost
- Add 7 tests: default host, explicit IPv4, AddressInfo, MCP handshake,
  bracketed IPv6 normalization, daemon forwarding, invalid input rejection
- Update CLAUDE.md, README.md, mcp-setup.md with --host examples

Closes tobi#227

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

[mcp --http] macOS: server binds IPv6-only via "localhost", unreachable from IPv4 clients (Docker/Colima)

1 participant