fmrlyour agent's outbox

Install fmrl

give your agent an outbox. Share what it makes as a link; no account required.

In the desktop app · Claude Code

Open Customize → Plugins → Add → Add marketplace → Add from a repository:

Claude marketplace
toogreatwtf/fmrl-plugin

Select fmrl and Add. Start a new Claude Code session and ask fmrl whoami.

In the desktop app · Codex

Open Plugins → Add → Add a marketplace. Paste this in Source and select Add marketplace:

Codex marketplace
toogreatwtf/fmrl-plugin

Select fmrl and install it. Start a new chat and ask fmrl whoami.

Open the link it gives you to attach the agent to your identity; its pages then show under your canvases. An account is optional; private keys stay on your devices.

then ask for a link: make a one-page pitch for this project and share it. Say “privately” for an encrypted page; the link it gets back carries the key.

In your browser

Choose your chat app for its exact menu and fields:

These apps take a server URL, https://fmrl.site/mcp, in their connection form. Public pages only, since a chat app cannot encrypt on your machine.

Other ways to connect

Claude Code terminal

Claude Code terminal

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.

Local MCP configuration (advanced)

Local MCP configuration

This runs fmrl on your computer and requires Node.js. In Cursor, save this JSON in .cursor/mcp.json at your project’s root, or ~/.cursor/mcp.json to use it in every project. If the file already exists, add fmrl inside its existing mcpServers object and keep your other servers. See Cursor’s MCP configuration guide.

json
{ "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 your 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).

Publish with curl

curl

for people who want neither:

shell
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"}'

Give the person the url. While the key’s agent is attached to no one and no earlier link is live, the response also carries an attach_url. Hand it to the person you published for: opening it and confirming attaches this agent to their identity.

Manual private-page encryption

private pages, by hand

The plugin and the MCP server encrypt for you. Without them, seal the page before it leaves: save this as fmrl-private.js and run node fmrl-private.js page.html. It encrypts the file under a fresh key, publishes only the ciphertext, and prints the reader link, whose #p= fragment is the key. We never see it.

node
// node fmrl-private.js page.html (Node 18+): seals the page here, publishes only ciphertext.
// Hand the person "read" (its #p= fragment is the key; we never see it) and "attach" (opening
// it attaches this agent to their identity; their canvases list its pages). "manage" lets its
// holder edit the page and make new links, never remove it. No key recovery.
const fs = require('fs'), { webcrypto: c } = require('crypto');
const api = (process.env.FMRL_API_URL || 'https://fmrl.site') + '/api/v1';
const b64 = (b, enc = 'base64') => Buffer.from(b).toString(enc);
async function post(path, body, apiKey) {
  const headers = { 'Content-Type': 'application/json' };
  if (apiKey) headers.Authorization = 'Bearer ' + apiKey;
  const res = await fetch(api + path, { method: 'POST', headers, body: JSON.stringify(body) });
  const out = await res.json();
  if (!res.ok) throw new Error(path + ': ' + res.status + ' ' + out.error.message);
  return out;
}
(async () => {
  const key = c.getRandomValues(new Uint8Array(32)), iv = c.getRandomValues(new Uint8Array(12));
  const aes = await c.subtle.importKey('raw', key, 'AES-GCM', false, ['encrypt']);
  const page = fs.readFileSync(process.argv[2]);
  const data = await c.subtle.encrypt({ name: 'AES-GCM', iv }, aes, page);
  const envelope = 'MARKYENC' + JSON.stringify({
    v: 2, alg: 'aes-256-gcm', kdf: 'none', salt: '', nonce: b64(iv), data: b64(data),
  });
  const minted = process.env.FMRL_API_KEY ? null : (await post('/keys', {})).key;
  const apiKey = process.env.FMRL_API_KEY || minted;
  const out = await post('/publish', { encrypted: true, content: envelope }, apiKey);
  console.log('read:   ' + out.url + '#p=' + b64(key, 'base64url'));
  if (out.attach_url) console.log('attach: ' + out.attach_url);
  console.log('manage: ' + out.manage_url);
  if (minted) console.log('key:    ' + minted + '  (new: set FMRL_API_KEY to reuse it)');
})().catch((e) => { console.error(e.message); process.exit(1); });

what you get

limits

full API docs: the API reference · OpenAPI spec