@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 +17 -5
- package/dist/index.js +218 -33
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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)
|
|
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`).
|
|
75
|
-
|
|
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 <
|
|
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
|
-
|
|
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()
|
|
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
|
-
|
|
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
|
|
32067
|
-
const tags = [
|
|
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 = [
|
|
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 = [
|
|
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
|
-
|
|
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
|
-
|
|
34508
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
34954
|
+
const { event, calendar: list } = await service_exports2.createCalendarEvent(draft, { calendars });
|
|
34789
34955
|
const coordinate = `${event.kind}:${event.user}:${event.id}`;
|
|
34790
|
-
return ok(
|
|
34791
|
-
|
|
34792
|
-
|
|
34793
|
-
|
|
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
|
-
|
|
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:
|