@formstr/mcp 0.4.0 → 0.5.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/dist/index.js CHANGED
@@ -24568,7 +24568,13 @@ function fileBackend() {
24568
24568
  if (!passphrase) {
24569
24569
  throw new Error("Encrypted keystore file exists but FORMSTR_MCP_PASSPHRASE is not set.");
24570
24570
  }
24571
- return parseMap(decryptBlob((0, import_node_fs.readFileSync)(path, "utf8"), passphrase));
24571
+ try {
24572
+ return parseMap(decryptBlob((0, import_node_fs.readFileSync)(path, "utf8"), passphrase));
24573
+ } catch {
24574
+ throw new Error(
24575
+ `Could not decrypt the keystore file (${path}): FORMSTR_MCP_PASSPHRASE does not match the passphrase it was encrypted with (or the file is corrupt). Fix the passphrase, or delete the file and run \`formstr-mcp login\` to sign in again.`
24576
+ );
24577
+ }
24572
24578
  },
24573
24579
  save(map) {
24574
24580
  const passphrase = process.env.FORMSTR_MCP_PASSPHRASE;
@@ -27554,6 +27560,8 @@ function helpText() {
27554
27560
  "",
27555
27561
  "Flags:",
27556
27562
  " --allow-writes Enable gated write tools (update / delete / share / submit).",
27563
+ " Note: create/import tools (create_form, create_calendar_event, \u2026)",
27564
+ " are ALWAYS enabled and publish to relays even without this flag.",
27557
27565
  " --account <npub|hex> Boot a specific account instead of the active one.",
27558
27566
  " --relays <a,b,\u2026> Override relays (comma-separated).",
27559
27567
  "",
@@ -27580,7 +27588,8 @@ function helpText() {
27580
27588
  " }",
27581
27589
  "",
27582
27590
  " Tip: a NIP-46 (bunker) account needs no passphrase \u2014 `switch` to one and the config",
27583
- ' can stay secret-free. Add "--allow-writes" to args to enable gated write tools.'
27591
+ ' can stay secret-free. Add "--allow-writes" to args to enable gated write tools',
27592
+ " (creates are always on)."
27584
27593
  ].join("\n");
27585
27594
  }
27586
27595
 
@@ -32406,11 +32415,11 @@ async function decryptListEntries(signer, userPubkey, content) {
32406
32415
  const parsed = JSON.parse(decrypted);
32407
32416
  return Array.isArray(parsed) ? parsed : [];
32408
32417
  }
32409
- async function publishMyFormsList(signer, relays, entries) {
32418
+ async function publishMyFormsList(signer, relays, entries, prevCreatedAt) {
32410
32419
  const encrypted = await nip44SelfEncrypt(signer, JSON.stringify(entries));
32411
32420
  const event = {
32412
32421
  kind: FORM_KINDS.myFormsList,
32413
- created_at: Math.floor(Date.now() / 1e3),
32422
+ created_at: Math.max(Math.floor(Date.now() / 1e3), prevCreatedAt + 1),
32414
32423
  tags: [],
32415
32424
  content: encrypted
32416
32425
  };
@@ -32471,9 +32480,10 @@ async function readMyFormsEntriesForWrite(signer, userPubkey, relays) {
32471
32480
  listEvent = await fetchLatestMyFormsEvent(relays, userPubkey);
32472
32481
  if (listEvent?.content) break;
32473
32482
  }
32474
- if (!listEvent?.content) return [];
32483
+ if (!listEvent?.content) return { entries: [], prevCreatedAt: 0 };
32475
32484
  try {
32476
- return await decryptListEntries(signer, userPubkey, listEvent.content);
32485
+ const entries = await decryptListEntries(signer, userPubkey, listEvent.content);
32486
+ return { entries, prevCreatedAt: listEvent.created_at };
32477
32487
  } catch (err) {
32478
32488
  throw new Error(
32479
32489
  `Could not read your existing forms list to add this form, so it was left unchanged to avoid losing your other forms. The form itself was published \u2014 retry to add it. (${err instanceof Error ? err.message : String(err)})`
@@ -32484,8 +32494,12 @@ async function appendToMyFormsList(formPubkey, formId, relay, signingKeyHex, vie
32484
32494
  const signer = await signerManager.getSigner();
32485
32495
  const userPubkey = await signer.getPublicKey();
32486
32496
  const relays = relayManager.getRelaysForModule("forms");
32487
- let entries = await readMyFormsEntriesForWrite(signer, userPubkey, relays);
32488
- entries = entries.map(
32497
+ const { entries: existing, prevCreatedAt } = await readMyFormsEntriesForWrite(
32498
+ signer,
32499
+ userPubkey,
32500
+ relays
32501
+ );
32502
+ const entries = existing.map(
32489
32503
  (e) => e[0] === "f" && e.length < 4 ? ["f", e[1] ?? "", e[2] ?? "", ""] : e
32490
32504
  );
32491
32505
  const coordKey = `${formPubkey}:${formId}`;
@@ -32493,10 +32507,11 @@ async function appendToMyFormsList(formPubkey, formId, relay, signingKeyHex, vie
32493
32507
  const secrets = signingKeyHex ? encodeFormKeys(signingKeyHex, viewKeyHex) : "";
32494
32508
  entries.push(["f", coordKey, relay, secrets]);
32495
32509
  }
32496
- await publishMyFormsList(signer, relays, entries);
32510
+ await publishMyFormsList(signer, relays, entries, prevCreatedAt);
32497
32511
  }
32498
32512
  async function saveToMyForms(summaries) {
32499
32513
  const signer = await signerManager.getSigner();
32514
+ const userPubkey = await signer.getPublicKey();
32500
32515
  const relays = relayManager.getRelaysForModule("forms");
32501
32516
  const entries = summaries.map((s) => [
32502
32517
  "f",
@@ -32504,7 +32519,8 @@ async function saveToMyForms(summaries) {
32504
32519
  s.relay ?? "",
32505
32520
  s.signingKey ? encodeFormKeys(s.signingKey, s.viewKey) : ""
32506
32521
  ]);
32507
- await publishMyFormsList(signer, relays, entries);
32522
+ const prev = await fetchLatestMyFormsEvent(relays, userPubkey);
32523
+ await publishMyFormsList(signer, relays, entries, prev?.created_at ?? 0);
32508
32524
  }
32509
32525
  async function fetchMyFormsByAuthor(pubkey, relays) {
32510
32526
  const filter = {
@@ -32551,7 +32567,7 @@ async function deleteForm(formId, formPubkey) {
32551
32567
  const coordKey = `${formPubkey}:${formId}`;
32552
32568
  const trimmed = entries.filter((e) => e[1] !== coordKey);
32553
32569
  if (trimmed.length === entries.length) return;
32554
- await publishMyFormsList(signer, relays, trimmed);
32570
+ await publishMyFormsList(signer, relays, trimmed, listEvent.created_at);
32555
32571
  }
32556
32572
  async function updateForm(params) {
32557
32573
  const relays = relayManager.getRelaysForModule("forms");
@@ -34287,7 +34303,7 @@ async function fetchAllDocMetadata() {
34287
34303
  }
34288
34304
  return result;
34289
34305
  }
34290
- async function fetchDocMetadata(address) {
34306
+ async function fetchNewestDocMetadataEvent(address) {
34291
34307
  const signer = await signerManager.getSigner();
34292
34308
  const pubkey = await signer.getPublicKey();
34293
34309
  const events = await nostrRuntime.querySync(RELAYS(), {
@@ -34296,7 +34312,12 @@ async function fetchDocMetadata(address) {
34296
34312
  "#d": [address]
34297
34313
  });
34298
34314
  if (events.length === 0) return void 0;
34299
- const newest = events.reduce((a, b) => b.created_at > a.created_at ? b : a);
34315
+ return events.reduce((a, b) => b.created_at > a.created_at ? b : a);
34316
+ }
34317
+ async function fetchDocMetadata(address) {
34318
+ const signer = await signerManager.getSigner();
34319
+ const newest = await fetchNewestDocMetadataEvent(address);
34320
+ if (!newest) return void 0;
34300
34321
  try {
34301
34322
  return parseMetadataJson(await nip44SelfDecrypt(signer, newest.content));
34302
34323
  } catch {
@@ -34304,14 +34325,24 @@ async function fetchDocMetadata(address) {
34304
34325
  }
34305
34326
  }
34306
34327
  async function saveDocMetadata(address, patch) {
34307
- const existing = await fetchDocMetadata(address) ?? {};
34328
+ const signer = await signerManager.getSigner();
34329
+ const prevEvent = await fetchNewestDocMetadataEvent(address);
34330
+ let existing = {};
34331
+ if (prevEvent) {
34332
+ try {
34333
+ existing = parseMetadataJson(await nip44SelfDecrypt(signer, prevEvent.content)) ?? {};
34334
+ } catch {
34335
+ }
34336
+ }
34308
34337
  const merged = { ...existing, ...patch };
34309
34338
  if (!Array.isArray(merged.tags)) merged.tags = [];
34310
- const signer = await signerManager.getSigner();
34311
34339
  const content = await nip44SelfEncrypt(signer, JSON.stringify(merged));
34312
34340
  const signed = await signer.signEvent({
34313
34341
  kind: PAGES_KINDS.docMetadata,
34314
- created_at: Math.floor(Date.now() / 1e3),
34342
+ // Strictly supersede the event this merge was read from: two saves in the
34343
+ // same second otherwise tie on created_at and NIP-01 tie-breaking (lowest
34344
+ // id wins) can resurrect the stale copy — losing a just-saved viewKey/title.
34345
+ created_at: Math.max(Math.floor(Date.now() / 1e3), (prevEvent?.created_at ?? 0) + 1),
34315
34346
  tags: [["d", address]],
34316
34347
  content
34317
34348
  });
@@ -34560,14 +34591,22 @@ async function fetchFileIndex() {
34560
34591
  }
34561
34592
  async function saveFileMetadata(metadata) {
34562
34593
  const signer = await signerManager.getSigner();
34594
+ const relays = relayManager.getRelaysForModule("drive");
34563
34595
  const full = {
34564
34596
  ...metadata,
34565
34597
  encryptionAlgorithm: metadata.encryptionAlgorithm ?? "aes-gcm"
34566
34598
  };
34567
34599
  const encrypted = await nip44SelfEncrypt(signer, JSON.stringify(full));
34600
+ const pubkey = await signer.getPublicKey();
34601
+ const existing = await nostrRuntime.querySync(relays, {
34602
+ kinds: [DRIVE_KINDS.fileMetadata],
34603
+ authors: [pubkey],
34604
+ "#d": [metadata.hash]
34605
+ });
34606
+ const prevCreatedAt = existing.reduce((max, e) => Math.max(max, e.created_at), 0);
34568
34607
  const event = {
34569
34608
  kind: DRIVE_KINDS.fileMetadata,
34570
- created_at: Math.floor(Date.now() / 1e3),
34609
+ created_at: Math.max(Math.floor(Date.now() / 1e3), prevCreatedAt + 1),
34571
34610
  tags: [
34572
34611
  ["d", metadata.hash],
34573
34612
  ["client", "formstr-drive"],
@@ -34576,7 +34615,6 @@ async function saveFileMetadata(metadata) {
34576
34615
  content: encrypted
34577
34616
  };
34578
34617
  const signed = await signer.signEvent(event);
34579
- const relays = relayManager.getRelaysForModule("drive");
34580
34618
  await nostrRuntime.publish(relays, signed);
34581
34619
  }
34582
34620
  async function updateFileMetadata(hash, updates) {
@@ -34661,6 +34699,7 @@ __export(service_exports5, {
34661
34699
  });
34662
34700
 
34663
34701
  // ../agent/src/services/polls/pow.ts
34702
+ var MAX_MINED_POW_DIFFICULTY = 20;
34664
34703
  function minePollEvent(unsigned, difficulty) {
34665
34704
  const event = unsigned;
34666
34705
  const nonceTag = ["nonce", "0", difficulty.toString()];
@@ -34753,6 +34792,11 @@ async function submitPollResponse(pollId, pollAuthor, selectedOptionIds, pollRel
34753
34792
  tags,
34754
34793
  content: ""
34755
34794
  };
34795
+ if (powDifficulty && powDifficulty > MAX_MINED_POW_DIFFICULTY) {
34796
+ throw new Error(
34797
+ `This poll demands proof-of-work difficulty ${powDifficulty}, above the supported maximum of ${MAX_MINED_POW_DIFFICULTY} \u2014 refusing to mine (it could take hours).`
34798
+ );
34799
+ }
34756
34800
  if (powDifficulty && powDifficulty > 0) {
34757
34801
  const unsigned = { ...event, pubkey: await signer.getPublicKey() };
34758
34802
  const mined = minePollEvent(unsigned, powDifficulty);
@@ -34943,6 +34987,16 @@ function normalizePubkeyList(values) {
34943
34987
  }
34944
34988
 
34945
34989
  // ../agent/src/tools/calendar.ts
34990
+ function parseIsoDate(value) {
34991
+ const date3 = new Date(value);
34992
+ return Number.isFinite(date3.getTime()) ? date3 : null;
34993
+ }
34994
+ function badDate(field, value) {
34995
+ return fail(
34996
+ `Could not parse ${field} "${value}" \u2014 pass an ISO 8601 date-time (e.g. 2026-07-02T15:00:00Z).`,
34997
+ "BAD_INPUT"
34998
+ );
34999
+ }
34946
35000
  var calendarTools = buildCalendarTools();
34947
35001
  function buildCalendarTools() {
34948
35002
  const tools = [];
@@ -34959,11 +35013,15 @@ function buildCalendarTools() {
34959
35013
  inputSchema: { since: external_exports.string().optional(), until: external_exports.string().optional() }
34960
35014
  },
34961
35015
  async ({ since, until }) => {
35016
+ const sinceDate = since ? parseIsoDate(since) : void 0;
35017
+ if (since && !sinceDate) return badDate("since", since);
35018
+ const untilDate = until ? parseIsoDate(until) : void 0;
35019
+ if (until && !untilDate) return badDate("until", until);
34962
35020
  const pubkey = signerManager.getPublicKey();
34963
35021
  const events = await service_exports2.fetchCalendarEventsSync({
34964
35022
  authors: pubkey ? [pubkey] : void 0,
34965
- since: since ? Math.floor(new Date(since).getTime() / 1e3) : void 0,
34966
- until: until ? Math.floor(new Date(until).getTime() / 1e3) : void 0
35023
+ since: sinceDate ? Math.floor(sinceDate.getTime() / 1e3) : void 0,
35024
+ until: untilDate ? Math.floor(untilDate.getTime() / 1e3) : void 0
34967
35025
  });
34968
35026
  return ok(`Found ${events.length} event(s).`, {
34969
35027
  events: events.map((e) => ({
@@ -34981,7 +35039,7 @@ function buildCalendarTools() {
34981
35039
  server.registerTool(
34982
35040
  "create_calendar_event",
34983
35041
  {
34984
- description: "Schedule a calendar event. start/end are ISO 8601. Events default to PRIVATE (encrypted) and are linked into a calendar list \u2014 that is the only way they show on calendar.formstr.app, which renders only events referenced in a calendar list. Pass calendarId to choose which calendar; if omitted and the user already has calendars, this tool returns the list so you can ASK the user which one (then re-run with calendarId). Set isPrivate:false for a public, unencrypted event \u2014 note public events do NOT sync to calendar.formstr.app. participants (npub or hex) receive NIP-59 invitations.",
35042
+ description: "Schedule a calendar event. start/end are ISO 8601. Events default to PRIVATE (encrypted) and are linked into a calendar list \u2014 that is the only way they show on calendar.formstr.app, which renders only events referenced in a calendar list. Pass calendarId to choose which calendar; if omitted and the user already has calendars, this tool returns the list so you can ASK the user which one (then re-run with calendarId). Set isPrivate:false for a public, unencrypted event \u2014 note public events do NOT sync to calendar.formstr.app. participants (npub or hex) receive NIP-59 invitations. registrationFormRef attaches a Formstr form; for an ENCRYPTED form also pass registrationFormViewKey or attendees cannot read it.",
34985
35043
  inputSchema: {
34986
35044
  title: external_exports.string(),
34987
35045
  description: external_exports.string().optional(),
@@ -34993,10 +35051,15 @@ function buildCalendarTools() {
34993
35051
  participants: external_exports.array(external_exports.string()).optional(),
34994
35052
  rrule: external_exports.string().optional(),
34995
35053
  startTzid: external_exports.string().optional(),
34996
- registrationFormRef: external_exports.string().optional()
35054
+ registrationFormRef: external_exports.string().optional(),
35055
+ registrationFormViewKey: external_exports.string().optional()
34997
35056
  }
34998
35057
  },
34999
35058
  async (args) => {
35059
+ const begin = parseIsoDate(args.start);
35060
+ if (!begin) return badDate("start", args.start);
35061
+ const end = args.end ? parseIsoDate(args.end) : new Date(begin.getTime() + 36e5);
35062
+ if (!end) return badDate("end", args.end);
35000
35063
  const isPrivate = args.isPrivate ?? true;
35001
35064
  const calendars = await service_exports2.fetchCalendarLists();
35002
35065
  if (isPrivate && !args.calendarId && calendars.length > 0) {
@@ -35013,8 +35076,6 @@ function buildCalendarTools() {
35013
35076
  "NOT_FOUND"
35014
35077
  );
35015
35078
  }
35016
- const begin = new Date(args.start);
35017
- const end = args.end ? new Date(args.end) : new Date(begin.getTime() + 36e5);
35018
35079
  const draft = {
35019
35080
  title: args.title,
35020
35081
  description: args.description ?? "",
@@ -35028,17 +35089,20 @@ function buildCalendarTools() {
35028
35089
  calendarId: args.calendarId,
35029
35090
  rrule: args.rrule,
35030
35091
  startTzid: args.startTzid,
35031
- registrationFormRef: args.registrationFormRef
35092
+ registrationFormRef: args.registrationFormRef,
35093
+ registrationFormViewKey: args.registrationFormViewKey
35032
35094
  };
35033
35095
  const { event, calendar: list } = await service_exports2.createCalendarEvent(draft, { calendars });
35034
35096
  const coordinate = `${event.kind}:${event.user}:${event.id}`;
35097
+ const invitationsSent = isPrivate ? draft.participants?.length ?? 0 : 0;
35035
35098
  return ok(
35036
- `Created ${isPrivate ? "private" : "public"} event "${args.title}"${list ? ` in calendar "${list.title}"` : ""}.`,
35099
+ `Created ${isPrivate ? "private" : "public"} event "${args.title}"${list ? ` in calendar "${list.title}"` : ""}${invitationsSent ? ` \u2014 sent ${invitationsSent} invitation(s)` : ""}.`,
35037
35100
  {
35038
35101
  id: event.id,
35039
35102
  eventId: event.eventId,
35040
35103
  coordinate,
35041
- calendarId: list?.id
35104
+ calendarId: list?.id,
35105
+ invitationsSent
35042
35106
  }
35043
35107
  );
35044
35108
  }
@@ -35057,12 +35121,19 @@ function buildCalendarTools() {
35057
35121
  event: {
35058
35122
  id: event.id,
35059
35123
  title: event.title,
35124
+ description: event.description,
35060
35125
  begin: event.begin,
35061
35126
  end: event.end,
35062
35127
  location: event.location,
35063
35128
  isPrivate: event.isPrivate,
35064
35129
  rrule: event.repeat?.rrule ?? null,
35065
- participants: event.participants
35130
+ startTzid: event.startTzid ?? null,
35131
+ participants: event.participants,
35132
+ calendarId: event.calendarId ?? null,
35133
+ // The attached form ref is surfaced so a write can be verified; the
35134
+ // form's viewKey itself is never returned (no key material in results).
35135
+ registrationFormRef: event.registrationFormRef ?? null,
35136
+ registrationFormHasViewKey: Boolean(event.registrationFormViewKey)
35066
35137
  }
35067
35138
  });
35068
35139
  }
@@ -35073,7 +35144,12 @@ function buildCalendarTools() {
35073
35144
  async () => {
35074
35145
  const lists = await service_exports2.fetchCalendarLists();
35075
35146
  return ok(`Found ${lists.length} calendar(s).`, {
35076
- calendars: lists.map((c) => ({ id: c.id, title: c.title, color: c.color }))
35147
+ calendars: lists.map((c) => ({
35148
+ id: c.id,
35149
+ title: c.title,
35150
+ color: c.color,
35151
+ description: c.description
35152
+ }))
35077
35153
  });
35078
35154
  }
35079
35155
  );
@@ -35282,7 +35358,7 @@ function buildCalendarTools() {
35282
35358
  server.registerTool(
35283
35359
  "update_calendar_event",
35284
35360
  {
35285
- description: "Update a calendar event by its coordinate kind:pubkey:d. Only changed fields need be sent. Requires confirm:true.",
35361
+ description: "Update a calendar event by its coordinate kind:pubkey:d. Only changed fields need be sent. registrationFormRef attaches/replaces the event's registration form (empty string detaches it); for an ENCRYPTED form also pass registrationFormViewKey or attendees cannot read it. Requires confirm:true.",
35286
35362
  inputSchema: {
35287
35363
  coordinate: external_exports.string(),
35288
35364
  title: external_exports.string().optional(),
@@ -35292,6 +35368,8 @@ function buildCalendarTools() {
35292
35368
  location: external_exports.string().optional(),
35293
35369
  rrule: external_exports.string().optional(),
35294
35370
  startTzid: external_exports.string().optional(),
35371
+ registrationFormRef: external_exports.string().optional(),
35372
+ registrationFormViewKey: external_exports.string().optional(),
35295
35373
  confirm: external_exports.boolean().optional()
35296
35374
  }
35297
35375
  },
@@ -35302,21 +35380,27 @@ function buildCalendarTools() {
35302
35380
  `updates event ${args.coordinate}`
35303
35381
  );
35304
35382
  if (blocked) return blocked;
35383
+ const newBegin = args.start ? parseIsoDate(args.start) : void 0;
35384
+ if (args.start && !newBegin) return badDate("start", args.start);
35385
+ const newEnd = args.end ? parseIsoDate(args.end) : void 0;
35386
+ if (args.end && !newEnd) return badDate("end", args.end);
35305
35387
  const viewKey = await service_exports2.lookupEventViewKey(args.coordinate);
35306
35388
  const existing = await service_exports2.fetchCalendarEventByCoordinate(args.coordinate, viewKey);
35307
35389
  if (!existing) return fail(`No event found for ${args.coordinate}.`, "NOT_FOUND");
35390
+ const formRef = args.registrationFormRef === void 0 ? existing.registrationFormRef : args.registrationFormRef || void 0;
35391
+ const formViewKey = !formRef ? void 0 : args.registrationFormViewKey ?? (formRef === existing.registrationFormRef ? existing.registrationFormViewKey : void 0);
35308
35392
  const draft = {
35309
35393
  title: args.title ?? existing.title,
35310
35394
  description: args.description ?? existing.description,
35311
- begin: args.start ? new Date(args.start) : new Date(existing.begin),
35312
- end: args.end ? new Date(args.end) : new Date(existing.end),
35395
+ begin: newBegin ?? new Date(existing.begin),
35396
+ end: newEnd ?? new Date(existing.end),
35313
35397
  location: args.location ?? existing.location[0],
35314
35398
  participants: existing.participants,
35315
35399
  isPrivate: existing.isPrivate,
35316
35400
  rrule: args.rrule ?? existing.repeat.rrule ?? void 0,
35317
35401
  startTzid: args.startTzid ?? existing.startTzid,
35318
- registrationFormRef: existing.registrationFormRef,
35319
- registrationFormViewKey: existing.registrationFormViewKey,
35402
+ registrationFormRef: formRef,
35403
+ registrationFormViewKey: formViewKey,
35320
35404
  notificationPreference: existing.notificationPreference,
35321
35405
  viewKey: existing.viewKey,
35322
35406
  existingId: existing.id
@@ -35331,14 +35415,15 @@ function buildCalendarTools() {
35331
35415
  server.registerTool(
35332
35416
  "attach_form_to_event",
35333
35417
  {
35334
- description: "Attach a Formstr form (naddr or coordinate) as an event's registration form. Requires confirm:true.",
35418
+ description: "Attach a Formstr form (naddr or coordinate) as an event's registration form. For an ENCRYPTED form, also pass formViewKey (the form's view key) \u2014 without it attendees cannot read the attached form. Requires confirm:true.",
35335
35419
  inputSchema: {
35336
35420
  coordinate: external_exports.string(),
35337
35421
  formRef: external_exports.string(),
35422
+ formViewKey: external_exports.string().optional(),
35338
35423
  confirm: external_exports.boolean().optional()
35339
35424
  }
35340
35425
  },
35341
- async ({ coordinate, formRef, confirm }) => {
35426
+ async ({ coordinate, formRef, formViewKey, confirm }) => {
35342
35427
  const blocked = requireConfirm(
35343
35428
  "attach_form_to_event",
35344
35429
  { confirm },
@@ -35348,6 +35433,7 @@ function buildCalendarTools() {
35348
35433
  const viewKey = await service_exports2.lookupEventViewKey(coordinate);
35349
35434
  const existing = await service_exports2.fetchCalendarEventByCoordinate(coordinate, viewKey);
35350
35435
  if (!existing) return fail(`No event found for ${coordinate}.`, "NOT_FOUND");
35436
+ const registrationFormViewKey = formViewKey ?? (formRef === existing.registrationFormRef ? existing.registrationFormViewKey : void 0);
35351
35437
  const draft = {
35352
35438
  title: existing.title,
35353
35439
  description: existing.description,
@@ -35359,6 +35445,7 @@ function buildCalendarTools() {
35359
35445
  rrule: existing.repeat.rrule ?? void 0,
35360
35446
  startTzid: existing.startTzid,
35361
35447
  registrationFormRef: formRef,
35448
+ registrationFormViewKey,
35362
35449
  notificationPreference: existing.notificationPreference,
35363
35450
  viewKey: existing.viewKey,
35364
35451
  existingId: existing.id
@@ -35551,7 +35638,7 @@ function buildDriveTools() {
35551
35638
  server.registerTool(
35552
35639
  "delete_file",
35553
35640
  {
35554
- description: "Delete a file from your drive by name. Requires confirm:true.",
35641
+ description: "Remove a file from your drive index by name. This is a soft delete: the encrypted blob stays on the Blossom server (unreadable without your key) \u2014 only the drive listing forgets it. Requires confirm:true.",
35555
35642
  inputSchema: {
35556
35643
  name: external_exports.string(),
35557
35644
  folder: external_exports.string().optional(),
@@ -35564,7 +35651,9 @@ function buildDriveTools() {
35564
35651
  const resolved = await resolveFile(name, folder);
35565
35652
  if ("error" in resolved) return resolved.error;
35566
35653
  await service_exports4.deleteFile(resolved.file);
35567
- return ok(`Deleted "${name}".`);
35654
+ return ok(
35655
+ `Removed "${name}" from your drive index. The encrypted blob remains on the Blossom server; it is unreadable without your key.`
35656
+ );
35568
35657
  }
35569
35658
  );
35570
35659
  server.registerTool(
@@ -36098,16 +36187,29 @@ function buildPagesTools() {
36098
36187
  return ok(`Saved page "${title}".`, { address: page.address });
36099
36188
  }
36100
36189
  );
36190
+ server.registerTool(
36191
+ "set_page_tags",
36192
+ {
36193
+ description: "Set the private labels/tags for a document address.",
36194
+ inputSchema: { address: external_exports.string(), tags: external_exports.array(external_exports.string()) }
36195
+ },
36196
+ async ({ address, tags }) => {
36197
+ await service_exports3.setDocTags(address, tags);
36198
+ return ok(`Set ${tags.length} tag(s) on ${address}.`);
36199
+ }
36200
+ );
36201
+ write = true;
36101
36202
  server.registerTool(
36102
36203
  "update_page",
36103
36204
  {
36104
- description: "Update an existing document. Pass its docId (d-tag) and the new Markdown.",
36205
+ description: "Update an existing document, replacing its content. Pass its docId (d-tag) and the new Markdown. Requires confirm:true.",
36105
36206
  inputSchema: {
36106
36207
  docId: external_exports.string(),
36107
36208
  content: external_exports.string(),
36108
36209
  title: external_exports.string().optional(),
36109
36210
  viewKey: external_exports.string().optional(),
36110
- editKey: external_exports.string().optional()
36211
+ editKey: external_exports.string().optional(),
36212
+ confirm: external_exports.boolean().optional()
36111
36213
  }
36112
36214
  },
36113
36215
  async ({
@@ -36115,8 +36217,15 @@ function buildPagesTools() {
36115
36217
  content,
36116
36218
  title,
36117
36219
  viewKey,
36118
- editKey
36220
+ editKey,
36221
+ confirm
36119
36222
  }) => {
36223
+ const blocked = requireConfirm(
36224
+ "update_page",
36225
+ { confirm },
36226
+ `overwrites document ${docId} with new content`
36227
+ );
36228
+ if (blocked) return blocked;
36120
36229
  const page = await service_exports3.savePage({
36121
36230
  existingId: docId,
36122
36231
  content: withTitle(title, content),
@@ -36126,18 +36235,6 @@ function buildPagesTools() {
36126
36235
  return ok(`Updated page ${docId}.`, { address: page.address });
36127
36236
  }
36128
36237
  );
36129
- server.registerTool(
36130
- "set_page_tags",
36131
- {
36132
- description: "Set the private labels/tags for a document address.",
36133
- inputSchema: { address: external_exports.string(), tags: external_exports.array(external_exports.string()) }
36134
- },
36135
- async ({ address, tags }) => {
36136
- await service_exports3.setDocTags(address, tags);
36137
- return ok(`Set ${tags.length} tag(s) on ${address}.`);
36138
- }
36139
- );
36140
- write = true;
36141
36238
  server.registerTool(
36142
36239
  "delete_page",
36143
36240
  {
@@ -36287,7 +36384,11 @@ function buildPollsTools() {
36287
36384
  pubkey: poll.pubkey,
36288
36385
  createdAt: poll.createdAt,
36289
36386
  endsAt: poll.endsAt,
36290
- hashtags: poll.hashtags
36387
+ hashtags: poll.hashtags,
36388
+ // Votes on a PoW-gated poll must be NIP-13 mined to this difficulty
36389
+ // (submit_poll_response caps mining at 20) — surface it so callers
36390
+ // know why a vote may be slow or refused.
36391
+ powDifficulty: poll.powDifficulty ?? null
36291
36392
  }
36292
36393
  });
36293
36394
  }
@@ -36326,11 +36427,18 @@ function buildPollsTools() {
36326
36427
  }
36327
36428
  },
36328
36429
  async (args) => {
36430
+ const endsAt = args.endsAt ? new Date(args.endsAt) : void 0;
36431
+ if (args.endsAt && !Number.isFinite(endsAt.getTime())) {
36432
+ return fail(
36433
+ `Could not parse endsAt "${args.endsAt}" \u2014 pass an ISO 8601 date-time (e.g. 2026-07-02T15:00:00Z).`,
36434
+ "BAD_INPUT"
36435
+ );
36436
+ }
36329
36437
  const poll = await service_exports5.createPoll({
36330
36438
  question: args.question,
36331
36439
  options: args.options.map((label) => ({ label })),
36332
36440
  pollType: args.pollType ?? "singlechoice",
36333
- endsAt: args.endsAt ? new Date(args.endsAt) : void 0,
36441
+ endsAt,
36334
36442
  hashtags: args.hashtags
36335
36443
  });
36336
36444
  return ok(`Created poll "${args.question}".`, { id: poll.id });
@@ -36401,6 +36509,26 @@ function buildPollsTools() {
36401
36509
  return tools;
36402
36510
  }
36403
36511
 
36512
+ // ../agent/src/tools/validate.ts
36513
+ function withStrictArgs(entry) {
36514
+ const schema = external_exports.object(entry.inputSchema).strict();
36515
+ return {
36516
+ ...entry,
36517
+ handler: async (args, ctx) => {
36518
+ const parsed = schema.safeParse(args ?? {});
36519
+ if (!parsed.success) {
36520
+ const problems = parsed.error.issues.map((i4) => i4.path.length ? `${i4.path.join(".")}: ${i4.message}` : i4.message).join("; ");
36521
+ const valid = Object.keys(entry.inputSchema);
36522
+ return fail(
36523
+ `Invalid arguments for ${entry.name}: ${problems}. Valid parameters: ${valid.length ? valid.join(", ") : "(none)"}.`,
36524
+ "BAD_INPUT"
36525
+ );
36526
+ }
36527
+ return entry.handler(parsed.data, ctx);
36528
+ }
36529
+ };
36530
+ }
36531
+
36404
36532
  // ../agent/src/tools/index.ts
36405
36533
  var toolRegistry = [
36406
36534
  ...formsTools,
@@ -36408,7 +36536,7 @@ var toolRegistry = [
36408
36536
  ...pagesTools,
36409
36537
  ...pollsTools,
36410
36538
  ...driveTools
36411
- ];
36539
+ ].map(withStrictArgs);
36412
36540
 
36413
36541
  // ../../node_modules/.pnpm/zod-to-json-schema@3.25.2_zod@3.25.76/node_modules/zod-to-json-schema/dist/esm/Options.js
36414
36542
  var ignoreOverride = /* @__PURE__ */ Symbol("Let zodToJsonSchema decide on which parser to use");
@@ -46581,33 +46709,6 @@ var StdioServerTransport = class {
46581
46709
  }
46582
46710
  };
46583
46711
 
46584
- // src/server.ts
46585
- function adapt(r) {
46586
- const text = r.ok || r.errorCode === void 0 ? r.text : `${r.text} (${r.errorCode})`;
46587
- return {
46588
- content: [{ type: "text", text }],
46589
- ...r.data !== void 0 ? { structuredContent: r.data } : {},
46590
- ...r.ok ? {} : { isError: true }
46591
- };
46592
- }
46593
- function buildServer(ctx) {
46594
- const server = new McpServer({ name: "formstr", version: "0.1.0" });
46595
- for (const t of toolRegistry) {
46596
- if (t.write && !ctx.allowWrites) continue;
46597
- server.registerTool(
46598
- t.name,
46599
- { description: t.description, inputSchema: t.inputSchema },
46600
- async (args) => adapt(await t.handler(args, ctx))
46601
- );
46602
- }
46603
- return server;
46604
- }
46605
- async function startStdio(ctx) {
46606
- const server = buildServer(ctx);
46607
- const transport = new StdioServerTransport();
46608
- await server.connect(transport);
46609
- }
46610
-
46611
46712
  // src/version.ts
46612
46713
  var import_node_fs2 = require("fs");
46613
46714
  var import_node_path2 = require("path");
@@ -46697,6 +46798,37 @@ function formatVersionReport(installed, latest) {
46697
46798
  return `${head} (latest)`;
46698
46799
  }
46699
46800
 
46801
+ // src/server.ts
46802
+ function adapt(r) {
46803
+ const text = r.ok || r.errorCode === void 0 ? r.text : `${r.text} (${r.errorCode})`;
46804
+ return {
46805
+ content: [{ type: "text", text }],
46806
+ ...r.data !== void 0 ? { structuredContent: r.data } : {},
46807
+ ...r.ok ? {} : { isError: true }
46808
+ };
46809
+ }
46810
+ function buildServer(ctx) {
46811
+ const server = new McpServer({ name: "formstr", version: readInstalledVersion() });
46812
+ for (const t of toolRegistry) {
46813
+ if (t.write && !ctx.allowWrites) continue;
46814
+ server.registerTool(
46815
+ t.name,
46816
+ // Strict object, not the raw shape: the SDK's own parse would silently
46817
+ // STRIP unknown keys before our handler runs, so a misspelled parameter
46818
+ // looked accepted while doing nothing. Strict makes it an InvalidParams
46819
+ // error the model can react to (and advertises additionalProperties:false).
46820
+ { description: t.description, inputSchema: external_exports.object(t.inputSchema).strict() },
46821
+ async (args) => adapt(await t.handler(args, ctx))
46822
+ );
46823
+ }
46824
+ return server;
46825
+ }
46826
+ async function startStdio(ctx) {
46827
+ const server = buildServer(ctx);
46828
+ const transport = new StdioServerTransport();
46829
+ await server.connect(transport);
46830
+ }
46831
+
46700
46832
  // src/index.ts
46701
46833
  async function runServer(cli) {
46702
46834
  const cfg = resolveConfig(cli, process.env);
@@ -46712,7 +46844,7 @@ async function runServer(cli) {
46712
46844
  } : {};
46713
46845
  const account = await bootstrap({ account: cfg.account, relays: cfg.relays }, deps);
46714
46846
  console.error(
46715
- `formstr-mcp: signed in as ${account.npub} (${account.method}), writes ${cfg.allowWrites ? "ENABLED" : "disabled"}`
46847
+ `formstr-mcp: signed in as ${account.npub} (${account.method}), gated writes ${cfg.allowWrites ? "ENABLED" : "disabled (creates always on)"}`
46716
46848
  );
46717
46849
  await startStdio({ allowWrites: cfg.allowWrites });
46718
46850
  console.error("formstr-mcp: server running on stdio");