@formstr/mcp 0.6.0 → 0.7.1
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 +300 -0
- package/README.md +14 -1
- package/dist/index.js +105 -6
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/AGENTS.md
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
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
|
+
## Golden rules
|
|
14
|
+
|
|
15
|
+
1. **Never invent an id.** Every tool that acts on existing data takes an id, pubkey, or
|
|
16
|
+
`coordinate` returned by a *list/get* tool. Chain reads before writes: list → pick → act.
|
|
17
|
+
2. **A write tool needs `confirm: true`.** Without it the tool returns a "Confirmation
|
|
18
|
+
required…" message and does nothing. See [the confirm gate](#the-confirm-gate).
|
|
19
|
+
3. **Creating is not gated, but it is real and public-permanent.** `create_*` tools are always
|
|
20
|
+
on and publish immediately on the user's identity. Don't create things the user didn't ask
|
|
21
|
+
for.
|
|
22
|
+
4. **Addresses and coordinates are exact strings.** Coordinates look like `kind:pubkey:d`.
|
|
23
|
+
naddr/npub are bech32. Don't "fix" or reformat them.
|
|
24
|
+
5. **Report outcomes honestly.** Tools return `ok`/`errorCode`. Relay publishes return per-relay
|
|
25
|
+
results; partial success is normal. Say what actually happened, don't claim success on a
|
|
26
|
+
failed call.
|
|
27
|
+
6. **The user's key never reaches you.** No tool returns secrets. If you ever see key material
|
|
28
|
+
in output, stop and tell the user — it's a bug.
|
|
29
|
+
7. **Prefer the smallest action.** Use list/get to confirm before update/delete. Never delete or
|
|
30
|
+
overwrite without the user's explicit go-ahead in the conversation.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## The confirm gate
|
|
35
|
+
|
|
36
|
+
Roughly half the tools are marked **gated** (destructive, outward, or identity-changing). Each
|
|
37
|
+
is registered *and* requires `confirm: true` on the call.
|
|
38
|
+
|
|
39
|
+
### How to use it safely
|
|
40
|
+
|
|
41
|
+
The first call **without** `confirm` is a free preview: it returns exactly what would happen
|
|
42
|
+
and executes nothing.
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
send_mail({ to: "alice@example.com", subject: "Hi", text: "…" })
|
|
46
|
+
→ { ok: false, text: "Confirmation required for \"send_mail\". This action is irreversible
|
|
47
|
+
and acts on your Nostr identity: sends email to \"alice@example.com\". Re-call with
|
|
48
|
+
\"confirm\": true to proceed." }
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The correct pattern is:
|
|
52
|
+
|
|
53
|
+
1. Call the gated tool **once without `confirm`** (or just describe the effect to the user).
|
|
54
|
+
2. Show the user what will happen and get their agreement **in the conversation**.
|
|
55
|
+
3. Call again with `confirm: true`.
|
|
56
|
+
|
|
57
|
+
Do **not** silently pass `confirm: true` on the first try for anything destructive. For
|
|
58
|
+
outward actions (send mail, share) or deletions, always surface the effect to the user first.
|
|
59
|
+
|
|
60
|
+
### What is gated
|
|
61
|
+
|
|
62
|
+
Gated (`confirm: true` required) — 27 of 60 tools:
|
|
63
|
+
`update_form`, `share_form`, `delete_form`, `submit_form_response`,
|
|
64
|
+
`delete_calendar_event`, `update_calendar_event`, `attach_form_to_event`, `update_calendar`,
|
|
65
|
+
`delete_calendar`, `add_event_to_calendar`, `remove_event_from_calendar`, `approve_booking`,
|
|
66
|
+
`decline_booking`, `rsvp_event`, `delete_page`, `update_page`, `share_page`, `add_page_comment`,
|
|
67
|
+
`delete_poll`, `clear_my_vote`, `submit_poll_response`, `delete_file`, `rename_file`, `move_file`,
|
|
68
|
+
`send_mail`, `claim_mailbox`, `publish_mail_setup`.
|
|
69
|
+
|
|
70
|
+
The other 33 are always on. **"Always on" still means it publishes to the user's identity** —
|
|
71
|
+
`create_*`, `save_private_note`, `import_form_from_naddr`, and `submit_*` are real writes even
|
|
72
|
+
though they don't need `confirm`.
|
|
73
|
+
|
|
74
|
+
> If a gated tool is **absent** from your tool list entirely, the server was started without
|
|
75
|
+
> `--allow-writes`. Tell the user; you cannot enable it from here.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Addresses, ids and coordinates
|
|
80
|
+
|
|
81
|
+
| Thing | Format | Example | Produced by |
|
|
82
|
+
| --- | --- | --- | --- |
|
|
83
|
+
| Form | `formId` + author `pubkey` | `a1b2…` / `c3d4…` | `list_forms`, `create_form` |
|
|
84
|
+
| Form reference | `naddr1…`, `pubkey:formId`, or `kind:pubkey:formId` | — | `create_form` (`naddr`) |
|
|
85
|
+
| Calendar event / calendar | `coordinate` = `kind:pubkey:d` | `31923:<pk>:my-event` | `list_calendar_events`, `create_calendar_event` |
|
|
86
|
+
| Calendar list | its `id` (d-tag) | `work` | `list_calendars`, `create_calendar` |
|
|
87
|
+
| Page / doc | `docId` (d-tag) + author `pubkey`, or an `address` | — | `list_pages`, `create_page` |
|
|
88
|
+
| Poll | `pollEventId` | `ab12…` | `list_polls`, `create_poll` |
|
|
89
|
+
| Drive file | `name` (+ optional `folder`) | `report.pdf` | `browse_files` |
|
|
90
|
+
| Mail message | `mailId` (the wrap id) | `ef56…` | `list_mail` |
|
|
91
|
+
|
|
92
|
+
**Private/encrypted things need a view key.** Forms, pages, and private calendar events can be
|
|
93
|
+
encrypted; pass `viewKey` (an `nsec`) where a tool offers it or you will get ciphertext or an
|
|
94
|
+
error. `list_shared_pages` returns the `viewKey` for documents shared with the user.
|
|
95
|
+
`get_calendar_event` reports `registrationFormHasViewKey` so you can check an attached form is
|
|
96
|
+
readable.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Tool catalog
|
|
101
|
+
|
|
102
|
+
### Forms (9)
|
|
103
|
+
|
|
104
|
+
| Tool | Required args | Notes |
|
|
105
|
+
| --- | --- | --- |
|
|
106
|
+
| `list_forms` | — | The user's forms index. Start here. |
|
|
107
|
+
| `get_form` | `pubkey`, `formId` | Pass `viewKey` for encrypted forms. |
|
|
108
|
+
| `fetch_form_responses` | `formAuthorPubkey`, `formId` | Submissions with responder + answers. |
|
|
109
|
+
| `create_form` | `name`, `fields` | Always on. Returns `formId`, `pubkey`, `naddr`. |
|
|
110
|
+
| `import_form_from_naddr` | `ref` | `naddr1…`, `pubkey:formId`, or `kind:pubkey:formId`. |
|
|
111
|
+
| `update_form` ⚠ | `formId`, `formPubkey` | Republish name/fields/description. Gated. |
|
|
112
|
+
| `share_form` ⚠ | `formId`, `formPubkey`, `recipients` | Gift-wraps the **view key**; `editors` also get edit access. Gated. |
|
|
113
|
+
| `delete_form` ⚠ | `formId`, `formPubkey` | NIP-09 deletion. Gated. |
|
|
114
|
+
| `submit_form_response` ⚠ | `formAuthorPubkey`, `formId`, `answers` | Submits on the user's identity. Gated. |
|
|
115
|
+
|
|
116
|
+
`create_form` fields: each is `{ type, label, required?, options?, validation?, … }`. Supported
|
|
117
|
+
types: `short`, `paragraph`, `choice`, `dropdown`, `number`, `date`, `time`, `grid`, `file`,
|
|
118
|
+
`signature`, `section`. Optional: `description`, `publicForm`, `encrypted`,
|
|
119
|
+
`allowedResponders`, `collaborators`, `notifyNpubs`, `titleImageUrl`, `coverImageUrl`,
|
|
120
|
+
`thankYouText`.
|
|
121
|
+
|
|
122
|
+
> **Encrypted form?** After `create_form` with `encrypted: true`, share it with
|
|
123
|
+
> `share_form` so the people you want can read it — otherwise only the creator can.
|
|
124
|
+
|
|
125
|
+
### Calendar (19)
|
|
126
|
+
|
|
127
|
+
| Tool | Required args | Notes |
|
|
128
|
+
| --- | --- | --- |
|
|
129
|
+
| `list_calendar_events` | — | Optional ISO-8601 `since`/`until`. |
|
|
130
|
+
| `get_calendar_event` | `coordinate` | |
|
|
131
|
+
| `create_calendar_event` | `title`, `start` | **Defaults to PRIVATE.** See note below. |
|
|
132
|
+
| `list_calendars` | — | |
|
|
133
|
+
| `create_calendar` | `title` | A calendar *list* (like a folder). |
|
|
134
|
+
| `fetch_event_rsvps` | `coordinate` | |
|
|
135
|
+
| `list_invitations` | — | NIP-59 invitations received. |
|
|
136
|
+
| `list_scheduling_pages` | — | Booking links, each with a shareable URL. |
|
|
137
|
+
| `list_booking_requests` | — | Incoming appointment requests. |
|
|
138
|
+
| `approve_booking` ⚠ | `requestId`, `calendarId` | Creates the appointment, notifies booker. Gated. |
|
|
139
|
+
| `decline_booking` ⚠ | `requestId` | Gated. |
|
|
140
|
+
| `delete_calendar_event` ⚠ | `eventId` | Gated. |
|
|
141
|
+
| `rsvp_event` ⚠ | `eventCoordinate`, `status` | Gated. Optional suggested time/comment. |
|
|
142
|
+
| `update_calendar_event` ⚠ | `coordinate` | Only send changed fields. Gated. |
|
|
143
|
+
| `attach_form_to_event` ⚠ | `coordinate`, `formRef` | Gated. Pass `formViewKey` for encrypted forms. |
|
|
144
|
+
| `update_calendar` ⚠ | `id` | Gated. |
|
|
145
|
+
| `delete_calendar` ⚠ | `coordinate` | Gated. |
|
|
146
|
+
| `add_event_to_calendar` ⚠ | `calendarId`, `coordinate` | Gated. |
|
|
147
|
+
| `remove_event_from_calendar` ⚠ | `calendarId`, `coordinate` | Gated. |
|
|
148
|
+
|
|
149
|
+
> **`create_calendar_event` workflow (important).** Events are linked into a calendar list;
|
|
150
|
+
> that link is the only way they render on calendar.formstr.app. If you omit `calendarId` and
|
|
151
|
+
> the user already has calendars, the tool returns the list and a `CALENDAR_REQUIRED` code —
|
|
152
|
+
> **ask the user which calendar**, then re-run with `calendarId`. `isPrivate:false` makes a
|
|
153
|
+
> public unencrypted event, which does **not** sync to calendar.formstr.app. `participants`
|
|
154
|
+
> (npub/hex) receive NIP-59 invitations. To attach a registration form, pass
|
|
155
|
+
> `registrationFormRef` and, for encrypted forms, `registrationFormViewKey`.
|
|
156
|
+
|
|
157
|
+
### Pages (12)
|
|
158
|
+
|
|
159
|
+
| Tool | Required args | Notes |
|
|
160
|
+
| --- | --- | --- |
|
|
161
|
+
| `list_pages` | — | |
|
|
162
|
+
| `get_page` | `pubkey`, `docId` | Pass `viewKey` if encrypted. |
|
|
163
|
+
| `list_shared_pages` | — | Docs shared with the user (carries `viewKey`). |
|
|
164
|
+
| `get_page_tags` | `address` | Private labels. |
|
|
165
|
+
| `list_page_comments` | `address`, `viewKey` | Inline comments/suggestions (kind 1494). |
|
|
166
|
+
| `create_page` | `title`, `content` | Markdown, encrypted. Always on. |
|
|
167
|
+
| `save_private_note` | `title`, `content` | Quick encrypted note. Always on. |
|
|
168
|
+
| `set_page_tags` | `address`, `tags` | Always on. |
|
|
169
|
+
| `update_page` ⚠ | `docId`, `content` | Replaces content. Gated. |
|
|
170
|
+
| `delete_page` ⚠ | `address` | NIP-09. Gated. |
|
|
171
|
+
| `share_page` ⚠ | `address`, `content` | Re-encrypts under a view key; `canEdit` for edit links. Gated. |
|
|
172
|
+
| `add_page_comment` ⚠ | `address`, `eventId`, `viewKey`, `content` | Gated. |
|
|
173
|
+
|
|
174
|
+
### Polls (8)
|
|
175
|
+
|
|
176
|
+
| Tool | Required args | Notes |
|
|
177
|
+
| --- | --- | --- |
|
|
178
|
+
| `list_polls` | — | User's own polls. |
|
|
179
|
+
| `list_recent_polls` | — | Public polls to discover. Optional `limit`. |
|
|
180
|
+
| `get_poll` | `pollEventId` | Includes option ids. |
|
|
181
|
+
| `fetch_poll_results` | `pollEventId` | |
|
|
182
|
+
| `create_poll` | `question`, `options` | Always on. |
|
|
183
|
+
| `submit_poll_response` ⚠ | `pollEventId`, `optionIds` | Gated. |
|
|
184
|
+
| `delete_poll` ⚠ | `pollEventId` | Gated. |
|
|
185
|
+
| `clear_my_vote` ⚠ | `pollEventId` | Retract own votes. Gated. |
|
|
186
|
+
|
|
187
|
+
### Drive (5)
|
|
188
|
+
|
|
189
|
+
| Tool | Required args | Notes |
|
|
190
|
+
| --- | --- | --- |
|
|
191
|
+
| `browse_files` | — | Encrypted drive; optional `folder`. |
|
|
192
|
+
| `get_file_info` | `name` | Optional `folder`. |
|
|
193
|
+
| `delete_file` ⚠ | `name` | **Soft delete** — blob stays on Blossom, index forgets it. Gated. |
|
|
194
|
+
| `rename_file` ⚠ | `name`, `newName` | Gated. |
|
|
195
|
+
| `move_file` ⚠ | `name`, `newFolder` | Gated. |
|
|
196
|
+
|
|
197
|
+
> There is **no upload/create-file tool** — Blossom blobs can't stream over the MCP text
|
|
198
|
+
> channel. Upload happens in the web app; the MCP browses and manages metadata only.
|
|
199
|
+
|
|
200
|
+
### Mail — Mailstr (7)
|
|
201
|
+
|
|
202
|
+
| Tool | Required args | Notes |
|
|
203
|
+
| --- | --- | --- |
|
|
204
|
+
| `list_mail` | — | Inbox, newest first: `id`, sender, subject, date. |
|
|
205
|
+
| `read_mail` | `mailId` | Full body of one message. |
|
|
206
|
+
| `who_is_my_mail_address` | — | Signed-in identity, default `From:`, and aliases. |
|
|
207
|
+
| `list_mail_aliases` | — | Every address the account can send as, and the default. |
|
|
208
|
+
| `send_mail` ⚠ | `to` | `to` = npub/hex **or** external email. Gated. |
|
|
209
|
+
| `claim_mailbox` ⚠ | `name` | Returns a **bolt11 invoice**; the server cannot pay it. Gated. |
|
|
210
|
+
| `publish_mail_setup` ⚠ | `name` | Profile (nip05) + kind-10050 delivery relays. Gated. |
|
|
211
|
+
|
|
212
|
+
**Aliases.** One identity owns many NIP-05 addresses (`you@mailstr.app`, or
|
|
213
|
+
`you@yourdomain.com` on a workspace), all sharing one inbox. Pass `from` to `send_mail` to
|
|
214
|
+
choose which alias appears in the `From:`; call `list_mail_aliases` first to see the options. A
|
|
215
|
+
`from` the account doesn't own is rejected with the valid list. `send_mail` needs `text` or
|
|
216
|
+
`raw`. For external email, the `From:` must be a registered alias (not the bare npub) or the
|
|
217
|
+
bridge bounces it.
|
|
218
|
+
|
|
219
|
+
> **Claiming is two-step and human-driven.** `claim_mailbox` returns a bolt11 invoice. The
|
|
220
|
+
> user pays it in their own wallet. Then the address starts working once NIP-05 propagates.
|
|
221
|
+
> You **cannot** complete payment from here.
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Recipes
|
|
226
|
+
|
|
227
|
+
### "Make me a survey and share it"
|
|
228
|
+
|
|
229
|
+
```
|
|
230
|
+
create_form { name: "Team offsite", fields: [
|
|
231
|
+
{ type: "short", label: "Your name", required: true },
|
|
232
|
+
{ type: "choice", label: "Preferred month", options: ["May","June","July"] },
|
|
233
|
+
{ type: "paragraph", label: "Dietary needs" }
|
|
234
|
+
], encrypted: true }
|
|
235
|
+
→ formId = F, pubkey = P
|
|
236
|
+
|
|
237
|
+
# encrypted forms must be shared to be readable:
|
|
238
|
+
share_form { formId: F, formPubkey: P, recipients: ["npub1…"], confirm: true }
|
|
239
|
+
→ tell the user it's shared and irreversible-ish
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### "Schedule a meeting with X"
|
|
243
|
+
|
|
244
|
+
```
|
|
245
|
+
list_calendars # does the user have a calendar list?
|
|
246
|
+
create_calendar_event { title: "Sync", start: "2026-10-20T15:00:00Z",
|
|
247
|
+
end: "2026-10-20T15:30:00Z", participants: ["npub1…"] }
|
|
248
|
+
# if it returns CALENDAR_REQUIRED → ask the user which calendar, then:
|
|
249
|
+
create_calendar_event { …, calendarId: "<chosen>" }
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### "Send an email to alice@example.com as me"
|
|
253
|
+
|
|
254
|
+
```
|
|
255
|
+
list_mail_aliases # see what From: addresses are available
|
|
256
|
+
send_mail { to: "alice@example.com", subject: "Hi", text: "…",
|
|
257
|
+
from: "me@mailstr.app" } # preview (no confirm)
|
|
258
|
+
# show the user, get agreement, then:
|
|
259
|
+
send_mail { to: "alice@example.com", subject: "Hi", text: "…",
|
|
260
|
+
from: "me@mailstr.app", confirm: true }
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### "What did I miss in my inbox?"
|
|
264
|
+
|
|
265
|
+
```
|
|
266
|
+
list_mail # newest first
|
|
267
|
+
read_mail { mailId: "<id from list>" }
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Errors and edge cases
|
|
273
|
+
|
|
274
|
+
- **`"Confirmation required…"`** — expected. Re-call with `confirm: true` after the user agrees.
|
|
275
|
+
- **`NOT_FOUND`** — the id/name doesn't exist. Re-run the relevant `list_*`; don't retry blindly.
|
|
276
|
+
- **`CALENDAR_REQUIRED`** — `create_calendar_event` needs a `calendarId`. The message lists the
|
|
277
|
+
available calendars; ask the user, then re-run.
|
|
278
|
+
- **`BAD_INPUT`** — a parameter is missing, unknown, or invalid (this includes a `from` address
|
|
279
|
+
the account doesn't own). The message names the valid values; fix and retry.
|
|
280
|
+
- **`CLAIM_FAILED`** — `claim_mailbox` couldn't create an invoice (name taken, or the API
|
|
281
|
+
rejected the request). Read the message, don't retry blindly.
|
|
282
|
+
- **Unknown parameter error** — tool schemas are **strict**; an unrecognized key is rejected
|
|
283
|
+
with `BAD_INPUT` rather than ignored. Use exactly the parameter names in this guide. (This is
|
|
284
|
+
deliberate: before, a typo'd parameter was silently dropped and you'd think a write happened
|
|
285
|
+
when it didn't.)
|
|
286
|
+
- **Partial relay success** — publish tools return per-relay results. Report how many relays
|
|
287
|
+
accepted; a few failing is normal and not an error.
|
|
288
|
+
- **Gated tools missing** — the server runs without `--allow-writes`. You can't change it; tell
|
|
289
|
+
the user.
|
|
290
|
+
- **Encrypted data looks like ciphertext** — you need the `viewKey`/`nsec`. Ask the user or list
|
|
291
|
+
shared items.
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## For the operator (setup, not for the agent)
|
|
296
|
+
|
|
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
|
@@ -12,6 +12,10 @@ 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. This README is the
|
|
17
|
+
> operator's setup guide.
|
|
18
|
+
|
|
15
19
|
## Quick start
|
|
16
20
|
|
|
17
21
|
```bash
|
|
@@ -236,17 +240,26 @@ Email through mailstr, using the same identity you logged in with (the account t
|
|
|
236
240
|
|
|
237
241
|
- `list_mail` — your inbox: sender, subject, date per message (kind-1059 gift-wrapped mail).
|
|
238
242
|
- `read_mail` — the full body of one message by its id.
|
|
239
|
-
- `
|
|
243
|
+
- `list_mail_aliases` — every address (NIP-05 alias) this account can send as, and the default.
|
|
244
|
+
- `who_is_my_mail_address` — the signed-in identity, its default `From:`, and its aliases.
|
|
240
245
|
|
|
241
246
|
**Gated (require `--allow-writes` + `confirm: true`)**
|
|
242
247
|
|
|
243
248
|
- `send_mail` — send email to an npub/hex key, or an external address via the domain's SMTP bridge.
|
|
249
|
+
Pass `from` to choose which alias to send as (one of your own addresses); omit it to send from
|
|
250
|
+
an owned alias, or the npub mailbox when you have none. A `from` you do not own is rejected with
|
|
251
|
+
the list of valid senders.
|
|
244
252
|
- `claim_mailbox` — start claiming `you@mailstr.app`. Returns a **Lightning invoice to pay in
|
|
245
253
|
your own wallet** — the server holds no wallet, so it cannot pay it. Mail works once NIP-05
|
|
246
254
|
propagates.
|
|
247
255
|
- `publish_mail_setup` — publish your profile (with the NIP-05 address) and the kind-10050
|
|
248
256
|
delivery-relay list senders read.
|
|
249
257
|
|
|
258
|
+
**Aliases.** A mail identity is one Nostr key; an *alias* is a NIP-05 name bound to that key.
|
|
259
|
+
All of an account's aliases share one key and one inbox — mail is encrypted to the key, so which
|
|
260
|
+
alias a message was addressed to is only a header. `send_mail`'s `from` picks which alias appears
|
|
261
|
+
in the `From:`; `list_mail_aliases` shows the choices.
|
|
262
|
+
|
|
250
263
|
## Safety model
|
|
251
264
|
|
|
252
265
|
Destructive / outward tools are **not registered** unless `--allow-writes` (or
|
package/dist/index.js
CHANGED
|
@@ -33354,6 +33354,64 @@ var defaultFetchJson = async (url, init) => {
|
|
|
33354
33354
|
clearTimeout(timer);
|
|
33355
33355
|
}
|
|
33356
33356
|
};
|
|
33357
|
+
var OWNED_ADDRESSES_PATH = "/api/nip-05/get-nip05";
|
|
33358
|
+
function normalizeOwnedAddresses(body, domain = DEFAULT_MAIL_DOMAIN) {
|
|
33359
|
+
const qualify = (addr, entryDomain) => {
|
|
33360
|
+
if (addr.includes("@")) return addr;
|
|
33361
|
+
if (typeof entryDomain === "string" && entryDomain.trim()) return `${addr}@${entryDomain}`;
|
|
33362
|
+
return `${addr}@${domain}`;
|
|
33363
|
+
};
|
|
33364
|
+
if (typeof body === "string") return [qualify(body)];
|
|
33365
|
+
let raw = [];
|
|
33366
|
+
if (Array.isArray(body)) {
|
|
33367
|
+
raw = body.flatMap((entry) => {
|
|
33368
|
+
if (typeof entry === "string") return [{ value: entry }];
|
|
33369
|
+
if (entry && typeof entry === "object") {
|
|
33370
|
+
const obj = entry;
|
|
33371
|
+
if (typeof obj.nip05 === "string") return [{ value: obj.nip05, domain: obj.domain }];
|
|
33372
|
+
if (typeof obj.name === "string") return [{ value: obj.name, domain: obj.domain }];
|
|
33373
|
+
}
|
|
33374
|
+
return [];
|
|
33375
|
+
});
|
|
33376
|
+
} else if (body && typeof body === "object") {
|
|
33377
|
+
const obj = body;
|
|
33378
|
+
if (typeof obj.nip05 === "string") {
|
|
33379
|
+
raw = [{ value: obj.nip05, domain: obj.domain }];
|
|
33380
|
+
} else if (Array.isArray(obj.nip05Addresses)) {
|
|
33381
|
+
raw = obj.nip05Addresses.filter((v) => typeof v === "string").map((value) => ({ value }));
|
|
33382
|
+
}
|
|
33383
|
+
}
|
|
33384
|
+
return raw.map(({ value, domain: d }) => qualify(value, d));
|
|
33385
|
+
}
|
|
33386
|
+
async function defaultFetch(url, init) {
|
|
33387
|
+
const res = await fetch(url, init);
|
|
33388
|
+
let body = null;
|
|
33389
|
+
try {
|
|
33390
|
+
body = await res.json();
|
|
33391
|
+
} catch {
|
|
33392
|
+
}
|
|
33393
|
+
return { ok: res.ok, status: res.status, body };
|
|
33394
|
+
}
|
|
33395
|
+
async function fetchAddresses(url, header, opts) {
|
|
33396
|
+
const fetchJson = opts.fetchJson ?? defaultFetch;
|
|
33397
|
+
const res = await fetchJson(url, { method: "GET", headers: { Authorization: header } });
|
|
33398
|
+
if (res.status === 404 || res.status === 401) return [];
|
|
33399
|
+
if (!res.ok) throw new Error(`Address lookup failed (${res.status})`);
|
|
33400
|
+
return normalizeOwnedAddresses(res.body, opts.domain ?? DEFAULT_MAIL_DOMAIN);
|
|
33401
|
+
}
|
|
33402
|
+
async function fetchOwnedAddressesWith(signer, opts = {}) {
|
|
33403
|
+
const api = (opts.api ?? DEFAULT_CLAIM_API).replace(/\/+$/, "");
|
|
33404
|
+
const url = `${api}${OWNED_ADDRESSES_PATH}`;
|
|
33405
|
+
return fetchAddresses(url, await signNip98With(signer, url, "GET"), opts);
|
|
33406
|
+
}
|
|
33407
|
+
function defaultFromAddress(pubkey, ownedAddresses) {
|
|
33408
|
+
const isNpubAlias = (a) => {
|
|
33409
|
+
const at = a.indexOf("@");
|
|
33410
|
+
return at > 0 && a.slice(0, at).toLowerCase().startsWith("npub1");
|
|
33411
|
+
};
|
|
33412
|
+
const alias = ownedAddresses.find((a) => !isNpubAlias(a));
|
|
33413
|
+
return alias ?? ownedAddresses[0] ?? `${nip19_exports.npubEncode(pubkey)}@${DEFAULT_MAIL_DOMAIN}`;
|
|
33414
|
+
}
|
|
33357
33415
|
function verifySealAndRumor(wrap, seal, rumor, opts = {}) {
|
|
33358
33416
|
const maxAge = opts.maxAgeSeconds ?? MAX_RUMOR_AGE_SECONDS;
|
|
33359
33417
|
const now3 = opts.now ?? Math.floor(Date.now() / 1e3);
|
|
@@ -33719,9 +33777,11 @@ async function publishEverywhere(pool, relays, event) {
|
|
|
33719
33777
|
};
|
|
33720
33778
|
}
|
|
33721
33779
|
|
|
33722
|
-
// ../agent/dist/chunk-
|
|
33780
|
+
// ../agent/dist/chunk-5L26ARX5.js
|
|
33723
33781
|
var service_exports2 = {};
|
|
33724
33782
|
__export3(service_exports2, {
|
|
33783
|
+
defaultSenderAddress: () => defaultSenderAddress,
|
|
33784
|
+
listAliases: () => listAliases,
|
|
33725
33785
|
mailIdentity: () => mailIdentity,
|
|
33726
33786
|
publishMailSetup: () => publishMailSetup,
|
|
33727
33787
|
readMail: () => readMail,
|
|
@@ -33740,6 +33800,15 @@ async function mailIdentity() {
|
|
|
33740
33800
|
const profile = await fetchProfile(pubkey).catch(() => null);
|
|
33741
33801
|
return { pubkey, npub: nip19_exports.npubEncode(pubkey), nip05: profile?.nip05 ?? null };
|
|
33742
33802
|
}
|
|
33803
|
+
async function listAliases() {
|
|
33804
|
+
const signer = await mailSigner();
|
|
33805
|
+
return fetchOwnedAddressesWith(signer).catch(() => []);
|
|
33806
|
+
}
|
|
33807
|
+
async function defaultSenderAddress() {
|
|
33808
|
+
const signer = await signerManager.getSigner();
|
|
33809
|
+
const pubkey = await signer.getPublicKey();
|
|
33810
|
+
return defaultFromAddress(pubkey, await listAliases());
|
|
33811
|
+
}
|
|
33743
33812
|
async function readMail(opts = {}) {
|
|
33744
33813
|
const signer = await mailSigner();
|
|
33745
33814
|
const { mail, failures } = await readInboxWith(signer, {
|
|
@@ -33750,12 +33819,13 @@ async function readMail(opts = {}) {
|
|
|
33750
33819
|
}
|
|
33751
33820
|
async function sendMail(params) {
|
|
33752
33821
|
const signer = await mailSigner();
|
|
33822
|
+
const from = params.from ?? await defaultSenderAddress();
|
|
33753
33823
|
return sendMailWith(signer, {
|
|
33754
33824
|
to: params.to,
|
|
33755
33825
|
...params.subject !== void 0 ? { subject: params.subject } : {},
|
|
33756
33826
|
...params.text !== void 0 ? { text: params.text } : {},
|
|
33757
33827
|
...params.raw !== void 0 ? { raw: params.raw } : {},
|
|
33758
|
-
|
|
33828
|
+
from,
|
|
33759
33829
|
relays: relayManager.getRelaysForModule("mail")
|
|
33760
33830
|
});
|
|
33761
33831
|
}
|
|
@@ -44700,7 +44770,7 @@ var coerce = {
|
|
|
44700
44770
|
};
|
|
44701
44771
|
var NEVER = INVALID;
|
|
44702
44772
|
|
|
44703
|
-
// ../agent/dist/chunk-
|
|
44773
|
+
// ../agent/dist/chunk-JORUNTXB.js
|
|
44704
44774
|
function ok(text, data) {
|
|
44705
44775
|
return data !== void 0 ? { ok: true, text, data } : { ok: true, text };
|
|
44706
44776
|
}
|
|
@@ -45956,14 +46026,31 @@ function buildMailTools() {
|
|
|
45956
46026
|
server.registerTool(
|
|
45957
46027
|
"who_is_my_mail_address",
|
|
45958
46028
|
{
|
|
45959
|
-
description: "Show the
|
|
46029
|
+
description: "Show the account signed in to this server, its default mail address, and every alias it can send as.",
|
|
45960
46030
|
inputSchema: {}
|
|
45961
46031
|
},
|
|
45962
46032
|
async () => {
|
|
45963
46033
|
const who = await service_exports2.mailIdentity();
|
|
46034
|
+
const aliases = await service_exports2.listAliases();
|
|
46035
|
+
const defaultFrom = await service_exports2.defaultSenderAddress();
|
|
45964
46036
|
return ok(
|
|
45965
|
-
|
|
45966
|
-
who
|
|
46037
|
+
`Signed in as ${who.npub}. Sending as ${defaultFrom}.` + (aliases.length ? ` Aliases: ${aliases.join(", ")}.` : " No registered alias yet \u2014 claim one with claim_mailbox."),
|
|
46038
|
+
{ ...who, aliases, defaultFrom }
|
|
46039
|
+
);
|
|
46040
|
+
}
|
|
46041
|
+
);
|
|
46042
|
+
server.registerTool(
|
|
46043
|
+
"list_mail_aliases",
|
|
46044
|
+
{
|
|
46045
|
+
description: "List every mail address (NIP-05 alias) this account can send as, and which is the default. All aliases share one inbox.",
|
|
46046
|
+
inputSchema: {}
|
|
46047
|
+
},
|
|
46048
|
+
async () => {
|
|
46049
|
+
const aliases = await service_exports2.listAliases();
|
|
46050
|
+
const defaultFrom = await service_exports2.defaultSenderAddress();
|
|
46051
|
+
return ok(
|
|
46052
|
+
aliases.length ? `You can send as ${aliases.length} address(es); default is ${defaultFrom}.` : `No aliases yet \u2014 sending uses ${defaultFrom}. Claim an address with claim_mailbox.`,
|
|
46053
|
+
{ aliases, defaultFrom }
|
|
45967
46054
|
);
|
|
45968
46055
|
}
|
|
45969
46056
|
);
|
|
@@ -45987,6 +46074,18 @@ function buildMailTools() {
|
|
|
45987
46074
|
if (args.text === void 0 && args.raw === void 0) {
|
|
45988
46075
|
return fail("send_mail needs either `text` or `raw`.", "BAD_INPUT");
|
|
45989
46076
|
}
|
|
46077
|
+
if (args.from !== void 0) {
|
|
46078
|
+
const identity = await service_exports2.mailIdentity();
|
|
46079
|
+
const aliases = await service_exports2.listAliases();
|
|
46080
|
+
const npubAddress = `${identity.npub}@${DEFAULT_MAIL_DOMAIN}`;
|
|
46081
|
+
const validSenders = [npubAddress, ...aliases];
|
|
46082
|
+
if (!validSenders.map((a) => a.toLowerCase()).includes(args.from.toLowerCase())) {
|
|
46083
|
+
return fail(
|
|
46084
|
+
`You cannot send as "${args.from}". Use one of: ${validSenders.join(", ")}.`,
|
|
46085
|
+
"BAD_SENDER"
|
|
46086
|
+
);
|
|
46087
|
+
}
|
|
46088
|
+
}
|
|
45990
46089
|
try {
|
|
45991
46090
|
const result = await service_exports2.sendMail({
|
|
45992
46091
|
to: args.to,
|