@formstr/mcp 0.2.0 → 0.3.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/README.md CHANGED
@@ -21,7 +21,9 @@ npx -y @formstr/mcp login
21
21
  ```
22
22
 
23
23
  Subcommands: `formstr-mcp login` · `formstr-mcp whoami` · `formstr-mcp accounts` ·
24
- `formstr-mcp logout` · `formstr-mcp` (run the stdio server, the default).
24
+ `formstr-mcp switch <npub>` · `formstr-mcp logout` · `formstr-mcp help` ·
25
+ `formstr-mcp` (run the stdio server, the default). Run `formstr-mcp help` (or `-h`) for
26
+ the full usage.
25
27
 
26
28
  ## Sign-in
27
29
 
@@ -39,7 +41,11 @@ offers four methods:
39
41
  Linux Secret Service via `@napi-rs/keyring`). On hosts without a keychain (e.g. headless
40
42
  Linux), set `FORMSTR_MCP_PASSPHRASE` to use an AES-256-GCM encrypted file at
41
43
  `~/.config/formstr-mcp/keystore.enc` (mode `0600`). Multiple identities are supported
42
- (`formstr-mcp accounts` lists them); select one at boot with `--account <pubkey>`.
44
+ (`formstr-mcp accounts` lists them). Change the persisted active account with
45
+ `formstr-mcp switch <npub>`, or pick one for a single boot with `--account <npub>`; the
46
+ server follows the active account when neither is given, so switching accounts just works.
47
+ Both `switch` and `--account` accept either the `npub` (as shown by `accounts`) or the hex
48
+ pubkey.
43
49
 
44
50
  **Defense in depth:** even on the encrypted-file fallback the stored key is _also_ NIP-49
45
51
  encrypted, so recovering it needs **both** the keystore **and** the unlock passphrase.
@@ -71,8 +77,13 @@ Run `formstr-mcp login` once interactively to populate the keystore, then run th
71
77
  unattended. At boot the active account is unlocked headlessly:
72
78
 
73
79
  - **ncryptsec accounts** decrypt using `FORMSTR_MCP_NCRYPTSEC_PASSPHRASE` (the passphrase
74
- you set during `login`). Required for the `run` command when the active account is local.
75
- - **NIP-46 accounts** reconnect from their stored session — no passphrase needed.
80
+ you set during `login`). On an **interactive terminal** the server instead **prompts**
81
+ for it (and re-prompts up to 3× on a typo) — so the env var is only _required_ when an
82
+ MCP host spawns the server, since then stdin is the JSON-RPC channel and there's nobody
83
+ to prompt. Each account has its own passphrase.
84
+ - **NIP-46 accounts** reconnect from their stored session — no passphrase needed. This is
85
+ the simplest setup for a host: `formstr-mcp switch <npub>` to a bunker account and the
86
+ config needs no secret at all.
76
87
 
77
88
  | Variable | Meaning |
78
89
  | ---------------------------------- | ----------------------------------------------------------------- |
@@ -81,8 +92,9 @@ unattended. At boot the active account is unlocked headlessly:
81
92
  | `FORMSTR_MCP_KEYSTORE` | force `file` or `keychain` backend (optional) |
82
93
  | `FORMSTR_MCP_CONFIG_DIR` | keystore directory (default `~/.config/formstr-mcp`) |
83
94
  | `FORMSTR_RELAYS` | comma-separated relay override (optional) |
95
+ | `FORMSTR_MCP_DEBUG` | print full stack traces on fatal errors (set to `1`) |
84
96
 
85
- CLI flags: `--relays <wss://a,wss://b>`, `--allow-writes`, `--account <pubkey>`.
97
+ CLI flags: `--relays <wss://a,wss://b>`, `--allow-writes`, `--account <npub|hex>`.
86
98
  There is no plaintext-nsec path — a raw key is never read from env, flags, or a config file.
87
99
 
88
100
  ## Forms tools
package/dist/index.js CHANGED
@@ -24405,7 +24405,26 @@ async function loginImport(deps) {
24405
24405
  }
24406
24406
  async function loginBunker(deps) {
24407
24407
  const uri = (await deps.prompt("Paste the bunker:// URI: ")).trim();
24408
- await deps.signer.loginWithBunkerUri(uri, { pool: deps.pool, perms: NIP46_PERMS });
24408
+ try {
24409
+ await deps.signer.loginWithBunkerUri(uri, { pool: deps.pool, perms: NIP46_PERMS });
24410
+ } catch (err) {
24411
+ throw new Error(describeBunkerError(uri, err));
24412
+ }
24413
+ }
24414
+ function describeBunkerError(uri, err) {
24415
+ const raw = err instanceof Error ? err.message : String(err);
24416
+ const hasSecret = /[?&]secret=/.test(uri);
24417
+ const expected = "`bunker://<pubkey>?relay=wss://\u2026&secret=<token>`";
24418
+ if (/no secret/i.test(raw) || !hasSecret) {
24419
+ return "Could not pair with this bunker URI" + (hasSecret ? "" : " \u2014 it has no `secret=` token") + `, so the remote signer rejected the connection. A pairing URI must look like ${expected}. Copy the FULL connection URI (including \`&secret=\u2026\`) from your signer app (e.g. nsec.app \u2192 Connect app), and make sure nothing was truncated \u2014 the secret is often one-time, so generate a fresh connection if needed. Or use the QR flow: \`formstr-mcp login\` \u2192 \`q\`. (signer reported: ${raw})`;
24420
+ }
24421
+ if (/invalid bunker uri/i.test(raw)) {
24422
+ return `That doesn't look like a valid bunker URI. Expected ${expected}. (parser reported: ${raw})`;
24423
+ }
24424
+ if (/at least one relay/i.test(raw)) {
24425
+ return `This bunker URI has no relays. Expected ${expected}. (reported: ${raw})`;
24426
+ }
24427
+ return `Bunker login failed: ${raw}. Check that the URI includes a \`secret=\` token and at least one \`relay=\`, that the remote signer is online, or try the QR flow (\`formstr-mcp login\` \u2192 \`q\`).`;
24409
24428
  }
24410
24429
  async function loginNostrConnect(deps, log) {
24411
24430
  const relays = deps.relays?.length ? deps.relays : DEFAULT_NOSTRCONNECT_RELAYS;
@@ -24426,6 +24445,19 @@ function whoami(signer) {
24426
24445
  function listAccounts(signer) {
24427
24446
  return signer.listAccounts();
24428
24447
  }
24448
+ function findAccount(accounts, target) {
24449
+ return accounts.find((a) => a.npub === target || a.pubkey === target) ?? null;
24450
+ }
24451
+ async function doSwitch(signer, target) {
24452
+ const match = findAccount(listAccounts(signer), target);
24453
+ if (!match) {
24454
+ throw new Error(
24455
+ `No stored account matching "${target}". Run \`formstr-mcp accounts\` to list them.`
24456
+ );
24457
+ }
24458
+ await signer.switchAccount(match.pubkey);
24459
+ return match;
24460
+ }
24429
24461
 
24430
24462
  // src/auth/kvStore.ts
24431
24463
  var import_node_crypto = require("crypto");
@@ -27366,7 +27398,7 @@ async function bootstrap(input, deps = {}) {
27366
27398
  }
27367
27399
  async function selectAccount(signer, requested) {
27368
27400
  if (requested) {
27369
- const match = signer.listAccounts().find((a) => a.pubkey === requested);
27401
+ const match = findAccount(signer.listAccounts(), requested);
27370
27402
  if (!match) {
27371
27403
  throw new Error(
27372
27404
  `No stored account for --account ${requested}. Run \`formstr-mcp accounts\` to list them.`
@@ -27383,18 +27415,7 @@ async function selectAccount(signer, requested) {
27383
27415
  }
27384
27416
  async function unlock(signer, account, deps) {
27385
27417
  if (account.method === "ncryptsec") {
27386
- if (!account.ncryptsec) {
27387
- throw new Error(
27388
- "Stored ncryptsec account is missing its encrypted key. Run `formstr-mcp login`."
27389
- );
27390
- }
27391
- const passphrase = deps.passphrase ?? process.env.FORMSTR_MCP_NCRYPTSEC_PASSPHRASE ?? (deps.promptPassphrase ? await deps.promptPassphrase("Passphrase to unlock your key: ") : void 0);
27392
- if (!passphrase) {
27393
- throw new Error(
27394
- "Set FORMSTR_MCP_NCRYPTSEC_PASSPHRASE to unlock the ncryptsec account at boot (or run in an interactive terminal to be prompted)."
27395
- );
27396
- }
27397
- await signer.loginWithNcryptsec(account.ncryptsec, passphrase);
27418
+ await unlockNcryptsec(signer, account, deps);
27398
27419
  return;
27399
27420
  }
27400
27421
  if (account.method === "nip46") {
@@ -27414,31 +27435,108 @@ async function unlock(signer, account, deps) {
27414
27435
  `Account method "${account.method}" cannot be unlocked headlessly \u2014 use ncryptsec or nip46.`
27415
27436
  );
27416
27437
  }
27438
+ var MAX_PASSPHRASE_ATTEMPTS = 3;
27439
+ async function unlockNcryptsec(signer, account, deps) {
27440
+ if (!account.ncryptsec) {
27441
+ throw new Error(
27442
+ `Account ${account.npub} is stored as an ncryptsec but its encrypted key is missing. Re-add it with \`formstr-mcp login\`.`
27443
+ );
27444
+ }
27445
+ const ncryptsec = account.ncryptsec;
27446
+ const tryUnlock = async (passphrase) => {
27447
+ try {
27448
+ await signer.loginWithNcryptsec(ncryptsec, passphrase);
27449
+ return true;
27450
+ } catch {
27451
+ return false;
27452
+ }
27453
+ };
27454
+ const configured = deps.passphrase ?? process.env.FORMSTR_MCP_NCRYPTSEC_PASSPHRASE;
27455
+ if (configured) {
27456
+ if (await tryUnlock(configured)) return;
27457
+ if (!deps.promptPassphrase) {
27458
+ throw new Error(
27459
+ `The configured passphrase did not unlock account ${account.npub} \u2014 it is most likely the wrong passphrase for this account (each account has its own). Set FORMSTR_MCP_NCRYPTSEC_PASSPHRASE to the passphrase that matches this account, or switch to another with \`formstr-mcp switch <npub>\` (a nip46/bunker account needs no passphrase).`
27460
+ );
27461
+ }
27462
+ console.error(
27463
+ `formstr-mcp: the configured passphrase didn't unlock ${account.npub} \u2014 enter it manually.`
27464
+ );
27465
+ } else if (!deps.promptPassphrase) {
27466
+ throw new Error(
27467
+ `Cannot unlock account ${account.npub} (ncryptsec): no passphrase is available. FORMSTR_MCP_NCRYPTSEC_PASSPHRASE is not set and this process has no interactive terminal to prompt on \u2014 an MCP host runs the server with stdin wired to the JSON-RPC channel. Fix: add FORMSTR_MCP_NCRYPTSEC_PASSPHRASE to your MCP server config's \`env\`, or switch to a nip46/bunker account with \`formstr-mcp switch <npub>\` (resumes with no passphrase).`
27468
+ );
27469
+ }
27470
+ const promptPassphrase = deps.promptPassphrase;
27471
+ for (let attempt = 1; attempt <= MAX_PASSPHRASE_ATTEMPTS; attempt++) {
27472
+ const passphrase = await promptPassphrase(`Passphrase to unlock ${account.npub}: `);
27473
+ if (await tryUnlock(passphrase)) return;
27474
+ const left = MAX_PASSPHRASE_ATTEMPTS - attempt;
27475
+ if (left > 0) console.error(`formstr-mcp: incorrect passphrase, ${left} attempt(s) left.`);
27476
+ }
27477
+ throw new Error(
27478
+ `Incorrect passphrase for account ${account.npub} after ${MAX_PASSPHRASE_ATTEMPTS} attempts.`
27479
+ );
27480
+ }
27417
27481
 
27418
27482
  // src/cli.ts
27419
- var SUBCOMMANDS = /* @__PURE__ */ new Set(["login", "logout", "whoami", "accounts"]);
27483
+ var SUBCOMMANDS = /* @__PURE__ */ new Set(["login", "logout", "whoami", "accounts", "switch", "help"]);
27420
27484
  function parseCli(argv) {
27421
27485
  const rest = [...argv];
27486
+ if (rest.includes("-h") || rest.includes("--help")) {
27487
+ return { command: "help", allowWrites: false };
27488
+ }
27422
27489
  let command = "run";
27423
27490
  if (rest[0] && SUBCOMMANDS.has(rest[0])) {
27424
27491
  command = rest.shift();
27425
27492
  }
27426
27493
  let relays;
27427
27494
  let account;
27495
+ let target;
27428
27496
  let allowWrites = false;
27429
27497
  for (let i4 = 0; i4 < rest.length; i4++) {
27430
27498
  const arg = rest[i4];
27431
27499
  if (arg === "--relays") relays = splitRelays(rest[++i4]);
27432
27500
  else if (arg === "--allow-writes") allowWrites = true;
27433
27501
  else if (arg === "--account") account = rest[++i4];
27502
+ else if (!arg.startsWith("-") && target === void 0) target = arg;
27434
27503
  }
27435
- return { command, relays, allowWrites, account };
27504
+ return { command, relays, allowWrites, account, target };
27505
+ }
27506
+ function formatFatal(err, debug = false) {
27507
+ if (err instanceof Error) return debug && err.stack ? err.stack : err.message;
27508
+ return String(err);
27436
27509
  }
27437
27510
  function splitRelays(value) {
27438
27511
  if (!value) return void 0;
27439
27512
  const parts = value.split(",").map((s) => s.trim()).filter(Boolean);
27440
27513
  return parts.length ? parts : void 0;
27441
27514
  }
27515
+ function helpText() {
27516
+ return [
27517
+ "formstr-mcp \u2014 MCP server for the Formstr super-app (Nostr forms & more).",
27518
+ "",
27519
+ "Usage: formstr-mcp [command] [flags]",
27520
+ "",
27521
+ "Commands:",
27522
+ " run Run the stdio MCP server (default when no command is given).",
27523
+ " login Sign in (create / import / bunker URI / QR) and store the key.",
27524
+ " logout [npub] Remove a stored account (defaults to the active one).",
27525
+ " whoami Print the active account.",
27526
+ " accounts List stored accounts ('*' marks the active one).",
27527
+ " switch <npub> Set the active account (accepts an npub or hex pubkey).",
27528
+ " help Show this help (also -h, --help).",
27529
+ "",
27530
+ "Flags:",
27531
+ " --allow-writes Enable gated write tools (update / delete / share / submit).",
27532
+ " --account <npub|hex> Boot a specific account instead of the active one.",
27533
+ " --relays <a,b,\u2026> Override relays (comma-separated).",
27534
+ "",
27535
+ "Env:",
27536
+ " FORMSTR_MCP_NCRYPTSEC_PASSPHRASE Unlock the active ncryptsec account at boot.",
27537
+ " FORMSTR_MCP_PASSPHRASE Encrypt the keystore file (keychain-less hosts)."
27538
+ ].join("\n");
27539
+ }
27442
27540
 
27443
27541
  // src/config.ts
27444
27542
  function resolveConfig(cli, env) {
@@ -32063,14 +32161,22 @@ function buildSpecRows(formId, name, fields, settings) {
32063
32161
  for (const field of fields) rows.push(buildFieldTag(field));
32064
32162
  return rows;
32065
32163
  }
32066
- function buildEncryptedOuterTags(formId, name, relays, settings) {
32067
- const tags = [["d", formId], ["name", name], ...relays.map((r) => ["relay", r])];
32164
+ function buildParticipantTags(settings) {
32165
+ const tags = [];
32068
32166
  const allowed = settings?.allowedResponders ?? [];
32069
32167
  const pTags = /* @__PURE__ */ new Set([...allowed, ...settings?.collaborators ?? []]);
32070
32168
  for (const pk of allowed) tags.push(["allowed", pk]);
32071
32169
  for (const pk of pTags) tags.push(["p", pk]);
32072
32170
  return tags;
32073
32171
  }
32172
+ function buildEncryptedOuterTags(formId, name, relays, settings) {
32173
+ return [
32174
+ ["d", formId],
32175
+ ["name", name],
32176
+ ...relays.map((r) => ["relay", r]),
32177
+ ...buildParticipantTags(settings)
32178
+ ];
32179
+ }
32074
32180
  async function createForm(params) {
32075
32181
  await signerManager.getSigner();
32076
32182
  const formId = crypto.randomUUID().slice(0, 8);
@@ -32096,7 +32202,12 @@ async function createForm(params) {
32096
32202
  await appendToMyFormsList(signingPubkey, formId, relays[0] ?? "", signingKeyHex, viewKeyHex);
32097
32203
  return { formId, pubkey: signingPubkey, signingKey: signingKeyHex, viewKey: viewKeyHex };
32098
32204
  }
32099
- const tags = [...specRows, ["t", "public"], ...relays.map((r) => ["relay", r])];
32205
+ const tags = [
32206
+ ...specRows,
32207
+ ["t", "public"],
32208
+ ...relays.map((r) => ["relay", r]),
32209
+ ...buildParticipantTags(params.settings)
32210
+ ];
32100
32211
  const event = {
32101
32212
  kind: FORM_KINDS.template,
32102
32213
  created_at: Math.floor(Date.now() / 1e3),
@@ -32416,7 +32527,12 @@ async function updateForm(params) {
32416
32527
  await nostrRuntime.publish(relays, finalizeEvent(event2, signingKeyBytes));
32417
32528
  return;
32418
32529
  }
32419
- const tags = [...specRows, ["t", "public"], ...relays.map((r) => ["relay", r])];
32530
+ const tags = [
32531
+ ...specRows,
32532
+ ["t", "public"],
32533
+ ...relays.map((r) => ["relay", r]),
32534
+ ...buildParticipantTags(settings)
32535
+ ];
32420
32536
  const event = {
32421
32537
  kind: FORM_KINDS.template,
32422
32538
  created_at: Math.floor(Date.now() / 1e3),
@@ -32548,6 +32664,7 @@ function parseResponseEvent(event) {
32548
32664
  var service_exports2 = {};
32549
32665
  __export(service_exports2, {
32550
32666
  addEventToCalendarList: () => addEventToCalendarList,
32667
+ createCalendarEvent: () => createCalendarEvent,
32551
32668
  createCalendarList: () => createCalendarList,
32552
32669
  deleteCalendarEvent: () => deleteCalendarEvent,
32553
32670
  deleteCalendarList: () => deleteCalendarList,
@@ -32978,6 +33095,33 @@ async function publishPrivateCalendarEvent(draft, calendarId) {
32978
33095
  event: signed
32979
33096
  };
32980
33097
  }
33098
+ async function createCalendarEvent(draft, opts = {}) {
33099
+ let calendars = opts.calendars ?? null;
33100
+ const loadCalendars = async () => calendars ?? (calendars = await fetchCalendarLists());
33101
+ let targetCalendarId = draft.calendarId;
33102
+ let targetCalendar;
33103
+ if (targetCalendarId) {
33104
+ targetCalendar = (await loadCalendars()).find((c) => c.id === targetCalendarId);
33105
+ } else if (draft.isPrivate) {
33106
+ targetCalendar = (await loadCalendars())[0];
33107
+ if (!targetCalendar) targetCalendar = await createCalendarList("My Calendar", "#334155");
33108
+ targetCalendarId = targetCalendar.id;
33109
+ }
33110
+ const event = draft.isPrivate ? await publishPrivateCalendarEvent(
33111
+ { ...draft, calendarId: targetCalendarId },
33112
+ targetCalendarId ?? "default"
33113
+ ) : await publishPublicCalendarEvent(draft);
33114
+ let calendar;
33115
+ if (targetCalendar) {
33116
+ const ref = [
33117
+ `${event.kind}:${event.user}:${event.id}`,
33118
+ event.relayHint ?? "",
33119
+ event.viewKey ?? ""
33120
+ ];
33121
+ calendar = await addEventToCalendarList(targetCalendar, ref);
33122
+ }
33123
+ return { event, calendar };
33124
+ }
32981
33125
  async function fetchRelayListsForPubkeys(pubkeys) {
32982
33126
  const result = /* @__PURE__ */ new Map();
32983
33127
  if (pubkeys.length === 0) return result;
@@ -34104,6 +34248,7 @@ async function fetchDocMetadata(address) {
34104
34248
  async function saveDocMetadata(address, patch) {
34105
34249
  const existing = await fetchDocMetadata(address) ?? {};
34106
34250
  const merged = { ...existing, ...patch };
34251
+ if (!Array.isArray(merged.tags)) merged.tags = [];
34107
34252
  const signer = await signerManager.getSigner();
34108
34253
  const content = await nip44SelfEncrypt(signer, JSON.stringify(merged));
34109
34254
  const signed = await signer.signEvent({
@@ -34288,7 +34433,9 @@ async function uploadFile(params) {
34288
34433
  name: params.file.name,
34289
34434
  hash: result.sha256,
34290
34435
  size: params.file.size,
34291
- type: params.file.type,
34436
+ // Browsers report "" for unknown MIME types; coalesce exactly as upstream
34437
+ // formstr-drive's uploadPreparedFile does so the on-wire metadata matches.
34438
+ type: params.file.type || "application/octet-stream",
34292
34439
  folder: params.folder ?? "/",
34293
34440
  uploadedAt: Date.now(),
34294
34441
  server,
@@ -34504,8 +34651,9 @@ async function createPoll(draft) {
34504
34651
  for (const tag of draft.hashtags ?? []) {
34505
34652
  tags.push(["t", tag]);
34506
34653
  }
34507
- if (draft.endsAt) {
34508
- tags.push(["endsAt", String(Math.floor(draft.endsAt.getTime() / 1e3))]);
34654
+ const endsAtSec = draft.endsAt && Number.isFinite(draft.endsAt.getTime()) ? Math.floor(draft.endsAt.getTime() / 1e3) : void 0;
34655
+ if (endsAtSec !== void 0) {
34656
+ tags.push(["endsAt", String(endsAtSec)]);
34509
34657
  }
34510
34658
  const event = {
34511
34659
  kind: POLLS_KINDS.poll,
@@ -34522,7 +34670,7 @@ async function createPoll(draft) {
34522
34670
  pollType: draft.pollType,
34523
34671
  pubkey,
34524
34672
  createdAt: signed.created_at,
34525
- endsAt: draft.endsAt ? Math.floor(draft.endsAt.getTime() / 1e3) : void 0,
34673
+ endsAt: endsAtSec,
34526
34674
  relays,
34527
34675
  hashtags: draft.hashtags ?? [],
34528
34676
  event: signed
@@ -34756,7 +34904,7 @@ function buildCalendarTools() {
34756
34904
  server.registerTool(
34757
34905
  "create_calendar_event",
34758
34906
  {
34759
- description: "Schedule a calendar event. start/end are ISO 8601. Set isPrivate:true for an encrypted event; participants receive NIP-59 invitations.",
34907
+ 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 receive NIP-59 invitations.",
34760
34908
  inputSchema: {
34761
34909
  title: external_exports.string(),
34762
34910
  description: external_exports.string().optional(),
@@ -34764,6 +34912,7 @@ function buildCalendarTools() {
34764
34912
  end: external_exports.string().optional(),
34765
34913
  location: external_exports.string().optional(),
34766
34914
  isPrivate: external_exports.boolean().optional(),
34915
+ calendarId: external_exports.string().optional(),
34767
34916
  participants: external_exports.array(external_exports.string()).optional(),
34768
34917
  rrule: external_exports.string().optional(),
34769
34918
  startTzid: external_exports.string().optional(),
@@ -34771,6 +34920,22 @@ function buildCalendarTools() {
34771
34920
  }
34772
34921
  },
34773
34922
  async (args) => {
34923
+ const isPrivate = args.isPrivate ?? true;
34924
+ const calendars = await service_exports2.fetchCalendarLists();
34925
+ if (isPrivate && !args.calendarId && calendars.length > 0) {
34926
+ const choices = calendars.map((c) => `${c.title} (${c.id})`).join("; ");
34927
+ return fail(
34928
+ `Which calendar should "${args.title}" go in? Re-run create_calendar_event with calendarId set to one of: ${choices}.`,
34929
+ "CALENDAR_REQUIRED"
34930
+ );
34931
+ }
34932
+ if (args.calendarId && !calendars.some((c) => c.id === args.calendarId)) {
34933
+ const available = calendars.map((c) => c.id).join(", ") || "(none)";
34934
+ return fail(
34935
+ `No calendar found for id ${args.calendarId}. Available: ${available}.`,
34936
+ "NOT_FOUND"
34937
+ );
34938
+ }
34774
34939
  const begin = new Date(args.start);
34775
34940
  const end = args.end ? new Date(args.end) : new Date(begin.getTime() + 36e5);
34776
34941
  const draft = {
@@ -34780,18 +34945,23 @@ function buildCalendarTools() {
34780
34945
  end,
34781
34946
  location: args.location,
34782
34947
  participants: args.participants,
34783
- isPrivate: Boolean(args.isPrivate),
34948
+ isPrivate,
34949
+ calendarId: args.calendarId,
34784
34950
  rrule: args.rrule,
34785
34951
  startTzid: args.startTzid,
34786
34952
  registrationFormRef: args.registrationFormRef
34787
34953
  };
34788
- const event = args.isPrivate ? await service_exports2.publishPrivateCalendarEvent(draft, "default") : await service_exports2.publishPublicCalendarEvent(draft);
34954
+ const { event, calendar: list } = await service_exports2.createCalendarEvent(draft, { calendars });
34789
34955
  const coordinate = `${event.kind}:${event.user}:${event.id}`;
34790
- return ok(`Created ${args.isPrivate ? "private" : "public"} event "${args.title}".`, {
34791
- id: event.id,
34792
- eventId: event.eventId,
34793
- coordinate
34794
- });
34956
+ return ok(
34957
+ `Created ${isPrivate ? "private" : "public"} event "${args.title}"${list ? ` in calendar "${list.title}"` : ""}.`,
34958
+ {
34959
+ id: event.id,
34960
+ eventId: event.eventId,
34961
+ coordinate,
34962
+ calendarId: list?.id
34963
+ }
34964
+ );
34795
34965
  }
34796
34966
  );
34797
34967
  server.registerTool(
@@ -46448,6 +46618,19 @@ async function main() {
46448
46618
  }
46449
46619
  return cli.command;
46450
46620
  }
46621
+ case "switch": {
46622
+ if (!cli.target) {
46623
+ throw new Error(
46624
+ "Usage: formstr-mcp switch <npub|hex>. Run `formstr-mcp accounts` to list them."
46625
+ );
46626
+ }
46627
+ const account = await doSwitch(await buildMcpSigner(), cli.target);
46628
+ console.error(`formstr-mcp: active account is now ${account.npub} (${account.method}).`);
46629
+ return cli.command;
46630
+ }
46631
+ case "help":
46632
+ console.error(helpText());
46633
+ return cli.command;
46451
46634
  case "run":
46452
46635
  default:
46453
46636
  await runServer(cli);
@@ -46457,7 +46640,9 @@ async function main() {
46457
46640
  main().then((command) => {
46458
46641
  if (command !== "run") process.exit(0);
46459
46642
  }).catch((err) => {
46460
- console.error("formstr-mcp: fatal:", err instanceof Error ? err.message : err);
46643
+ const debug = !!(process.env.FORMSTR_MCP_DEBUG || process.env.DEBUG);
46644
+ console.error("formstr-mcp: fatal:", formatFatal(err, debug));
46645
+ if (!debug) console.error("formstr-mcp: (set FORMSTR_MCP_DEBUG=1 for a full stack trace)");
46461
46646
  process.exit(1);
46462
46647
  });
46463
46648
  /*! Bundled license information: