MCP hosting with agent-mcp-host
agent-mcp-host is a CLI binary installed on $PATH. It is the operator
tool of the agent-* family: it runs every mounted tool's MCP server
behind ONE https origin, with ONE OAuth 2.1 authorization server and a
separate login per tool. Each tool stays a full MCP server in its own binary
— the host mounts it behind a path (/<name>/mcp) and reverse-proxies to it.
Run agent-mcp-host usage for the full LLM-optimized reference card.
Mental model
- One origin (e.g.
https://hub.tailnet.example) fronts N tools. - Each tool is a mount:
--mount slack=agent-slackserveshttps://<host>/slack/mcp. A person adds one Claude connector per tool. - The host owns pairing (who may connect), OAuth (token minting), and the
browser enrollment pages. Tools validate the host's Ed25519 tokens and mint
nothing themselves (delegate mode). A slack token is useless at
/lin/mcpby construction.
Serve (long-running — run in the background)
agent-mcp-host serve --tailscale funnel \
--mount slack=agent-slack --mount lin=lin
--tailscale funnel|servefronts the host with a Tailscale tunnel and derives--public-urlfrom MagicDNS — no URL to figure out; the tunnel is torn down on exit (--tailscale-port 443|8443|10000).- Without
--tailscale, pass--public-url https://…(the OAuth issuer) and front the--httplistener (default127.0.0.1:8000) yourself. servedoes not exit. Run it in the background and read stdout for NDJSON lifecycle events —{"event":"ready"}then one{"event":"mount_ready","tool":…,"url":…}per mount means it's up. Connector URLs and the pairing banner go to stderr.- There is no separate step to start mounted tools:
servespawns each one as<binary> mcp --http 127.0.0.1:<port> --oauth <public-url>with its audience and verify key injected via env. The binaries just need to be installed (brew install shhac/tap/agent-slack); stopping the host stops them.
Attach mounts (run a tool yourself)
To run a tool under your own control (debugger, launchd) instead of having
serve spawn it:
agent-mcp-host mount-env lin=lin --http 127.0.0.1:9410 --tailscale funnel
# → prints the exact launch command (env + flags) for that tool; run it, then:
agent-mcp-host serve --tailscale funnel --mount lin=lin@127.0.0.1:9410 ...
The binary name is still required (the host execs it for mcp schema and
mcp enroll). mount-env's --public-url/--tailscale must match what
serve runs with — --tailscale here only derives the URL; no tunnel starts.
People (pairing + per-tool credentials)
agent-mcp-host pair add alice --bind slack:workspace=acme --bind lin:workspace=acme
agent-mcp-host pair list # principals + bindings (codes never shown)
agent-mcp-host pair show alice # prints alice's code (a secret)
agent-mcp-host pair rotate alice # fresh code, bindings preserved
agent-mcp-host pair remove alice # revokes code + refresh tokens + sessions
Bindings are namespaced per tool (slack:workspace=acme); each tool's token
carries only its own slice, prefix stripped. Without --bind for a tool that
supports enrollment, the person enters their own credentials in the browser
the first time they connect that tool (fields come from the tool's
mcp schema; secrets go tool-ward via stdin, never argv).
How a person connects: (1) add the connector URL — the approval page opens; (2) enter the pairing code once, then the tool's enrollment form if no binding was provisioned; (3) the next tool skips the code (a 30-day browser session covers identity) and prompts only for that tool's enrollment.
Output contract
- stdout — NDJSON event stream, one line per lifecycle moment:
ready,mount_ready,client_registered,paired,session_started,enrolled,authorized(withtool,principal,client,via,timefields as applicable). Events never contain secrets. - stderr — the human boot banner (connector URLs + pairing code) and each tool's own stderr prefixed with its mount name.
paircommands print human-readable text, not NDJSON.
Handling secrets
Pairing codes are passwords. pair add, pair rotate, and pair show
print one, and the serve stderr banner shows the shared code — never quote
a code into summaries, commit messages, or shared documents; share it only
with the person it belongs to. If a code may have leaked, pair rotate <name>; to revoke a person entirely, pair remove <name>. Stored secrets
(signing key, codes, sessions, refresh tokens) live in the keychain service
app.paulie.agent-mcp-host.mcp and are never printed by list.
Global flags
--format json|yaml|jsonl— output format--color auto|always|never,--timeout <ms>,--debug
References
- references/commands.md: full command map + all flags
- references/output.md: event stream + output shapes