@formstr/mcp 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -10,6 +10,52 @@ first write. Then jump to the module you need.
10
10
 
11
11
  ---
12
12
 
13
+ ## Before you can do anything
14
+
15
+ You almost certainly cannot change this yourself — it is a one-time, human, out-of-band step.
16
+ But you should know what it is so you can tell the user exactly what's wrong if a tool is
17
+ missing or a command fails.
18
+
19
+ **1. A human signs in once** (interactive terminal; never in the chat):
20
+
21
+ ```bash
22
+ npx -y @formstr/mcp login
23
+ ```
24
+
25
+ They pick **Bunker URI (NIP-46)** for the best setup — the private key stays in their signer
26
+ app (Amber, nsec.app), only a session is stored, and no passphrase is ever needed in a config
27
+ file. The alternative, an `ncryptsec` key, unlocks with a passphrase supplied via the
28
+ `FORMSTR_MCP_NCRYPTSEC_PASSPHRASE` env var in the host config. Either way **the key never
29
+ reaches you**.
30
+
31
+ **2. The host starts the server.** The user adds an entry to their MCP host config
32
+ (`claude_desktop_config.json`, Cursor's `~/.cursor/mcp.json`, Goose, …):
33
+
34
+ ```json
35
+ {
36
+ "mcpServers": {
37
+ "formstr": {
38
+ "command": "npx",
39
+ "args": ["-y", "@formstr/mcp", "--allow-writes"]
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ - **`--allow-writes` is what registers the gated tools.** Without it, tools like `send_mail`,
46
+ `delete_form`, `update_page`, `share_form` are **absent from your tool list entirely** — not
47
+ disabled, just not there. If a write tool the user expects is missing, this is why: tell them
48
+ to add the flag and restart the host.
49
+ - The flag does **not** make anything automatic — every gated tool still requires you to pass
50
+ `confirm: true`, and you should only do that after the user agrees (see
51
+ [the confirm gate](#the-confirm-gate)).
52
+ - Add `"--relays", "wss://a,wss://b"` to override the relay set if the user asks.
53
+
54
+ That's the whole setup. If the tools are present and `list_*` calls work, you're good — the
55
+ sections below are everything else.
56
+
57
+ ---
58
+
13
59
  ## Golden rules
14
60
 
15
61
  1. **Never invent an id.** Every tool that acts on existing data takes an id, pubkey, or
@@ -201,8 +247,8 @@ types: `short`, `paragraph`, `choice`, `dropdown`, `number`, `date`, `time`, `gr
201
247
 
202
248
  | Tool | Required args | Notes |
203
249
  | --- | --- | --- |
204
- | `list_mail` | — | Inbox, newest first: `id`, sender, subject, date. |
205
- | `read_mail` | `mailId` | Full body of one message. |
250
+ | `list_mail` | — | A page of the inbox, newest first. Page with `until`. |
251
+ | `read_mail` | `mailId` | Full body of one message (fetched by id). |
206
252
  | `who_is_my_mail_address` | — | Signed-in identity, default `From:`, and aliases. |
207
253
  | `list_mail_aliases` | — | Every address the account can send as, and the default. |
208
254
  | `send_mail` ⚠ | `to` | `to` = npub/hex **or** external email. Gated. |
@@ -216,6 +262,12 @@ choose which alias appears in the `From:`; call `list_mail_aliases` first to see
216
262
  `raw`. For external email, the `From:` must be a registered alias (not the bare npub) or the
217
263
  bridge bounces it.
218
264
 
265
+ **Paging a large inbox.** `list_mail` returns a bounded **page** (default 50, newest first) —
266
+ it does not scan the whole mailbox. When the page is full the result includes `oldestReceivedAt`
267
+ and `hasMore: true`; to read further back, call again with `until` set to that value. `since`
268
+ and `until` are unix seconds. If you only need one message you already have the id for, call
269
+ `read_mail` — it fetches that exact wrap and never depends on the page window.
270
+
219
271
  > **Claiming is two-step and human-driven.** `claim_mailbox` returns a bolt11 invoice. The
220
272
  > user pays it in their own wallet. Then the address starts working once NIP-05 propagates.
221
273
  > You **cannot** complete payment from here.
@@ -292,9 +344,18 @@ read_mail { mailId: "<id from list>" }
292
344
 
293
345
  ---
294
346
 
295
- ## For the operator (setup, not for the agent)
347
+ ## Pointing other agents here
348
+
349
+ This guide is the single entry point. Canonical source is the ngit repository; GitHub is a
350
+ read-only mirror.
351
+
352
+ - **Agent-readable (raw Markdown — what you want to feed a model):**
353
+ `https://raw.githubusercontent.com/formstr-hq/common-packages/main/packages/mcp/AGENTS.md`
354
+ - **Human-readable (rendered):**
355
+ `https://github.com/formstr-hq/common-packages/blob/main/packages/mcp/AGENTS.md`
356
+ - **Shipped in the package too:** `AGENTS.md` is included in the `@formstr/mcp` npm tarball, so
357
+ `node_modules/@formstr/mcp/AGENTS.md` exists after any install.
358
+
359
+ For deeper operator detail — keystore internals, the full environment-variable and CLI-flag
360
+ reference, Ollama/Goose setup, troubleshooting — see [`README.md`](./README.md).
296
361
 
297
- See [`README.md`](./README.md) for installation, `formstr-mcp login`, host configuration
298
- (`claude_desktop_config.json`, Cursor, Goose/Ollama), the passphrase env var, and the full
299
- environment-variable and CLI-flag reference. In short: `npx -y @formstr/mcp`; add
300
- `"--allow-writes"` to enable gated tools.
package/README.md CHANGED
@@ -13,8 +13,10 @@ the same login engine the Formstr web app uses. Local keys are stored **NIP-49 e
13
13
  persisted. Remote keys stay in your NIP-46 signer.
14
14
 
15
15
  > **Driving this from an AI agent?** Read [`AGENTS.md`](./AGENTS.md) — a task-oriented guide to
16
- > every tool, the `confirm` gate, id/coordinate formats, and worked recipes. This README is the
17
- > operator's setup guide.
16
+ > every tool, the `confirm` gate, id/coordinate formats, and worked recipes. It is
17
+ > self-contained (setup included), so you can point a model straight at the raw file:
18
+ > `https://raw.githubusercontent.com/formstr-hq/common-packages/main/packages/mcp/AGENTS.md`.
19
+ > This README is the operator's setup guide.
18
20
 
19
21
  ## Quick start
20
22
 
package/dist/index.js CHANGED
@@ -33501,7 +33501,14 @@ async function collectInbox(recipient, opts, unwrap) {
33501
33501
  const filter = {
33502
33502
  kinds: [KIND_GIFTWRAP],
33503
33503
  "#p": [recipient],
33504
- limit: opts.limit ?? 100
33504
+ limit: opts.limit ?? 100,
33505
+ // Time-window paging. Both optional: absent `until` means "newest", absent
33506
+ // `since` means "all the way back". Together they turn a potentially huge
33507
+ // inbox scan into bounded pages (oldest receivedAt → next `until`).
33508
+ ...opts.since !== void 0 ? { since: opts.since } : {},
33509
+ ...opts.until !== void 0 ? { until: opts.until } : {},
33510
+ // Exact lookup by id — no time window, for reading one known message.
33511
+ ...opts.ids !== void 0 ? { ids: opts.ids } : {}
33505
33512
  };
33506
33513
  const wraps = opts.queryWraps ? await opts.queryWraps(filter) : await new SimplePool().querySync(relays, filter);
33507
33514
  const seen = /* @__PURE__ */ new Map();
@@ -33544,7 +33551,8 @@ async function readInboxWith(signer, opts = {}) {
33544
33551
  recipient,
33545
33552
  opts,
33546
33553
  (wrap) => unwrapMailWith(wrap, signer, {
33547
- maxAgeSeconds: opts.maxAgeSeconds,
33554
+ // See readInbox: the inbox has no staleness bound by default.
33555
+ maxAgeSeconds: opts.maxAgeSeconds ?? Infinity,
33548
33556
  acceptKinds: opts.acceptKinds,
33549
33557
  now: opts.now
33550
33558
  })
@@ -33777,7 +33785,7 @@ async function publishEverywhere(pool, relays, event) {
33777
33785
  };
33778
33786
  }
33779
33787
 
33780
- // ../agent/dist/chunk-5L26ARX5.js
33788
+ // ../agent/dist/chunk-CCSELOJL.js
33781
33789
  var service_exports2 = {};
33782
33790
  __export3(service_exports2, {
33783
33791
  defaultSenderAddress: () => defaultSenderAddress,
@@ -33785,6 +33793,7 @@ __export3(service_exports2, {
33785
33793
  mailIdentity: () => mailIdentity,
33786
33794
  publishMailSetup: () => publishMailSetup,
33787
33795
  readMail: () => readMail,
33796
+ readMailById: () => readMailById,
33788
33797
  requestMailbox: () => requestMailbox,
33789
33798
  sendMail: () => sendMail
33790
33799
  });
@@ -33811,11 +33820,27 @@ async function defaultSenderAddress() {
33811
33820
  }
33812
33821
  async function readMail(opts = {}) {
33813
33822
  const signer = await mailSigner();
33823
+ const limit = opts.limit ?? 50;
33814
33824
  const { mail, failures } = await readInboxWith(signer, {
33815
33825
  relays: inboxRelays(),
33816
- ...opts.limit !== void 0 ? { limit: opts.limit } : {}
33826
+ limit,
33827
+ ...opts.since !== void 0 ? { since: opts.since } : {},
33828
+ ...opts.until !== void 0 ? { until: opts.until } : {}
33817
33829
  });
33818
- return { mail, failures };
33830
+ mail.sort((a, b) => b.receivedAt - a.receivedAt);
33831
+ return {
33832
+ mail,
33833
+ failures,
33834
+ oldestReceivedAt: mail.length ? mail[mail.length - 1].receivedAt : void 0,
33835
+ // A full page means there are probably older messages to page to. We cannot
33836
+ // know for certain without a second query; this is a cheap, honest hint.
33837
+ hasMore: mail.length >= limit
33838
+ };
33839
+ }
33840
+ async function readMailById(mailId) {
33841
+ const signer = await mailSigner();
33842
+ const { mail } = await readInboxWith(signer, { relays: inboxRelays(), ids: [mailId] });
33843
+ return mail[0] ?? null;
33819
33844
  }
33820
33845
  async function sendMail(params) {
33821
33846
  const signer = await mailSigner();
@@ -44770,7 +44795,7 @@ var coerce = {
44770
44795
  };
44771
44796
  var NEVER = INVALID;
44772
44797
 
44773
- // ../agent/dist/chunk-JORUNTXB.js
44798
+ // ../agent/dist/chunk-XHSE23RE.js
44774
44799
  function ok(text, data) {
44775
44800
  return data !== void 0 ? { ok: true, text, data } : { ok: true, text };
44776
44801
  }
@@ -45967,25 +45992,35 @@ function buildMailTools() {
45967
45992
  server.registerTool(
45968
45993
  "list_mail",
45969
45994
  {
45970
- description: "List the mail in your mailstr inbox (kind-1059 gift-wrapped email). Returns each message's id, sender, subject and date, newest first.",
45971
- inputSchema: { limit: external_exports.number().optional() }
45995
+ description: "List a page of the mailstr inbox (kind-1059 gift-wrapped email), newest first. Returns each message's id, sender, subject and date. Pages are bounded (default 50); when the page is full the result carries `oldestReceivedAt` \u2014 pass it back as `until` to page further back. `since`/`until` are unix seconds; omitted `until` means newest.",
45996
+ inputSchema: {
45997
+ limit: external_exports.number().optional(),
45998
+ since: external_exports.number().optional(),
45999
+ until: external_exports.number().optional()
46000
+ }
45972
46001
  },
45973
- async ({ limit }) => {
45974
- const { mail: messages, failures } = await service_exports2.readMail(
45975
- limit !== void 0 ? { limit } : {}
45976
- );
45977
- messages.sort((a, b) => b.receivedAt - a.receivedAt);
46002
+ async ({ limit, since, until }) => {
46003
+ const { mail: messages, failures, oldestReceivedAt, hasMore } = await service_exports2.readMail({
46004
+ ...limit !== void 0 ? { limit } : {},
46005
+ ...since !== void 0 ? { since } : {},
46006
+ ...until !== void 0 ? { until } : {}
46007
+ });
45978
46008
  const rows = messages.map((m) => ({
45979
46009
  id: m.wrapId,
45980
46010
  from: m.from,
45981
46011
  subject: m.subject,
45982
46012
  date: new Date(m.receivedAt * 1e3).toISOString()
45983
46013
  }));
45984
- let text = `${messages.length} message(s) in your inbox.`;
46014
+ let text = `${messages.length} message(s) in this page of your inbox.`;
46015
+ if (hasMore && oldestReceivedAt !== void 0) {
46016
+ text += ` Older mail may exist \u2014 call list_mail again with until: ${oldestReceivedAt}.`;
46017
+ }
45985
46018
  if (failures.length > 0) text += ` ${failures.length} wrap(s) could not be decoded.`;
45986
46019
  return ok(text, {
45987
46020
  mail: rows,
45988
46021
  count: messages.length,
46022
+ oldestReceivedAt,
46023
+ hasMore,
45989
46024
  failures
45990
46025
  });
45991
46026
  }
@@ -45997,8 +46032,7 @@ function buildMailTools() {
45997
46032
  inputSchema: { mailId: external_exports.string() }
45998
46033
  },
45999
46034
  async ({ mailId }) => {
46000
- const { mail: messages } = await service_exports2.readMail();
46001
- const msg = messages.find((m) => m.wrapId === mailId);
46035
+ const msg = await service_exports2.readMailById(mailId);
46002
46036
  if (!msg) {
46003
46037
  return fail(
46004
46038
  `No message with id "${mailId}". Use list_mail to see the ids in your inbox.`,