Skip to content

✨ feat: πŸ”Œ remote-mcp registers a bearer-token MCP server per Claude Code profile - #100

Merged
hadees merged 3 commits into
mainfrom
feat/remote-mcp
Oct 12, 2026
Merged

hadees merged 3 commits into
mainfrom
feat/remote-mcp

Conversation

@hadees

@hadees hadees commented Oct 12, 2026

Copy link
Copy Markdown
Owner

What

bin/remote-mcp registers a remote, bearer-token MCP server in each Claude Code profile pinned for it, and relays one that lives on a tailnet through a per-server loopback forwarder. It names no server: URL, token references, tailnet and health path all come from machine-local git config (remote-mcp.<server>.*), which the overlays supply. remote-mcp-doctor joins doctor.

Why

claude mcp add covers a public server with a static header. It does not cover a self-hosted one whose token must not sit in a config file and whose host is reachable only over a tailnet the Tailscale app may not be on.

Measured on Claude Code 2.1.296

  • claude mcp add-json keeps headersHelper; claude mcp add has no flag for it. connect reads the written entry back.
  • The helper may carry arguments and is handed CLAUDE_CODE_MCP_SERVER_NAME and CLAUDE_CONFIG_DIR, so one helper serves every server and profile.
  • The helper gets about ten seconds. Past that the request goes out with no Authorization header and the server answers 401.
  • The MCP client honours HTTPS_PROXY, but process-wide: the API connection used it in the same test, and every child process would inherit it. Hence a forwarder per server instead.

Shape

  • The token is read from 1Password at connect time and never lands on disk.
  • The forwarder is plain HTTP on loopback and TLS to the real name through the tailnet daemon's CONNECT proxy, so the certificate and Host the far side sees are unchanged. It holds no secret, refuses an Origin header or a non-loopback Host, and identifies the config it was started for so a stale one is replaced. headers starts it on demand; nothing is installed or supervised.
  • Config is read from the user's git config only, never a checkout's .git/config: these keys decide where a token is sent.
  • Unconfigured it is inert on every machine class: one status line, headers fails without calling op, nothing runs at apply.
  • The loopback hop assumes a single-user machine, as the tailnet daemons' own loopback proxies do; the script and CLAUDE.md say so.

Also replaces one workset test fixture string with a name from the suite's own vocabulary.

Verified

  • bats tests/remote-mcp.bats: 26 tests (stub op/claude/tailnet/curl, sandboxed HOME and git config; the forwarder runs for real against a dead far side)
  • End to end against a real server with a throwaway profile: the real CLI ran the helper, the forwarder started on demand and relayed over the tailnet
  • Two public-clone review passes and a correctness review; findings folded into the commits
  • CI on all four platforms

πŸ€– Generated with Claude Code

The file's other fixtures are invented names (octo-alpha, demo); the note
value was the one that was not. Same assertion, same vocabulary as the rest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hadees hadees self-assigned this Oct 12, 2026
hadees and others added 2 commits October 12, 2026 15:23
…ode profile

`claude mcp add` covers a public server with a static header. It does not
cover a self-hosted one: a token that must not sit in a config file, on a
host reachable only over a tailnet the Tailscale app may not be on.

The token comes from 1Password at connect time through a headers helper.
Measured on Claude Code 2.1.296: `claude mcp add-json` keeps `headersHelper`
(`claude mcp add` has no flag for it), the helper may carry arguments, it
is handed CLAUDE_CODE_MCP_SERVER_NAME and CLAUDE_CONFIG_DIR so one helper
serves every server and profile, and it gets about ten seconds β€” after
which the request goes out with no Authorization header at all.

The tailnet path is a per-server loopback forwarder rather than a proxy
variable. The MCP client does honour HTTPS_PROXY, but process-wide: the
API connection went through it in the same test, and every command a
session runs would inherit it. The forwarder is plain HTTP on loopback and
TLS to the real name through the tailnet daemon's CONNECT proxy, so the
certificate and Host the far side sees are unchanged. It holds no secret,
refuses an Origin header or a non-loopback Host, and identifies the config
it was started for so a stale one is replaced instead of trusted. It
assumes a single-user machine, as the tailnet daemons' own loopback
proxies do, and says so.

Every name is machine-local git config: the script contains no server, and
with nothing configured it is one status line and never calls op.
remote-mcp-doctor joins `doctor` and reads config and loopback only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The three measurements that shape it, why the forwarder is per server, what
an unconfigured clone gets, and the config block an overlay supplies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hadees
hadees merged commit 1937bbf into main Oct 12, 2026
5 checks passed
@hadees
hadees deleted the feat/remote-mcp branch October 12, 2026 20:52
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.

1 participant