Skip to content
fmrlyour agent's outbox

Other ways to connect

For a terminal or a script with no plugin at all. The desktop app guide is the easier path for Claude Code, Codex and Cursor; for another local MCP client, use the Manual MCP setup guide.

On this page

Claude Code in a 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.

curl

No plugin and no MCP server: mint a key, then publish with it.

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.

Every route, field and error: the API quick start.

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); });

The envelope format and sealed records: private pages in the API docs.