webmcp
The open-source daemon for webmcp.fast: any local MCP
server, reachable by any agent harness. webmcp pairs your machine, holds one
outbound connection to the gateway (no inbound ports, no tunnel), and exposes
each local MCP server you attach at its own authenticated URL:
https://<handle>.webmcp.fast/<device>/<server>/mcp.
The daemon is the part that runs on your machine, so it is open (Apache-2.0).
The hosted gateway is not in this repository. Wire protocol: docs/PROTOCOL.md;
what the daemon can and cannot be made to do: THREAT_MODEL.md.
Install
With Node 18+ (macOS Apple silicon or Intel, Linux x86_64 or arm64, Windows x64):
npm i -g @proticom/webmcp
webmcp up
or, without installing, npx @proticom/webmcp up. The npm package is a small
launcher with no runtime dependencies and no install script; the prebuilt
binary comes from a platform package (@proticom/webmcp-darwin-arm64 and so
on) picked by npm. Every package is published from GitHub Actions with a
provenance attestation: npm audit signatures verifies that what you
installed was built by the release workflow in this repository.
Linux, without Node: the GitHub release has one tarball per target and a
single SHA256SUMS file (releases).
scripts/install.sh, attached to every release, downloads one tarball, checks
it against SHA256SUMS and copies the binary to ~/.local/bin
(WEBMCP_INSTALL_DIR changes that, WEBMCP_VERSION=v0.1.0 pins a release):
curl -fsSL https://github.com/proticom/webmcp/releases/latest/download/install.sh | sh
Read it first. On macOS the script points you at npm instead, because the
tarballs are not code-signed (there is no Apple Developer ID) and Gatekeeper
blocks a downloaded unsigned binary; WEBMCP_ALLOW_UNSIGNED=1 overrides.
Windows gets a .zip on the release page. Or build from source (Rust 1.94+):
cargo install --git https://github.com/proticom/webmcp --locked
webmcp --version
Quick start: webmcp up
webmcp up
One command, safe to run again at any point. It does only what is still missing:
- Pairs this machine. It prints a link (and on macOS and Linux tries to open it in your browser): sign in with the
emailed code, which creates the account if you have none, pick a handle, approve the device. The
login code never passes through the daemon.
--name laptop,--base-url URL,--no-browser;--forcepairs again after a revoke (attached servers are kept). - Offers the MCP servers this machine already has, found read-only in the configs of Claude Code
(
~/.claude.json,./.mcp.json), Claude Desktop, Cursor (~/.cursor/mcp.json,./.cursor/mcp.json), Codex CLI (~/.codex/config.toml) and VS Code (./.vscode/mcp.json). At a terminal you pick by number, and only while nothing is attached yet;--attach <name>(repeatable) attaches without asking;--no-attachskips. Nothing is ever attached without one of those. Attaching a stdio server copies its env variables, values included, into the webmcp config (that is how the server gets its API keys); the output names the variables, never the values. Remote-URL entries are listed as "remote, not attachable".webmcp discovershows the list on its own. - Background service (macOS, Linux): asks at a terminal,
--serviceinstalls without asking; otherwise it says how. Elsewhere runwebmcp connectunder your own supervisor. - Reports each server's URL,
https://<handle>.webmcp.fast/<device>/<alias>/mcp. Paste it into Claude, ChatGPT or Grok as a custom connector, sign in, click Allow.
For agents: --json
webmcp up --json --attach github --service
--json (global; honoured by up, discover, status, servers, attach, detach,
approvals, approve, passkey, service status) prints exactly one JSON object on stdout, on one line;
progress and logs go to stderr, and nothing ever prompts. up is the one two-line case: if it has to pair it first prints
{"event":"approval_required","verification_uri_complete":"https://webmcp.fast/activate?code=ABCD-EFGH","user_code":"ABCD-EFGH","expires_in":900}
(show that link to the human and keep the process running), and always ends with
{"event":"ready","handle":"alice","device":"studio","servers":[{"alias":"github","url":"https://alice.webmcp.fast/studio/github/mcp","env_carried":["GITHUB_TOKEN"]}],"service":"installed","next":"…"}
service is installed, skipped or unsupported; env_carried (names only) appears on servers this
run attached. On failure event is error with code (access_denied, expired_token, device_limit,
device_name_taken, hardware_already_paired, rate_limited, invalid_request, unexpected_response,
network, or error)
and message; handle/device are null if it failed before pairing. Any other command fails as
{"event":"error","code":"error","message":"…"}. Exit codes: 0 ok, 1 error, 2 approval declined,
3 approval expired. webmcp discover --json gives
{"servers":[{"name","alias","kind":"stdio"|"http","command","args","env":[names],"cwd","url","attachable","reason","sources":[{"client","path","project"}]}]}
(absent fields omitted); pass an entry's alias or name to up --attach. webmcp approvals --json
(also approvals on and approvals off) gives
{"require_approval":true,"approved":[{"id","kind","name","approved_at"}],"waiting":[{"id","kind","name","servers","last_seen"}]};
approve --json gives {"approved":[…]} with what it approved, approvals revoke --json gives {"revoked":{…}},
passkey --json gives {"event":"passkey_link","url":"…","expires_in":600}, and its failures carry the
gateway's code (bad_signature, clock_skew, unknown_device, rate_limited, invalid_request).
Manual commands
webmcp login --code ABCD-EFGH [--name laptop] [--base-url https://webmcp.fast] [--force] # pair with a dashboard code
webmcp connect [--once] # keep the relay open; --once = handshake + one ping/pong
webmcp status # config path, handle, device id, key fingerprint
webmcp discover # MCP servers found in other tools' configs (read-only)
webmcp attach fs [--env KEY=VAL]... [--cwd DIR] [--mode per-session|exclusive] [--max-sessions N] \
-- npx -y @modelcontextprotocol/server-filesystem /tmp
webmcp attach gnosys --stdio "npx -y @acme/gnosys" # same, command as one quoted string
webmcp attach web --http http://localhost:3000/mcp --mode shared
webmcp servers / webmcp detach <alias>
webmcp tools <alias> [all | read-only | allow <name>...]
webmcp confirm <alias> [never | destructive | always]
Tool permissions on this machine
webmcp tools fs read-only # only tools the server marks read-only
webmcp tools fs allow read_file list_dir
webmcp confirm fs destructive # Allow/Deny dialog before tools that can change things
These rules are enforced by the daemon, whatever the gateway sends: a tool this machine does not allow
never reaches the server and never appears in tools/list. The dashboard can narrow them further, never
widen them. confirm destructive asks for tools that are not marked read-only or harmless (MCP treats an
unmarked tool as able to change things); unanswered for 60 seconds, or with no desktop session to show the
dialog, the answer is Deny.
A stdio server starts with a clean environment: PATH, HOME, the user and shell names, locale, temp
directories, proxy and CA-bundle variables, plus whatever its --env sets. Keys in your shell such as
AWS_* or GITHUB_TOKEN are not passed on, because an agent can often make a server print its
environment. Name a variable with --env to give it to one server. That includes what a server may
need from your session and no longer gets by default, such as SSH_AUTH_SOCK for git over ssh,
DISPLAY or WAYLAND_DISPLAY for a GUI, or VIRTUAL_ENV and JAVA_HOME:
webmcp attach repo --env SSH_AUTH_SOCK="$SSH_AUTH_SOCK" -- npx -y @scope/git-server
Only one webmcp connect runs per device: it holds a lock (daemon.lock in the config directory) and a
second one exits naming the first one's pid. If another machine connects with a copy of this identity,
the gateway closes this connection with 1012 "replaced" and the daemon stops instead of fighting for
the socket (under the background service it exits cleanly, so launchd or systemd does not restart it).
Config lives in ~/.config/webmcp/ ($XDG_CONFIG_HOME, or ~/Library/Application Support/webmcp/
on macOS, %APPDATA%\webmcp\config on Windows; override with WEBMCP_CONFIG_DIR): config.toml and the
Ed25519 seed in device.key, both written 0600 because config.toml can hold copied API keys.
Logs go to stderr; set RUST_LOG=debug for frame-level detail. webmcp connect serves the
attached servers: a stdio server is spawned once per MCP session (exclusive: one session at a time) and
killed when the session closes, idles for 30 min or the relay connection drops. At the cap
(--max-sessions, default 4) the least recently used session with no request running is closed to
make room, and its client starts a new one; only when every session is busy is a new one refused; an --http server is
reached as an MCP Streamable HTTP client; for it --mode is recorded only, and --max-sessions still
applies. --mode shared over stdio is not supported yet. Device names follow the gateway's rule: 1-32 of
a-z, 0-9 and -, starting and ending with a letter or digit.
attach and detach take effect on a running webmcp connect within a couple of seconds, no restart:
it polls config.toml and re-advertises the server list. Sessions on a detached alias end with
detached, sessions on an alias whose definition changed end with reconfigured (the next session uses
the new one), everything else keeps running. A config that does not parse is ignored until it does.
Only servers and the approval settings (below) are reloaded; after re-pairing (webmcp up --force or
webmcp login --force), restart webmcp connect.
Approving new agents on this machine
webmcp approvals on # from now on a new agent needs approval here
webmcp approvals # on or off, what is approved, what is waiting
webmcp approve # pick from the waiting agents; or: webmcp approve <id>... | --all
webmcp approvals revoke <id> # withdraw one; its live sessions end
webmcp approvals off
By default an agent the owner connects on webmcp.fast (OAuth consent or a connector token) can use
this machine at once. With approvals on, a new agent credential opens no session here until someone
at this machine runs webmcp approve. Its first attempt is refused (approval_required), recorded in
agents.json next to the config (ids and names, never tokens), and announced by a desktop
notification on macOS, and on Linux when notify-send is installed, at most once per agent every 10
minutes (WEBMCP_NO_NOTIFY=1 turns notifications off). Approve only an agent you just connected
yourself: its name comes from whoever connected it. approvals on offers to keep the agents that
already connected, so nothing that works today breaks, and approvals off clears the approved list.
A running webmcp connect picks up every change within seconds; a revoked agent's sessions end.
The trade-off. On: a stolen webmcp.fast account, or a gateway bug that mints credentials, cannot add
an agent to this machine. Cost: each new agent needs webmcp approve here once, and approvals can
only be turned off here. The setting lives in config.toml, the gateway has no way to change it, and
pairing again keeps it. It does not protect against a compromised gateway, which could reuse an
approved agent's credential id (THREAT_MODEL.md).
Adding a passkey from your machine
webmcp passkey [--no-browser]
Once your account has a paired device, webmcp.fast accepts a first passkey, or one added without an
existing passkey to recover the account, only through a link started from that device. So someone who
knows your email cannot enroll their own passkey first. webmcp passkey signs the request with this
machine's device key, prints the link and tries to open it: open it in the browser where you are
signed in to <handle>.webmcp.fast, within 10 minutes, and add the passkey. The link works once.
Running in the background (macOS, Linux)
webmcp service install # start at login, restart if it stops
webmcp service status
webmcp service uninstall # pairing and attached servers are kept
On macOS this writes a per-user LaunchAgent (~/Library/LaunchAgents/fast.webmcp.daemon.plist,
no sudo) that runs webmcp connect; logs go to ~/Library/Logs/webmcp/daemon.log.
On Linux it writes a systemd user unit (~/.config/systemd/user/webmcp.service,
Restart=on-failure, no sudo) and runs systemctl --user daemon-reload && enable && restart; logs go to ~/.local/state/webmcp/daemon.log (and journalctl --user -u webmcp).
The service runs the binary it was installed from. If that binary lives inside a Node version
manager (nvm, fnm, asdf, Volta, n), switching Node versions removes it: reinstall webmcp and run
webmcp service install again. service install prints a note when this applies.
A user unit only runs while you have a session; loginctl enable-linger $USER
keeps it up after logout and across reboots. Both record your shell's PATH at
install time, because stdio servers are usually launched through npx, uvx or a
#!/usr/bin/env node shim that the supervisor's minimal PATH would not find: run
install again after upgrading webmcp or changing your PATH. If the gateway
says the device must be paired again, the service exits cleanly instead of looping
(neither supervisor restarts a clean exit); webmcp up --force --service.
Windows: the binary runs (webmcp connect in a terminal or under your own
supervisor), but webmcp service is not available there yet.