@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 +68 -7
- package/README.md +4 -2
- package/dist/index.js +50 -16
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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` | — |
|
|
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
|
-
##
|
|
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.
|
|
17
|
-
>
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
33826
|
+
limit,
|
|
33827
|
+
...opts.since !== void 0 ? { since: opts.since } : {},
|
|
33828
|
+
...opts.until !== void 0 ? { until: opts.until } : {}
|
|
33817
33829
|
});
|
|
33818
|
-
|
|
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-
|
|
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
|
|
45971
|
-
inputSchema: {
|
|
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
|
-
|
|
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
|
|
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.`,
|