@formstr/mcp 0.7.0 → 0.7.2

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.
Files changed (3) hide show
  1. package/AGENTS.md +355 -0
  2. package/README.md +6 -0
  3. package/package.json +5 -4
package/AGENTS.md ADDED
@@ -0,0 +1,355 @@
1
+ # Formstr MCP — Agent Guide
2
+
3
+ You are connected to `@formstr/mcp`, a Model Context Protocol server that gives you tools to
4
+ manage a person's **Formstr** account: forms, calendar, pages, drive, polls, and Mailstr email.
5
+ Everything is stored on Nostr relays under the user's own identity. There is no backend you can
6
+ query for help — **this document is the contract**.
7
+
8
+ Read the [Golden rules](#golden-rules) and the [confirm gate](#the-confirm-gate) before your
9
+ first write. Then jump to the module you need.
10
+
11
+ ---
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
+
59
+ ## Golden rules
60
+
61
+ 1. **Never invent an id.** Every tool that acts on existing data takes an id, pubkey, or
62
+ `coordinate` returned by a *list/get* tool. Chain reads before writes: list → pick → act.
63
+ 2. **A write tool needs `confirm: true`.** Without it the tool returns a "Confirmation
64
+ required…" message and does nothing. See [the confirm gate](#the-confirm-gate).
65
+ 3. **Creating is not gated, but it is real and public-permanent.** `create_*` tools are always
66
+ on and publish immediately on the user's identity. Don't create things the user didn't ask
67
+ for.
68
+ 4. **Addresses and coordinates are exact strings.** Coordinates look like `kind:pubkey:d`.
69
+ naddr/npub are bech32. Don't "fix" or reformat them.
70
+ 5. **Report outcomes honestly.** Tools return `ok`/`errorCode`. Relay publishes return per-relay
71
+ results; partial success is normal. Say what actually happened, don't claim success on a
72
+ failed call.
73
+ 6. **The user's key never reaches you.** No tool returns secrets. If you ever see key material
74
+ in output, stop and tell the user — it's a bug.
75
+ 7. **Prefer the smallest action.** Use list/get to confirm before update/delete. Never delete or
76
+ overwrite without the user's explicit go-ahead in the conversation.
77
+
78
+ ---
79
+
80
+ ## The confirm gate
81
+
82
+ Roughly half the tools are marked **gated** (destructive, outward, or identity-changing). Each
83
+ is registered *and* requires `confirm: true` on the call.
84
+
85
+ ### How to use it safely
86
+
87
+ The first call **without** `confirm` is a free preview: it returns exactly what would happen
88
+ and executes nothing.
89
+
90
+ ```
91
+ send_mail({ to: "alice@example.com", subject: "Hi", text: "…" })
92
+ → { ok: false, text: "Confirmation required for \"send_mail\". This action is irreversible
93
+ and acts on your Nostr identity: sends email to \"alice@example.com\". Re-call with
94
+ \"confirm\": true to proceed." }
95
+ ```
96
+
97
+ The correct pattern is:
98
+
99
+ 1. Call the gated tool **once without `confirm`** (or just describe the effect to the user).
100
+ 2. Show the user what will happen and get their agreement **in the conversation**.
101
+ 3. Call again with `confirm: true`.
102
+
103
+ Do **not** silently pass `confirm: true` on the first try for anything destructive. For
104
+ outward actions (send mail, share) or deletions, always surface the effect to the user first.
105
+
106
+ ### What is gated
107
+
108
+ Gated (`confirm: true` required) — 27 of 60 tools:
109
+ `update_form`, `share_form`, `delete_form`, `submit_form_response`,
110
+ `delete_calendar_event`, `update_calendar_event`, `attach_form_to_event`, `update_calendar`,
111
+ `delete_calendar`, `add_event_to_calendar`, `remove_event_from_calendar`, `approve_booking`,
112
+ `decline_booking`, `rsvp_event`, `delete_page`, `update_page`, `share_page`, `add_page_comment`,
113
+ `delete_poll`, `clear_my_vote`, `submit_poll_response`, `delete_file`, `rename_file`, `move_file`,
114
+ `send_mail`, `claim_mailbox`, `publish_mail_setup`.
115
+
116
+ The other 33 are always on. **"Always on" still means it publishes to the user's identity** —
117
+ `create_*`, `save_private_note`, `import_form_from_naddr`, and `submit_*` are real writes even
118
+ though they don't need `confirm`.
119
+
120
+ > If a gated tool is **absent** from your tool list entirely, the server was started without
121
+ > `--allow-writes`. Tell the user; you cannot enable it from here.
122
+
123
+ ---
124
+
125
+ ## Addresses, ids and coordinates
126
+
127
+ | Thing | Format | Example | Produced by |
128
+ | --- | --- | --- | --- |
129
+ | Form | `formId` + author `pubkey` | `a1b2…` / `c3d4…` | `list_forms`, `create_form` |
130
+ | Form reference | `naddr1…`, `pubkey:formId`, or `kind:pubkey:formId` | — | `create_form` (`naddr`) |
131
+ | Calendar event / calendar | `coordinate` = `kind:pubkey:d` | `31923:<pk>:my-event` | `list_calendar_events`, `create_calendar_event` |
132
+ | Calendar list | its `id` (d-tag) | `work` | `list_calendars`, `create_calendar` |
133
+ | Page / doc | `docId` (d-tag) + author `pubkey`, or an `address` | — | `list_pages`, `create_page` |
134
+ | Poll | `pollEventId` | `ab12…` | `list_polls`, `create_poll` |
135
+ | Drive file | `name` (+ optional `folder`) | `report.pdf` | `browse_files` |
136
+ | Mail message | `mailId` (the wrap id) | `ef56…` | `list_mail` |
137
+
138
+ **Private/encrypted things need a view key.** Forms, pages, and private calendar events can be
139
+ encrypted; pass `viewKey` (an `nsec`) where a tool offers it or you will get ciphertext or an
140
+ error. `list_shared_pages` returns the `viewKey` for documents shared with the user.
141
+ `get_calendar_event` reports `registrationFormHasViewKey` so you can check an attached form is
142
+ readable.
143
+
144
+ ---
145
+
146
+ ## Tool catalog
147
+
148
+ ### Forms (9)
149
+
150
+ | Tool | Required args | Notes |
151
+ | --- | --- | --- |
152
+ | `list_forms` | — | The user's forms index. Start here. |
153
+ | `get_form` | `pubkey`, `formId` | Pass `viewKey` for encrypted forms. |
154
+ | `fetch_form_responses` | `formAuthorPubkey`, `formId` | Submissions with responder + answers. |
155
+ | `create_form` | `name`, `fields` | Always on. Returns `formId`, `pubkey`, `naddr`. |
156
+ | `import_form_from_naddr` | `ref` | `naddr1…`, `pubkey:formId`, or `kind:pubkey:formId`. |
157
+ | `update_form` ⚠ | `formId`, `formPubkey` | Republish name/fields/description. Gated. |
158
+ | `share_form` ⚠ | `formId`, `formPubkey`, `recipients` | Gift-wraps the **view key**; `editors` also get edit access. Gated. |
159
+ | `delete_form` ⚠ | `formId`, `formPubkey` | NIP-09 deletion. Gated. |
160
+ | `submit_form_response` ⚠ | `formAuthorPubkey`, `formId`, `answers` | Submits on the user's identity. Gated. |
161
+
162
+ `create_form` fields: each is `{ type, label, required?, options?, validation?, … }`. Supported
163
+ types: `short`, `paragraph`, `choice`, `dropdown`, `number`, `date`, `time`, `grid`, `file`,
164
+ `signature`, `section`. Optional: `description`, `publicForm`, `encrypted`,
165
+ `allowedResponders`, `collaborators`, `notifyNpubs`, `titleImageUrl`, `coverImageUrl`,
166
+ `thankYouText`.
167
+
168
+ > **Encrypted form?** After `create_form` with `encrypted: true`, share it with
169
+ > `share_form` so the people you want can read it — otherwise only the creator can.
170
+
171
+ ### Calendar (19)
172
+
173
+ | Tool | Required args | Notes |
174
+ | --- | --- | --- |
175
+ | `list_calendar_events` | — | Optional ISO-8601 `since`/`until`. |
176
+ | `get_calendar_event` | `coordinate` | |
177
+ | `create_calendar_event` | `title`, `start` | **Defaults to PRIVATE.** See note below. |
178
+ | `list_calendars` | — | |
179
+ | `create_calendar` | `title` | A calendar *list* (like a folder). |
180
+ | `fetch_event_rsvps` | `coordinate` | |
181
+ | `list_invitations` | — | NIP-59 invitations received. |
182
+ | `list_scheduling_pages` | — | Booking links, each with a shareable URL. |
183
+ | `list_booking_requests` | — | Incoming appointment requests. |
184
+ | `approve_booking` ⚠ | `requestId`, `calendarId` | Creates the appointment, notifies booker. Gated. |
185
+ | `decline_booking` ⚠ | `requestId` | Gated. |
186
+ | `delete_calendar_event` ⚠ | `eventId` | Gated. |
187
+ | `rsvp_event` ⚠ | `eventCoordinate`, `status` | Gated. Optional suggested time/comment. |
188
+ | `update_calendar_event` ⚠ | `coordinate` | Only send changed fields. Gated. |
189
+ | `attach_form_to_event` ⚠ | `coordinate`, `formRef` | Gated. Pass `formViewKey` for encrypted forms. |
190
+ | `update_calendar` ⚠ | `id` | Gated. |
191
+ | `delete_calendar` ⚠ | `coordinate` | Gated. |
192
+ | `add_event_to_calendar` ⚠ | `calendarId`, `coordinate` | Gated. |
193
+ | `remove_event_from_calendar` ⚠ | `calendarId`, `coordinate` | Gated. |
194
+
195
+ > **`create_calendar_event` workflow (important).** Events are linked into a calendar list;
196
+ > that link is the only way they render on calendar.formstr.app. If you omit `calendarId` and
197
+ > the user already has calendars, the tool returns the list and a `CALENDAR_REQUIRED` code —
198
+ > **ask the user which calendar**, then re-run with `calendarId`. `isPrivate:false` makes a
199
+ > public unencrypted event, which does **not** sync to calendar.formstr.app. `participants`
200
+ > (npub/hex) receive NIP-59 invitations. To attach a registration form, pass
201
+ > `registrationFormRef` and, for encrypted forms, `registrationFormViewKey`.
202
+
203
+ ### Pages (12)
204
+
205
+ | Tool | Required args | Notes |
206
+ | --- | --- | --- |
207
+ | `list_pages` | — | |
208
+ | `get_page` | `pubkey`, `docId` | Pass `viewKey` if encrypted. |
209
+ | `list_shared_pages` | — | Docs shared with the user (carries `viewKey`). |
210
+ | `get_page_tags` | `address` | Private labels. |
211
+ | `list_page_comments` | `address`, `viewKey` | Inline comments/suggestions (kind 1494). |
212
+ | `create_page` | `title`, `content` | Markdown, encrypted. Always on. |
213
+ | `save_private_note` | `title`, `content` | Quick encrypted note. Always on. |
214
+ | `set_page_tags` | `address`, `tags` | Always on. |
215
+ | `update_page` ⚠ | `docId`, `content` | Replaces content. Gated. |
216
+ | `delete_page` ⚠ | `address` | NIP-09. Gated. |
217
+ | `share_page` ⚠ | `address`, `content` | Re-encrypts under a view key; `canEdit` for edit links. Gated. |
218
+ | `add_page_comment` ⚠ | `address`, `eventId`, `viewKey`, `content` | Gated. |
219
+
220
+ ### Polls (8)
221
+
222
+ | Tool | Required args | Notes |
223
+ | --- | --- | --- |
224
+ | `list_polls` | — | User's own polls. |
225
+ | `list_recent_polls` | — | Public polls to discover. Optional `limit`. |
226
+ | `get_poll` | `pollEventId` | Includes option ids. |
227
+ | `fetch_poll_results` | `pollEventId` | |
228
+ | `create_poll` | `question`, `options` | Always on. |
229
+ | `submit_poll_response` ⚠ | `pollEventId`, `optionIds` | Gated. |
230
+ | `delete_poll` ⚠ | `pollEventId` | Gated. |
231
+ | `clear_my_vote` ⚠ | `pollEventId` | Retract own votes. Gated. |
232
+
233
+ ### Drive (5)
234
+
235
+ | Tool | Required args | Notes |
236
+ | --- | --- | --- |
237
+ | `browse_files` | — | Encrypted drive; optional `folder`. |
238
+ | `get_file_info` | `name` | Optional `folder`. |
239
+ | `delete_file` ⚠ | `name` | **Soft delete** — blob stays on Blossom, index forgets it. Gated. |
240
+ | `rename_file` ⚠ | `name`, `newName` | Gated. |
241
+ | `move_file` ⚠ | `name`, `newFolder` | Gated. |
242
+
243
+ > There is **no upload/create-file tool** — Blossom blobs can't stream over the MCP text
244
+ > channel. Upload happens in the web app; the MCP browses and manages metadata only.
245
+
246
+ ### Mail — Mailstr (7)
247
+
248
+ | Tool | Required args | Notes |
249
+ | --- | --- | --- |
250
+ | `list_mail` | — | Inbox, newest first: `id`, sender, subject, date. |
251
+ | `read_mail` | `mailId` | Full body of one message. |
252
+ | `who_is_my_mail_address` | — | Signed-in identity, default `From:`, and aliases. |
253
+ | `list_mail_aliases` | — | Every address the account can send as, and the default. |
254
+ | `send_mail` ⚠ | `to` | `to` = npub/hex **or** external email. Gated. |
255
+ | `claim_mailbox` ⚠ | `name` | Returns a **bolt11 invoice**; the server cannot pay it. Gated. |
256
+ | `publish_mail_setup` ⚠ | `name` | Profile (nip05) + kind-10050 delivery relays. Gated. |
257
+
258
+ **Aliases.** One identity owns many NIP-05 addresses (`you@mailstr.app`, or
259
+ `you@yourdomain.com` on a workspace), all sharing one inbox. Pass `from` to `send_mail` to
260
+ choose which alias appears in the `From:`; call `list_mail_aliases` first to see the options. A
261
+ `from` the account doesn't own is rejected with the valid list. `send_mail` needs `text` or
262
+ `raw`. For external email, the `From:` must be a registered alias (not the bare npub) or the
263
+ bridge bounces it.
264
+
265
+ > **Claiming is two-step and human-driven.** `claim_mailbox` returns a bolt11 invoice. The
266
+ > user pays it in their own wallet. Then the address starts working once NIP-05 propagates.
267
+ > You **cannot** complete payment from here.
268
+
269
+ ---
270
+
271
+ ## Recipes
272
+
273
+ ### "Make me a survey and share it"
274
+
275
+ ```
276
+ create_form { name: "Team offsite", fields: [
277
+ { type: "short", label: "Your name", required: true },
278
+ { type: "choice", label: "Preferred month", options: ["May","June","July"] },
279
+ { type: "paragraph", label: "Dietary needs" }
280
+ ], encrypted: true }
281
+ → formId = F, pubkey = P
282
+
283
+ # encrypted forms must be shared to be readable:
284
+ share_form { formId: F, formPubkey: P, recipients: ["npub1…"], confirm: true }
285
+ → tell the user it's shared and irreversible-ish
286
+ ```
287
+
288
+ ### "Schedule a meeting with X"
289
+
290
+ ```
291
+ list_calendars # does the user have a calendar list?
292
+ create_calendar_event { title: "Sync", start: "2026-10-20T15:00:00Z",
293
+ end: "2026-10-20T15:30:00Z", participants: ["npub1…"] }
294
+ # if it returns CALENDAR_REQUIRED → ask the user which calendar, then:
295
+ create_calendar_event { …, calendarId: "<chosen>" }
296
+ ```
297
+
298
+ ### "Send an email to alice@example.com as me"
299
+
300
+ ```
301
+ list_mail_aliases # see what From: addresses are available
302
+ send_mail { to: "alice@example.com", subject: "Hi", text: "…",
303
+ from: "me@mailstr.app" } # preview (no confirm)
304
+ # show the user, get agreement, then:
305
+ send_mail { to: "alice@example.com", subject: "Hi", text: "…",
306
+ from: "me@mailstr.app", confirm: true }
307
+ ```
308
+
309
+ ### "What did I miss in my inbox?"
310
+
311
+ ```
312
+ list_mail # newest first
313
+ read_mail { mailId: "<id from list>" }
314
+ ```
315
+
316
+ ---
317
+
318
+ ## Errors and edge cases
319
+
320
+ - **`"Confirmation required…"`** — expected. Re-call with `confirm: true` after the user agrees.
321
+ - **`NOT_FOUND`** — the id/name doesn't exist. Re-run the relevant `list_*`; don't retry blindly.
322
+ - **`CALENDAR_REQUIRED`** — `create_calendar_event` needs a `calendarId`. The message lists the
323
+ available calendars; ask the user, then re-run.
324
+ - **`BAD_INPUT`** — a parameter is missing, unknown, or invalid (this includes a `from` address
325
+ the account doesn't own). The message names the valid values; fix and retry.
326
+ - **`CLAIM_FAILED`** — `claim_mailbox` couldn't create an invoice (name taken, or the API
327
+ rejected the request). Read the message, don't retry blindly.
328
+ - **Unknown parameter error** — tool schemas are **strict**; an unrecognized key is rejected
329
+ with `BAD_INPUT` rather than ignored. Use exactly the parameter names in this guide. (This is
330
+ deliberate: before, a typo'd parameter was silently dropped and you'd think a write happened
331
+ when it didn't.)
332
+ - **Partial relay success** — publish tools return per-relay results. Report how many relays
333
+ accepted; a few failing is normal and not an error.
334
+ - **Gated tools missing** — the server runs without `--allow-writes`. You can't change it; tell
335
+ the user.
336
+ - **Encrypted data looks like ciphertext** — you need the `viewKey`/`nsec`. Ask the user or list
337
+ shared items.
338
+
339
+ ---
340
+
341
+ ## Pointing other agents here
342
+
343
+ This guide is the single entry point. Canonical source is the ngit repository; GitHub is a
344
+ read-only mirror.
345
+
346
+ - **Agent-readable (raw Markdown — what you want to feed a model):**
347
+ `https://raw.githubusercontent.com/formstr-hq/common-packages/main/packages/mcp/AGENTS.md`
348
+ - **Human-readable (rendered):**
349
+ `https://github.com/formstr-hq/common-packages/blob/main/packages/mcp/AGENTS.md`
350
+ - **Shipped in the package too:** `AGENTS.md` is included in the `@formstr/mcp` npm tarball, so
351
+ `node_modules/@formstr/mcp/AGENTS.md` exists after any install.
352
+
353
+ For deeper operator detail — keystore internals, the full environment-variable and CLI-flag
354
+ reference, Ollama/Goose setup, troubleshooting — see [`README.md`](./README.md).
355
+
package/README.md CHANGED
@@ -12,6 +12,12 @@ the same login engine the Formstr web app uses. Local keys are stored **NIP-49 e
12
12
  (`ncryptsec`)** inside an OS-keychain (or encrypted-file) keystore; a raw nsec is never
13
13
  persisted. Remote keys stay in your NIP-46 signer.
14
14
 
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. 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.
20
+
15
21
  ## Quick start
16
22
 
17
23
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@formstr/mcp",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "Model Context Protocol server for the Formstr super-app — drive Nostr forms (and more) from any MCP host, with secure keychain/NIP-46 login.",
5
5
  "keywords": [
6
6
  "mcp",
@@ -26,7 +26,8 @@
26
26
  "main": "./dist/index.js",
27
27
  "files": [
28
28
  "dist",
29
- "README.md"
29
+ "README.md",
30
+ "AGENTS.md"
30
31
  ],
31
32
  "publishConfig": {
32
33
  "access": "public"
@@ -48,9 +49,9 @@
48
49
  "vitest": "^3.2.4",
49
50
  "ws": "^8.18.0",
50
51
  "zod": "^3.24.0",
51
- "@formstr/agent": "^0.3.0",
52
52
  "@formstr/core": "^0.1.1",
53
- "@formstr/signer": "^0.3.2"
53
+ "@formstr/signer": "^0.3.2",
54
+ "@formstr/agent": "^0.3.1"
54
55
  },
55
56
  "scripts": {
56
57
  "build": "tsup",