Skip to content

Serve the MCP endpoint over HTTP from a self-hosted relay - #75

Merged
Daily-AC merged 1 commit into
mainfrom
feat/hosted-mcp
Sep 16, 2026
Merged

Daily-AC merged 1 commit into
mainfrom
feat/hosted-mcp

Conversation

@Daily-AC

Copy link
Copy Markdown
Owner

Why

The relay has carried a Streamable-HTTP MCP server at /wanctl-mcp since the MCP work landed — main.go mounts it whenever WANCTL_MCP_SEED is set. Nothing in selfhost/ could turn it on, so no deployment ever did. AI hosts that cannot spawn a local wanctl process (a browser chat, a cloud agent runner) have no way to reach the tools that a stdio host gets for free.

No Go change was needed for the endpoint itself. Only the wiring and the documentation were missing.

What changed

Compose. The relay now receives WANCTL_MCP_SEED, WANCTL_MCP_ALLOW_UNSAFE_TRUST_SERVER, WANCTL_PORTAL and WANCTL_RELAY. The two seeds default to empty, which is off.

The two origins are not optional extras. An MCP session is a controller: wanctl_login resolves the portal origin to build the enrollment URL, and every data tool resolves the relay URL to reach the broker. Measured with a local wanctl mcp --http carrying the seed and nothing else:

initialize   → 200, serverInfo.name = "wanctl"
tools/list   → 17 tools
wanctl_login → "no portal configured: run `wanctl config set portal=…`"

WANCTL_RELAY points at the container's own loopback, matching WANCTL_WEBFETCH_RELAY_URL, so MCP traffic does not hairpin out through the public edge and back.

Docs. A new portal guide, ai__mcp, covers both transports for users: stdio for a host that can spawn wanctl, the hosted endpoint for one that cannot, the registration command for each, the two-step portal login, the first-contact pairing approval, what a rebind credential is, and the security boundary. docs/self-hosting.md gained an "Optional: enable the hosted MCP endpoint" section, and docs/plans/2026-09-17-hosted-mcp.md records the decision, the measurements and the rollback.

Feishu wording. The MCP tool descriptions and the login prompt told every user to sign in via Feishu. The portal's identity provider is deployment configuration, and a GitHub-backed portal has no such button. The text is now provider-neutral. go test ./internal/mcp/... passes.

The limit this ships with

A hosted session keeps device trust in an in-memory store, and wanctl_trust_server is fail-closed unless the operator sets WANCTL_MCP_ALLOW_UNSAFE_TRUST_SERVER=1. Out of the box a hosted session can therefore log in and list devices but cannot run anything — the first wanctl_exec stops at DEVICE IDENTITY CONFIRMATION REQUIRED. Whether a relay you own yourself is the attacker that guard is protecting against is an operator decision, so the opt-in ships unset and both documents say plainly what that costs.

Drive-by: the docs site build was red on main

tools/docsite/build.py has failed on main since the WebFetch docs landed, which blocks publishing anything at all:

docs/architecture.md vs docs/architecture.zh.md: link targets differ — ['device-identity.md', 'webfetch.md'] vs ['webfetch.zh.md']
docs/environment.md vs docs/environment.zh.md: link targets differ — ['webfetch.md'] vs ['webfetch.zh.md']

architecture.zh.md had lost the device-identity paragraph its source carries, and three documents linked to webfetch.md, which the site does not publish. The webfetch links now point at the repository copy, device-identity.md joined the site, and the build is green:

index + 15 articles × 2 bodies, parity ok, 20 links checked, 0 broken

docs/webfetch.zh.md is a condensed rewrite rather than a structural translation of docs/webfetch.md (58 lines against 151, different headings, a missing table and code block), so publishing that page needs a real translation pass and is deliberately left out of this PR.

Verified against a live deployment

Enabled on the z10 relay and exercised end to end: initialize returns 200 with an Mcp-Session-Id, tools/list returns 17 tools, wanctl_login with no arguments returns the portal enroll URL, Claude Code registered with --transport http connects and calls the tool, and a real one-time code exchanged through wanctl_login bound the session to its namespace, after which wanctl_peers listed every online device. The relay logs MCP server enabled at /wanctl-mcp; the postgres container was untouched by the --no-deps recreate.

🤖 Generated with Claude Code

The relay has been able to run a Streamable-HTTP MCP server at /wanctl-mcp
since the MCP work landed; nothing in selfhost could turn it on. Pass the
seed through, along with the two origins an MCP session needs to act as a
controller: wanctl_login resolves the portal origin for the enrollment URL
and every data tool resolves the relay URL to reach the broker. With the
seed alone the endpoint answers initialize and tools/list and then fails
the first login with "no portal configured".

The relay reaches itself over loopback, matching WANCTL_WEBFETCH_RELAY_URL,
so MCP traffic does not hairpin out through the public edge.

Document both transports for users in a new portal guide: stdio for a host
that can spawn wanctl, the hosted endpoint for one that cannot. It also
states the limit that a hosted session hits, which is that it pins device
identities in memory and stays fail-closed on first contact unless the
operator opts in.

Drop Feishu from the MCP tool text. The portal's identity provider is
deployment configuration, and the tool was telling every user to look for a
button that a GitHub-backed portal does not have.

Fix the docs site build, red on main since the WebFetch docs landed:
architecture.zh.md had lost the device-identity link its source carries, and
three documents linked to webfetch.md, which the site does not publish.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Daily-AC
Daily-AC merged commit c14b40f into main Sep 16, 2026
4 checks passed
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