fmrl
Share anything your AI made. fmrl is a Claude Code plugin and an MCP server (fmrl-mcp) that publish a page to fmrl.site and hand back a link. No account; the page lasts seven days unless someone keeps it from the page itself — a private page cannot be kept.
Install
Claude Code:
/plugin marketplace add toogreatwtf/fmrl-plugin
/plugin install fmrl@fmrl-plugin
Keep it current: auto-update is your switch — no marketplace can set it for you. The plugin offers once to turn it on; you can flip it yourself in /plugin → Marketplaces → fmrl-plugin → Enable auto-update. In the Claude desktop app the switch does nothing: update by hand, or when the plugin says it is out of date. By hand: claude plugin marketplace update fmrl-plugin, then claude plugin update fmrl@fmrl-plugin, and restart Claude Code.
Any MCP client (Claude Desktop, Cursor, Codex, Windsurf, and the rest):
{ "mcpServers": { "fmrl": { "command": "npx", "args": ["-y", "fmrl-mcp"] } } }
No key step. The server mints a key the first time it publishes and keeps it in the user's config directory. FMRL_API_KEY overrides it; FMRL_API_URL points the server at a preview or a local make dev (default https://fmrl.site); FMRL_AGENT_NAME names the revisions this key makes, replacing any name already set (without it, the MCP client's own name fills an unset one).
curl, for people who want neither:
curl -sX POST https://fmrl.site/api/v1/keys
curl -sX POST https://fmrl.site/api/v1/publish \
-H "Authorization: Bearer fmrl_…" -H "Content-Type: application/json" \
-d '{"format":"md","content":"# hello"}'
What you get
In Claude Code, /fmrl:share publishes what is at hand — a file you named or something the agent composed — and replies with the link, the expiry, and a recovery link that grants management and editing, never deletion. Before publishing a canvas that hands work to someone else to review, it offers the handoff-review starter — eight sections and a fmrl-profile block recording their shape, readable at fmrl.site/h4ndrv. /fmrl:whoami shows the key, this month's quota, where the key ring is kept, and whether the plugin is up to date. /fmrl:status answers at once from local state — fmrl-mcp and plugin versions, what launched the server, the key and ring on this machine, the last API call — and /fmrl:status verbose adds the network checks: whether fmrl.site answers, quota, inbox, and the newest fmrl-mcp on npm.
Any MCP client gets twelve tools:
Keys and limits
The server mints a key on first use and stores it in ~/.config/fmrl/credentials.json ($XDG_CONFIG_HOME/fmrl/credentials.json; %APPDATA%\fmrl\credentials.json on Windows), one key per API base URL, file mode 0600. FMRL_API_KEY overrides the file, and then fmrl_redeem and fmrl_rotate refuse rather than store a key it would shadow. A stored key that stops working is replaced with a fresh one on the next call, except that fmrl_whoami reports a revoked key first; a rotated key keeps its prefix, so its key ring still applies. A private page's key travels with it, sealed under a key ring kept in the same file (FMRL_RING overrides it) and handed to a browser only inside the link, so fmrl.site stores the sealed record and cannot open it; back up that file to keep every page's key. In a container that sets FMRL_API_KEY on a filesystem that does not persist, set FMRL_RING too — 32 random bytes as unpadded base64url, e.g. node -e "console.log(require('node:crypto').randomBytes(32).toString('base64url'))" — or every restart starts a new ring. FMRL_AGENT_NAME names the revisions this key makes on fmrl.site (so other editors know who made them); without it, the MCP client's own name fills a key that has none. Beside credentials.json, pages.json (same directory, same protections) remembers, per page this machine has opened or edited, its content key (private pages only) and any manage token that worked — so a page's link or manage link needs pasting only once. 25 publishes a month per key, 5 keys a day per network, 2 MiB per page — a private page's cap is on the sealed envelope, so roughly 1.4 MB of HTML fits. Content is subject to fmrl.site's acceptable use policy. Full API reference: fmrl.site/api.
Account vault ring sync
Sharing requires no account. When a key belongs to an account with private-page sync enabled, the local MCP server seals credential-file rings to the advertised account vault. Every successful public or private fmrl_publish or fmrl_publish_file makes a fresh GET /api/v1/me after publishing; every fmrl_whoami uses its own fresh response and syncs too. Nothing about the vault goes through the hosted /mcp endpoint.
The server validates the vault public key and recomputes its fingerprint before pinning it on first use (TOFU). Pins are stored per viewer origin in credentials.json, still version 1, with additive vault_pins alongside existing keys and rings. Historical slots now also record ring_origins, a locally established origin for each ring. A changed fingerprint stops uploads until the person explicitly confirms the fingerprint on /account and supplies that exact grouped value through fmrl_whoami's optional trust_vault. The agent must never trust a changed fingerprint automatically.
Only rings with local provenance for the configured origin are eligible: stored keys under that origin (including URL aliases), and historical slots whose ring_origins entry names it. Legacy history with no origin stays on disk and is excluded unless its exact prefix/ring pair matches a same-origin stored key. A server advertising a matching prefix cannot authorize exporting another origin's ring. This reduces recovery coverage for unscoped legacy rings; they require the original backup until an explicit migration is available.
The actual active ring, if eligible and stored in the credentials file, goes first in its own request. Superseded variants under that active prefix remain local and are not uploaded, preventing them from exhausting the budget or overwriting the active box. Other eligible prefixes retain numeric historical order with stored-key variants last. The vault retains one box per prefix, so it cannot recover every historical variant.
Each PUT /api/v1/me/rings contains at most 50 boxes and no repeated prefix. Requests share the edit budget (API_EDITS_PER_HOUR, default 60/hour), not the publish quota. Only on atomic HTTP 400 ring_ownership rejection, the client tries each member of a multi-row batch once separately and continues other prefixes; a refused singleton is not retried. The active request costs one edit; isolating a refused N-row batch costs up to 1+N requests. Each pass is capped at 10 PUT attempts and stops immediately on 429 or any other operational failure. Each call can consume 10 shared edit tokens; repeated calls still consume the hourly budget. The next authorized tool call starts afresh; there is no throttle, boxed-ring cache or background timer. HTTP 403/409 refusals remain silent; 409 always leaves status unknown. Credential recovery preserves the moved-aside file and emits only a fixed warning. Refused, unfinished or partially uploaded requested scope is unknown and produces no synced/not-synced claim. Malformed vault data, persistence failures and network/5xx failures preserve the original tool success and log a fixed non-secret diagnostic. A primary whoami /me failure retains its normal error/revocation behavior.
FMRL_RING is never added to the file snapshot. An environment-only or different override excludes stale file variants for the active prefix and omits a sync-status claim based on those uploads. An unavailable vault may still report the approved not-synced status. An override identical to an eligible stored active-prefix ring can qualify for the approved synced line after successful upload. An empty collection sends no PUT and cannot establish synced status. Keep backing up the credentials file and any FMRL_RING value: unknown-origin history, superseded variants and unclaimed keys may still require the original backup, a keyed link or a browser that already holds the ring.
Production code only seals ring boxes; it has no vault opener or vault private-key input. The server receives sealed boxes, never plaintext rings, vault private keys, recovery codes or PRF outputs. Local tool output omits account IDs and vault material, while retaining approved grouped fingerprint notices and the existing browser #r= and private-page #p= links.
Ring uploads include the validated, pinned fingerprint used to seal every box. A server key change returns 409 vault_changed; the client never automatically retries. 400 bad_request stops the pass without isolation. account_vault: null preserves whoami identity/quota/link data but leaves vault sync unknown with a fixed diagnostic.
Development
cd packages/mcp && npm install && npm test && npm run build
claude --plugin-dir ./plugins/fmrl # from the repo root; npm link in packages/mcp first so npx finds the local build
Pull requests open as drafts and are marked ready when npm test in packages/mcp is green; the test workflow (.github/workflows/test.yml) runs the same suite and build on every PR and on main, on Node 20 and 22. CodeRabbit runs on a metered allowance and .coderabbit.yaml skips drafts and reviews only when a PR opens or is marked ready, never on each push, so one review is spent per PR (none for docs: titles) and one more on @coderabbitai review after fixes.
Releasing is a version bump. plugins/fmrl/.mcp.json runs npx -y fmrl-mcp unpinned, so the npm publish is the deploy: every installed plugin runs the new server the next time it starts. The release workflow (.github/workflows/release.yml) runs on every merge to main that touches packages/mcp; when packages/mcp/package.json names a version npm does not have yet, it publishes it through npm Trusted Publishing (GitHub OIDC, no token in secrets, a provenance attestation back to the commit), and when the version is already on npm it does nothing. So a release PR is the change plus npm version <x.y.z> --no-git-tag-version in packages/mcp and the same version in plugins/fmrl/.claude-plugin/plugin.json — the manifest tracks the server on major.minor, and the test suite checks that they agree.
License
MIT, Too Great LLC.
Identity-server compatibility release hold
The approved sequence is to ship the compatibility bridge before the identity-server cutover. The bridge supports both server contracts so clients can update while the legacy server is still running. Package release is not yet approved: merge, version bump, package publication and production cutover each remain behind their applicable approval gates. A new package version merged to main publishes automatically; this documentation is not authorization to do so.
For editing someone else's page, supply its page ID or URL as id and its recovery token or /c/<token> link separately as manage_token. A recovery link does not identify the page: ask its sender for the page URL too. The plugin never opens or spends a recovery link to discover the page. Recovery grants management and editing, never deletion. Private page keys remain in the page URL's #p= fragment and on this machine.
Transitional server compatibility
The compatibility bridge accepts both legacy browser-link fields (linked_at,
link_url, and older editor name) and identity fields (attached_at,
attach_url, label/display_name, and publish recovery). Linking a browser is
not reported as attaching an identity. A missing recovery state is unknown;
spent or revoked links are not advertised as usable or saved for later edits.
Legacy management links keep their existing local token handling. The bridge
never opens a recovery link to find a page or replaces an attach link automatically.
Courtesy attach links seen during setup, status probes, or vault checks stay in process memory per API origin and credential for at most one hour. Publish and whoami can show that previously supplied link with the local ring fragment. A reported attachment clears it. Restarting loses this cache; another process or a replacement link can also make a cached link unusable. No capability is persisted or minted to repair that situation.
HTTP responses are projected onto closed MCP schemas; declared fields still
validate. A successful publish with an invalid response, or a timeout with an
uncertain outcome, must not be automatically republished. Use fmrl_list and the
saved page information to reconcile first. Publish idempotency remains absent:
this client sends no Idempotency-Key.
Before cutover, obtain the separate bridge release approval, publish the approved
version, and have clients update and restart/reconnect. Verify the running
fmrl_status version, not just the installed package: publishing does not replace
an already running MCP process or its cached schemas. Only then assess the
cutover readiness gates and seek the separate cutover approval. This order does
not prove that every client has updated. An old process can still reject a
new-server response after a write succeeds; reconcile rather than automatically
republishing. Passing tests or implementation review does not authorize a merge,
package release, deployment or cutover.