@formstr/mcp 0.3.2 → 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;
@@ -27493,12 +27499,23 @@ async function unlockNcryptsec(signer, account, deps) {
27493
27499
  }
27494
27500
 
27495
27501
  // src/cli.ts
27496
- var SUBCOMMANDS = /* @__PURE__ */ new Set(["login", "logout", "whoami", "accounts", "switch", "help"]);
27502
+ var SUBCOMMANDS = /* @__PURE__ */ new Set([
27503
+ "login",
27504
+ "logout",
27505
+ "whoami",
27506
+ "accounts",
27507
+ "switch",
27508
+ "help",
27509
+ "version"
27510
+ ]);
27497
27511
  function parseCli(argv) {
27498
27512
  const rest = [...argv];
27499
27513
  if (rest.includes("-h") || rest.includes("--help")) {
27500
27514
  return { command: "help", allowWrites: false };
27501
27515
  }
27516
+ if (rest.includes("-v") || rest.includes("--version")) {
27517
+ return { command: "version", allowWrites: false };
27518
+ }
27502
27519
  let command = "run";
27503
27520
  if (rest[0] && SUBCOMMANDS.has(rest[0])) {
27504
27521
  command = rest.shift();
@@ -27539,15 +27556,40 @@ function helpText() {
27539
27556
  " accounts List stored accounts ('*' marks the active one).",
27540
27557
  " switch <npub> Set the active account (accepts an npub or hex pubkey).",
27541
27558
  " help Show this help (also -h, --help).",
27559
+ " version Print the installed version and check for an update (also -v, --version).",
27542
27560
  "",
27543
27561
  "Flags:",
27544
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.",
27545
27565
  " --account <npub|hex> Boot a specific account instead of the active one.",
27546
27566
  " --relays <a,b,\u2026> Override relays (comma-separated).",
27547
27567
  "",
27548
27568
  "Env:",
27549
27569
  " FORMSTR_MCP_NCRYPTSEC_PASSPHRASE Unlock the active ncryptsec account at boot.",
27550
- " FORMSTR_MCP_PASSPHRASE Encrypt the keystore file (keychain-less hosts)."
27570
+ " FORMSTR_MCP_PASSPHRASE Encrypt the keystore file (keychain-less hosts).",
27571
+ "",
27572
+ "Setting the passphrase in your MCP host config:",
27573
+ " When an MCP host (Claude Desktop/Code, Cursor, \u2026) spawns the server, stdin is the",
27574
+ " JSON-RPC channel, so it can't prompt for your ncryptsec passphrase. Pass it via an",
27575
+ ' "env" block in the server entry of your MCP config (e.g. mcp_config.json /',
27576
+ " claude_desktop_config.json) so the host hands it to the server at startup:",
27577
+ "",
27578
+ " {",
27579
+ ' "mcpServers": {',
27580
+ ' "formstr": {',
27581
+ ' "command": "npx",',
27582
+ ' "args": ["-y", "@formstr/mcp"],',
27583
+ ' "env": {',
27584
+ ' "FORMSTR_MCP_NCRYPTSEC_PASSPHRASE": "your-passphrase-here"',
27585
+ " }",
27586
+ " }",
27587
+ " }",
27588
+ " }",
27589
+ "",
27590
+ " Tip: a NIP-46 (bunker) account needs no passphrase \u2014 `switch` to one and the config",
27591
+ ' can stay secret-free. Add "--allow-writes" to args to enable gated write tools',
27592
+ " (creates are always on)."
27551
27593
  ].join("\n");
27552
27594
  }
27553
27595
 
@@ -32373,11 +32415,11 @@ async function decryptListEntries(signer, userPubkey, content) {
32373
32415
  const parsed = JSON.parse(decrypted);
32374
32416
  return Array.isArray(parsed) ? parsed : [];
32375
32417
  }
32376
- async function publishMyFormsList(signer, relays, entries) {
32418
+ async function publishMyFormsList(signer, relays, entries, prevCreatedAt) {
32377
32419
  const encrypted = await nip44SelfEncrypt(signer, JSON.stringify(entries));
32378
32420
  const event = {
32379
32421
  kind: FORM_KINDS.myFormsList,
32380
- created_at: Math.floor(Date.now() / 1e3),
32422
+ created_at: Math.max(Math.floor(Date.now() / 1e3), prevCreatedAt + 1),
32381
32423
  tags: [],
32382
32424
  content: encrypted
32383
32425
  };
@@ -32431,20 +32473,33 @@ async function fetchMyForms() {
32431
32473
  return summary;
32432
32474
  }).filter((s) => s !== null);
32433
32475
  }
32476
+ var MY_FORMS_LIST_READ_ATTEMPTS = 3;
32477
+ async function readMyFormsEntriesForWrite(signer, userPubkey, relays) {
32478
+ let listEvent = null;
32479
+ for (let attempt = 0; attempt < MY_FORMS_LIST_READ_ATTEMPTS; attempt++) {
32480
+ listEvent = await fetchLatestMyFormsEvent(relays, userPubkey);
32481
+ if (listEvent?.content) break;
32482
+ }
32483
+ if (!listEvent?.content) return { entries: [], prevCreatedAt: 0 };
32484
+ try {
32485
+ const entries = await decryptListEntries(signer, userPubkey, listEvent.content);
32486
+ return { entries, prevCreatedAt: listEvent.created_at };
32487
+ } catch (err) {
32488
+ throw new Error(
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)})`
32490
+ );
32491
+ }
32492
+ }
32434
32493
  async function appendToMyFormsList(formPubkey, formId, relay, signingKeyHex, viewKeyHex) {
32435
32494
  const signer = await signerManager.getSigner();
32436
32495
  const userPubkey = await signer.getPublicKey();
32437
32496
  const relays = relayManager.getRelaysForModule("forms");
32438
- const existing = await fetchLatestMyFormsEvent(relays, userPubkey);
32439
- let entries = [];
32440
- if (existing?.content) {
32441
- try {
32442
- entries = await decryptListEntries(signer, userPubkey, existing.content);
32443
- } catch {
32444
- entries = [];
32445
- }
32446
- }
32447
- entries = entries.map(
32497
+ const { entries: existing, prevCreatedAt } = await readMyFormsEntriesForWrite(
32498
+ signer,
32499
+ userPubkey,
32500
+ relays
32501
+ );
32502
+ const entries = existing.map(
32448
32503
  (e) => e[0] === "f" && e.length < 4 ? ["f", e[1] ?? "", e[2] ?? "", ""] : e
32449
32504
  );
32450
32505
  const coordKey = `${formPubkey}:${formId}`;
@@ -32452,10 +32507,11 @@ async function appendToMyFormsList(formPubkey, formId, relay, signingKeyHex, vie
32452
32507
  const secrets = signingKeyHex ? encodeFormKeys(signingKeyHex, viewKeyHex) : "";
32453
32508
  entries.push(["f", coordKey, relay, secrets]);
32454
32509
  }
32455
- await publishMyFormsList(signer, relays, entries);
32510
+ await publishMyFormsList(signer, relays, entries, prevCreatedAt);
32456
32511
  }
32457
32512
  async function saveToMyForms(summaries) {
32458
32513
  const signer = await signerManager.getSigner();
32514
+ const userPubkey = await signer.getPublicKey();
32459
32515
  const relays = relayManager.getRelaysForModule("forms");
32460
32516
  const entries = summaries.map((s) => [
32461
32517
  "f",
@@ -32463,7 +32519,8 @@ async function saveToMyForms(summaries) {
32463
32519
  s.relay ?? "",
32464
32520
  s.signingKey ? encodeFormKeys(s.signingKey, s.viewKey) : ""
32465
32521
  ]);
32466
- await publishMyFormsList(signer, relays, entries);
32522
+ const prev = await fetchLatestMyFormsEvent(relays, userPubkey);
32523
+ await publishMyFormsList(signer, relays, entries, prev?.created_at ?? 0);
32467
32524
  }
32468
32525
  async function fetchMyFormsByAuthor(pubkey, relays) {
32469
32526
  const filter = {
@@ -32510,7 +32567,7 @@ async function deleteForm(formId, formPubkey) {
32510
32567
  const coordKey = `${formPubkey}:${formId}`;
32511
32568
  const trimmed = entries.filter((e) => e[1] !== coordKey);
32512
32569
  if (trimmed.length === entries.length) return;
32513
- await publishMyFormsList(signer, relays, trimmed);
32570
+ await publishMyFormsList(signer, relays, trimmed, listEvent.created_at);
32514
32571
  }
32515
32572
  async function updateForm(params) {
32516
32573
  const relays = relayManager.getRelaysForModule("forms");
@@ -32627,9 +32684,13 @@ async function fetchFormSummaryFromRef(pubkey, formId) {
32627
32684
  };
32628
32685
  }
32629
32686
  async function importForm(summary) {
32630
- const current = await fetchMyForms();
32631
- if (current.some((f) => f.id === summary.id && f.pubkey === summary.pubkey)) return;
32632
- await saveToMyForms([...current, summary]);
32687
+ await appendToMyFormsList(
32688
+ summary.pubkey,
32689
+ summary.id,
32690
+ summary.relay ?? "",
32691
+ summary.signingKey,
32692
+ summary.viewKey
32693
+ );
32633
32694
  }
32634
32695
  function parseFormEvent(event, decryptedRows) {
32635
32696
  const merged = decryptedRows ? [...event.tags, ...decryptedRows] : event.tags;
@@ -34242,7 +34303,7 @@ async function fetchAllDocMetadata() {
34242
34303
  }
34243
34304
  return result;
34244
34305
  }
34245
- async function fetchDocMetadata(address) {
34306
+ async function fetchNewestDocMetadataEvent(address) {
34246
34307
  const signer = await signerManager.getSigner();
34247
34308
  const pubkey = await signer.getPublicKey();
34248
34309
  const events = await nostrRuntime.querySync(RELAYS(), {
@@ -34251,7 +34312,12 @@ async function fetchDocMetadata(address) {
34251
34312
  "#d": [address]
34252
34313
  });
34253
34314
  if (events.length === 0) return void 0;
34254
- 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;
34255
34321
  try {
34256
34322
  return parseMetadataJson(await nip44SelfDecrypt(signer, newest.content));
34257
34323
  } catch {
@@ -34259,14 +34325,24 @@ async function fetchDocMetadata(address) {
34259
34325
  }
34260
34326
  }
34261
34327
  async function saveDocMetadata(address, patch) {
34262
- 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
+ }
34263
34337
  const merged = { ...existing, ...patch };
34264
34338
  if (!Array.isArray(merged.tags)) merged.tags = [];
34265
- const signer = await signerManager.getSigner();
34266
34339
  const content = await nip44SelfEncrypt(signer, JSON.stringify(merged));
34267
34340
  const signed = await signer.signEvent({
34268
34341
  kind: PAGES_KINDS.docMetadata,
34269
- 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),
34270
34346
  tags: [["d", address]],
34271
34347
  content
34272
34348
  });
@@ -34515,14 +34591,22 @@ async function fetchFileIndex() {
34515
34591
  }
34516
34592
  async function saveFileMetadata(metadata) {
34517
34593
  const signer = await signerManager.getSigner();
34594
+ const relays = relayManager.getRelaysForModule("drive");
34518
34595
  const full = {
34519
34596
  ...metadata,
34520
34597
  encryptionAlgorithm: metadata.encryptionAlgorithm ?? "aes-gcm"
34521
34598
  };
34522
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);
34523
34607
  const event = {
34524
34608
  kind: DRIVE_KINDS.fileMetadata,
34525
- created_at: Math.floor(Date.now() / 1e3),
34609
+ created_at: Math.max(Math.floor(Date.now() / 1e3), prevCreatedAt + 1),
34526
34610
  tags: [
34527
34611
  ["d", metadata.hash],
34528
34612
  ["client", "formstr-drive"],
@@ -34531,7 +34615,6 @@ async function saveFileMetadata(metadata) {
34531
34615
  content: encrypted
34532
34616
  };
34533
34617
  const signed = await signer.signEvent(event);
34534
- const relays = relayManager.getRelaysForModule("drive");
34535
34618
  await nostrRuntime.publish(relays, signed);
34536
34619
  }
34537
34620
  async function updateFileMetadata(hash, updates) {
@@ -34616,6 +34699,7 @@ __export(service_exports5, {
34616
34699
  });
34617
34700
 
34618
34701
  // ../agent/src/services/polls/pow.ts
34702
+ var MAX_MINED_POW_DIFFICULTY = 20;
34619
34703
  function minePollEvent(unsigned, difficulty) {
34620
34704
  const event = unsigned;
34621
34705
  const nonceTag = ["nonce", "0", difficulty.toString()];
@@ -34708,6 +34792,11 @@ async function submitPollResponse(pollId, pollAuthor, selectedOptionIds, pollRel
34708
34792
  tags,
34709
34793
  content: ""
34710
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
+ }
34711
34800
  if (powDifficulty && powDifficulty > 0) {
34712
34801
  const unsigned = { ...event, pubkey: await signer.getPublicKey() };
34713
34802
  const mined = minePollEvent(unsigned, powDifficulty);
@@ -34898,6 +34987,16 @@ function normalizePubkeyList(values) {
34898
34987
  }
34899
34988
 
34900
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
+ }
34901
35000
  var calendarTools = buildCalendarTools();
34902
35001
  function buildCalendarTools() {
34903
35002
  const tools = [];
@@ -34914,11 +35013,15 @@ function buildCalendarTools() {
34914
35013
  inputSchema: { since: external_exports.string().optional(), until: external_exports.string().optional() }
34915
35014
  },
34916
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);
34917
35020
  const pubkey = signerManager.getPublicKey();
34918
35021
  const events = await service_exports2.fetchCalendarEventsSync({
34919
35022
  authors: pubkey ? [pubkey] : void 0,
34920
- since: since ? Math.floor(new Date(since).getTime() / 1e3) : void 0,
34921
- 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
34922
35025
  });
34923
35026
  return ok(`Found ${events.length} event(s).`, {
34924
35027
  events: events.map((e) => ({
@@ -34936,7 +35039,7 @@ function buildCalendarTools() {
34936
35039
  server.registerTool(
34937
35040
  "create_calendar_event",
34938
35041
  {
34939
- 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.",
34940
35043
  inputSchema: {
34941
35044
  title: external_exports.string(),
34942
35045
  description: external_exports.string().optional(),
@@ -34948,10 +35051,15 @@ function buildCalendarTools() {
34948
35051
  participants: external_exports.array(external_exports.string()).optional(),
34949
35052
  rrule: external_exports.string().optional(),
34950
35053
  startTzid: external_exports.string().optional(),
34951
- registrationFormRef: external_exports.string().optional()
35054
+ registrationFormRef: external_exports.string().optional(),
35055
+ registrationFormViewKey: external_exports.string().optional()
34952
35056
  }
34953
35057
  },
34954
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);
34955
35063
  const isPrivate = args.isPrivate ?? true;
34956
35064
  const calendars = await service_exports2.fetchCalendarLists();
34957
35065
  if (isPrivate && !args.calendarId && calendars.length > 0) {
@@ -34968,8 +35076,6 @@ function buildCalendarTools() {
34968
35076
  "NOT_FOUND"
34969
35077
  );
34970
35078
  }
34971
- const begin = new Date(args.start);
34972
- const end = args.end ? new Date(args.end) : new Date(begin.getTime() + 36e5);
34973
35079
  const draft = {
34974
35080
  title: args.title,
34975
35081
  description: args.description ?? "",
@@ -34983,17 +35089,20 @@ function buildCalendarTools() {
34983
35089
  calendarId: args.calendarId,
34984
35090
  rrule: args.rrule,
34985
35091
  startTzid: args.startTzid,
34986
- registrationFormRef: args.registrationFormRef
35092
+ registrationFormRef: args.registrationFormRef,
35093
+ registrationFormViewKey: args.registrationFormViewKey
34987
35094
  };
34988
35095
  const { event, calendar: list } = await service_exports2.createCalendarEvent(draft, { calendars });
34989
35096
  const coordinate = `${event.kind}:${event.user}:${event.id}`;
35097
+ const invitationsSent = isPrivate ? draft.participants?.length ?? 0 : 0;
34990
35098
  return ok(
34991
- `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)` : ""}.`,
34992
35100
  {
34993
35101
  id: event.id,
34994
35102
  eventId: event.eventId,
34995
35103
  coordinate,
34996
- calendarId: list?.id
35104
+ calendarId: list?.id,
35105
+ invitationsSent
34997
35106
  }
34998
35107
  );
34999
35108
  }
@@ -35012,12 +35121,19 @@ function buildCalendarTools() {
35012
35121
  event: {
35013
35122
  id: event.id,
35014
35123
  title: event.title,
35124
+ description: event.description,
35015
35125
  begin: event.begin,
35016
35126
  end: event.end,
35017
35127
  location: event.location,
35018
35128
  isPrivate: event.isPrivate,
35019
35129
  rrule: event.repeat?.rrule ?? null,
35020
- 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)
35021
35137
  }
35022
35138
  });
35023
35139
  }
@@ -35028,7 +35144,12 @@ function buildCalendarTools() {
35028
35144
  async () => {
35029
35145
  const lists = await service_exports2.fetchCalendarLists();
35030
35146
  return ok(`Found ${lists.length} calendar(s).`, {
35031
- 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
+ }))
35032
35153
  });
35033
35154
  }
35034
35155
  );
@@ -35237,7 +35358,7 @@ function buildCalendarTools() {
35237
35358
  server.registerTool(
35238
35359
  "update_calendar_event",
35239
35360
  {
35240
- 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.",
35241
35362
  inputSchema: {
35242
35363
  coordinate: external_exports.string(),
35243
35364
  title: external_exports.string().optional(),
@@ -35247,6 +35368,8 @@ function buildCalendarTools() {
35247
35368
  location: external_exports.string().optional(),
35248
35369
  rrule: external_exports.string().optional(),
35249
35370
  startTzid: external_exports.string().optional(),
35371
+ registrationFormRef: external_exports.string().optional(),
35372
+ registrationFormViewKey: external_exports.string().optional(),
35250
35373
  confirm: external_exports.boolean().optional()
35251
35374
  }
35252
35375
  },
@@ -35257,21 +35380,27 @@ function buildCalendarTools() {
35257
35380
  `updates event ${args.coordinate}`
35258
35381
  );
35259
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);
35260
35387
  const viewKey = await service_exports2.lookupEventViewKey(args.coordinate);
35261
35388
  const existing = await service_exports2.fetchCalendarEventByCoordinate(args.coordinate, viewKey);
35262
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);
35263
35392
  const draft = {
35264
35393
  title: args.title ?? existing.title,
35265
35394
  description: args.description ?? existing.description,
35266
- begin: args.start ? new Date(args.start) : new Date(existing.begin),
35267
- 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),
35268
35397
  location: args.location ?? existing.location[0],
35269
35398
  participants: existing.participants,
35270
35399
  isPrivate: existing.isPrivate,
35271
35400
  rrule: args.rrule ?? existing.repeat.rrule ?? void 0,
35272
35401
  startTzid: args.startTzid ?? existing.startTzid,
35273
- registrationFormRef: existing.registrationFormRef,
35274
- registrationFormViewKey: existing.registrationFormViewKey,
35402
+ registrationFormRef: formRef,
35403
+ registrationFormViewKey: formViewKey,
35275
35404
  notificationPreference: existing.notificationPreference,
35276
35405
  viewKey: existing.viewKey,
35277
35406
  existingId: existing.id
@@ -35286,14 +35415,15 @@ function buildCalendarTools() {
35286
35415
  server.registerTool(
35287
35416
  "attach_form_to_event",
35288
35417
  {
35289
- 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.",
35290
35419
  inputSchema: {
35291
35420
  coordinate: external_exports.string(),
35292
35421
  formRef: external_exports.string(),
35422
+ formViewKey: external_exports.string().optional(),
35293
35423
  confirm: external_exports.boolean().optional()
35294
35424
  }
35295
35425
  },
35296
- async ({ coordinate, formRef, confirm }) => {
35426
+ async ({ coordinate, formRef, formViewKey, confirm }) => {
35297
35427
  const blocked = requireConfirm(
35298
35428
  "attach_form_to_event",
35299
35429
  { confirm },
@@ -35303,6 +35433,7 @@ function buildCalendarTools() {
35303
35433
  const viewKey = await service_exports2.lookupEventViewKey(coordinate);
35304
35434
  const existing = await service_exports2.fetchCalendarEventByCoordinate(coordinate, viewKey);
35305
35435
  if (!existing) return fail(`No event found for ${coordinate}.`, "NOT_FOUND");
35436
+ const registrationFormViewKey = formViewKey ?? (formRef === existing.registrationFormRef ? existing.registrationFormViewKey : void 0);
35306
35437
  const draft = {
35307
35438
  title: existing.title,
35308
35439
  description: existing.description,
@@ -35314,6 +35445,7 @@ function buildCalendarTools() {
35314
35445
  rrule: existing.repeat.rrule ?? void 0,
35315
35446
  startTzid: existing.startTzid,
35316
35447
  registrationFormRef: formRef,
35448
+ registrationFormViewKey,
35317
35449
  notificationPreference: existing.notificationPreference,
35318
35450
  viewKey: existing.viewKey,
35319
35451
  existingId: existing.id
@@ -35506,7 +35638,7 @@ function buildDriveTools() {
35506
35638
  server.registerTool(
35507
35639
  "delete_file",
35508
35640
  {
35509
- 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.",
35510
35642
  inputSchema: {
35511
35643
  name: external_exports.string(),
35512
35644
  folder: external_exports.string().optional(),
@@ -35519,7 +35651,9 @@ function buildDriveTools() {
35519
35651
  const resolved = await resolveFile(name, folder);
35520
35652
  if ("error" in resolved) return resolved.error;
35521
35653
  await service_exports4.deleteFile(resolved.file);
35522
- 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
+ );
35523
35657
  }
35524
35658
  );
35525
35659
  server.registerTool(
@@ -36053,16 +36187,29 @@ function buildPagesTools() {
36053
36187
  return ok(`Saved page "${title}".`, { address: page.address });
36054
36188
  }
36055
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;
36056
36202
  server.registerTool(
36057
36203
  "update_page",
36058
36204
  {
36059
- 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.",
36060
36206
  inputSchema: {
36061
36207
  docId: external_exports.string(),
36062
36208
  content: external_exports.string(),
36063
36209
  title: external_exports.string().optional(),
36064
36210
  viewKey: external_exports.string().optional(),
36065
- editKey: external_exports.string().optional()
36211
+ editKey: external_exports.string().optional(),
36212
+ confirm: external_exports.boolean().optional()
36066
36213
  }
36067
36214
  },
36068
36215
  async ({
@@ -36070,8 +36217,15 @@ function buildPagesTools() {
36070
36217
  content,
36071
36218
  title,
36072
36219
  viewKey,
36073
- editKey
36220
+ editKey,
36221
+ confirm
36074
36222
  }) => {
36223
+ const blocked = requireConfirm(
36224
+ "update_page",
36225
+ { confirm },
36226
+ `overwrites document ${docId} with new content`
36227
+ );
36228
+ if (blocked) return blocked;
36075
36229
  const page = await service_exports3.savePage({
36076
36230
  existingId: docId,
36077
36231
  content: withTitle(title, content),
@@ -36081,18 +36235,6 @@ function buildPagesTools() {
36081
36235
  return ok(`Updated page ${docId}.`, { address: page.address });
36082
36236
  }
36083
36237
  );
36084
- server.registerTool(
36085
- "set_page_tags",
36086
- {
36087
- description: "Set the private labels/tags for a document address.",
36088
- inputSchema: { address: external_exports.string(), tags: external_exports.array(external_exports.string()) }
36089
- },
36090
- async ({ address, tags }) => {
36091
- await service_exports3.setDocTags(address, tags);
36092
- return ok(`Set ${tags.length} tag(s) on ${address}.`);
36093
- }
36094
- );
36095
- write = true;
36096
36238
  server.registerTool(
36097
36239
  "delete_page",
36098
36240
  {
@@ -36242,7 +36384,11 @@ function buildPollsTools() {
36242
36384
  pubkey: poll.pubkey,
36243
36385
  createdAt: poll.createdAt,
36244
36386
  endsAt: poll.endsAt,
36245
- 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
36246
36392
  }
36247
36393
  });
36248
36394
  }
@@ -36281,11 +36427,18 @@ function buildPollsTools() {
36281
36427
  }
36282
36428
  },
36283
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
+ }
36284
36437
  const poll = await service_exports5.createPoll({
36285
36438
  question: args.question,
36286
36439
  options: args.options.map((label) => ({ label })),
36287
36440
  pollType: args.pollType ?? "singlechoice",
36288
- endsAt: args.endsAt ? new Date(args.endsAt) : void 0,
36441
+ endsAt,
36289
36442
  hashtags: args.hashtags
36290
36443
  });
36291
36444
  return ok(`Created poll "${args.question}".`, { id: poll.id });
@@ -36356,6 +36509,26 @@ function buildPollsTools() {
36356
36509
  return tools;
36357
36510
  }
36358
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
+
36359
36532
  // ../agent/src/tools/index.ts
36360
36533
  var toolRegistry = [
36361
36534
  ...formsTools,
@@ -36363,7 +36536,7 @@ var toolRegistry = [
36363
36536
  ...pagesTools,
36364
36537
  ...pollsTools,
36365
36538
  ...driveTools
36366
- ];
36539
+ ].map(withStrictArgs);
36367
36540
 
36368
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
36369
36542
  var ignoreOverride = /* @__PURE__ */ Symbol("Let zodToJsonSchema decide on which parser to use");
@@ -46536,6 +46709,95 @@ var StdioServerTransport = class {
46536
46709
  }
46537
46710
  };
46538
46711
 
46712
+ // src/version.ts
46713
+ var import_node_fs2 = require("fs");
46714
+ var import_node_path2 = require("path");
46715
+ var PACKAGE_NAME = "@formstr/mcp";
46716
+ function readInstalledVersion() {
46717
+ try {
46718
+ const pkg = JSON.parse((0, import_node_fs2.readFileSync)((0, import_node_path2.join)(__dirname, "..", "package.json"), "utf8"));
46719
+ return pkg.version ?? "0.0.0";
46720
+ } catch {
46721
+ return "0.0.0";
46722
+ }
46723
+ }
46724
+ function parseSemver(version2) {
46725
+ const cleaned = version2.trim().replace(/^v/i, "").split("+")[0];
46726
+ const [core, pre] = cleaned.split("-", 2);
46727
+ const release = core.split(".").map((n) => Number.parseInt(n, 10) || 0);
46728
+ while (release.length < 3) release.push(0);
46729
+ return { release, prerelease: pre ? pre.split(".") : [] };
46730
+ }
46731
+ function compareIdentifiers(a, b) {
46732
+ const na = /^\d+$/.test(a);
46733
+ const nb = /^\d+$/.test(b);
46734
+ if (na && nb) return Number(a) - Number(b) < 0 ? -1 : Number(a) === Number(b) ? 0 : 1;
46735
+ if (na) return -1;
46736
+ if (nb) return 1;
46737
+ return a < b ? -1 : a > b ? 1 : 0;
46738
+ }
46739
+ function compareVersions(a, b) {
46740
+ const va = parseSemver(a);
46741
+ const vb = parseSemver(b);
46742
+ for (let i4 = 0; i4 < 3; i4++) {
46743
+ if (va.release[i4] !== vb.release[i4]) return va.release[i4] < vb.release[i4] ? -1 : 1;
46744
+ }
46745
+ if (va.prerelease.length === 0 && vb.prerelease.length === 0) return 0;
46746
+ if (va.prerelease.length === 0) return 1;
46747
+ if (vb.prerelease.length === 0) return -1;
46748
+ const len = Math.max(va.prerelease.length, vb.prerelease.length);
46749
+ for (let i4 = 0; i4 < len; i4++) {
46750
+ const ia = va.prerelease[i4];
46751
+ const ib = vb.prerelease[i4];
46752
+ if (ia === void 0) return -1;
46753
+ if (ib === void 0) return 1;
46754
+ const c = compareIdentifiers(ia, ib);
46755
+ if (c !== 0) return c < 0 ? -1 : 1;
46756
+ }
46757
+ return 0;
46758
+ }
46759
+ async function fetchLatestVersion(pkgName, opts = {}) {
46760
+ const fetchImpl = opts.fetchImpl ?? fetch;
46761
+ const timeoutMs = opts.timeoutMs ?? 3e3;
46762
+ const controller = new AbortController();
46763
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
46764
+ try {
46765
+ const res = await fetchImpl(`https://registry.npmjs.org/${pkgName}/latest`, {
46766
+ signal: controller.signal,
46767
+ // The abbreviated metadata document is far smaller than the full packument.
46768
+ headers: { accept: "application/vnd.npm.install-v1+json" }
46769
+ });
46770
+ if (!res.ok) return null;
46771
+ const body = await res.json();
46772
+ return typeof body.version === "string" ? body.version : null;
46773
+ } catch {
46774
+ return null;
46775
+ } finally {
46776
+ clearTimeout(timer);
46777
+ }
46778
+ }
46779
+ function formatVersionReport(installed, latest) {
46780
+ const head = `${PACKAGE_NAME} ${installed}`;
46781
+ if (!latest) {
46782
+ return `${head}
46783
+ (could not check for updates \u2014 offline or registry unreachable)`;
46784
+ }
46785
+ const cmp = compareVersions(installed, latest);
46786
+ if (cmp < 0) {
46787
+ return [
46788
+ head,
46789
+ `Update available: ${latest} (you have ${installed}).`,
46790
+ `Upgrade: npm install -g ${PACKAGE_NAME}@latest`,
46791
+ `Or just re-run via: npx -y ${PACKAGE_NAME}@latest`
46792
+ ].join("\n");
46793
+ }
46794
+ if (cmp > 0) {
46795
+ return `${head}
46796
+ (ahead of the latest published release ${latest})`;
46797
+ }
46798
+ return `${head} (latest)`;
46799
+ }
46800
+
46539
46801
  // src/server.ts
46540
46802
  function adapt(r) {
46541
46803
  const text = r.ok || r.errorCode === void 0 ? r.text : `${r.text} (${r.errorCode})`;
@@ -46546,12 +46808,16 @@ function adapt(r) {
46546
46808
  };
46547
46809
  }
46548
46810
  function buildServer(ctx) {
46549
- const server = new McpServer({ name: "formstr", version: "0.1.0" });
46811
+ const server = new McpServer({ name: "formstr", version: readInstalledVersion() });
46550
46812
  for (const t of toolRegistry) {
46551
46813
  if (t.write && !ctx.allowWrites) continue;
46552
46814
  server.registerTool(
46553
46815
  t.name,
46554
- { description: t.description, inputSchema: t.inputSchema },
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() },
46555
46821
  async (args) => adapt(await t.handler(args, ctx))
46556
46822
  );
46557
46823
  }
@@ -46578,7 +46844,7 @@ async function runServer(cli) {
46578
46844
  } : {};
46579
46845
  const account = await bootstrap({ account: cfg.account, relays: cfg.relays }, deps);
46580
46846
  console.error(
46581
- `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)"}`
46582
46848
  );
46583
46849
  await startStdio({ allowWrites: cfg.allowWrites });
46584
46850
  console.error("formstr-mcp: server running on stdio");
@@ -46653,6 +46919,12 @@ async function main() {
46653
46919
  case "help":
46654
46920
  console.error(helpText());
46655
46921
  return cli.command;
46922
+ case "version": {
46923
+ const installed = readInstalledVersion();
46924
+ const latest = await fetchLatestVersion(PACKAGE_NAME);
46925
+ console.log(formatVersionReport(installed, latest));
46926
+ return cli.command;
46927
+ }
46656
46928
  case "run":
46657
46929
  default:
46658
46930
  await runServer(cli);