@formstr/mcp 0.3.2 → 0.4.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/README.md +97 -3
- package/dist/index.js +154 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,9 +21,27 @@ npx -y @formstr/mcp login
|
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
Subcommands: `formstr-mcp login` · `formstr-mcp whoami` · `formstr-mcp accounts` ·
|
|
24
|
-
`formstr-mcp switch <npub>` · `formstr-mcp logout` · `formstr-mcp
|
|
25
|
-
`formstr-mcp` (run the stdio server, the default). Run
|
|
26
|
-
the full usage.
|
|
24
|
+
`formstr-mcp switch <npub>` · `formstr-mcp logout` · `formstr-mcp version` ·
|
|
25
|
+
`formstr-mcp help` · `formstr-mcp` (run the stdio server, the default). Run
|
|
26
|
+
`formstr-mcp help` (or `-h`) for the full usage.
|
|
27
|
+
|
|
28
|
+
## Version & updates
|
|
29
|
+
|
|
30
|
+
`formstr-mcp version` (or `-v` / `--version`) prints the installed version and checks the
|
|
31
|
+
npm registry for a newer release:
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
$ formstr-mcp version
|
|
35
|
+
@formstr/mcp 0.4.0
|
|
36
|
+
Update available: 0.5.0 (you have 0.4.0).
|
|
37
|
+
Upgrade: npm install -g @formstr/mcp@latest
|
|
38
|
+
Or just re-run via: npx -y @formstr/mcp@latest
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The update check is best-effort — if you're offline or the registry is unreachable it
|
|
42
|
+
prints the installed version and a note, never an error. If you run the server via
|
|
43
|
+
`npx -y @formstr/mcp` you already get the latest published version on each launch; pin a
|
|
44
|
+
version (`@formstr/mcp@0.4.0`) in your host config if you'd rather control upgrades.
|
|
27
45
|
|
|
28
46
|
## Sign-in
|
|
29
47
|
|
|
@@ -71,6 +89,82 @@ After `login`, no key belongs in the config:
|
|
|
71
89
|
Add `"--allow-writes"` to `args` to enable the gated (destructive/outward) tools, and
|
|
72
90
|
`"--relays", "wss://a,wss://b"` to override relays.
|
|
73
91
|
|
|
92
|
+
### Passing the ncryptsec passphrase
|
|
93
|
+
|
|
94
|
+
If your active account is an `ncryptsec` key (Create / Import login), the server needs its
|
|
95
|
+
passphrase to unlock at boot. An MCP host spawns the server with stdin wired to the
|
|
96
|
+
JSON-RPC channel, so it **can't prompt** — supply the passphrase through an `"env"` block in
|
|
97
|
+
the server entry of your MCP config (`mcp_config.json`, `claude_desktop_config.json`, Cursor's
|
|
98
|
+
`~/.cursor/mcp.json`, etc.):
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"mcpServers": {
|
|
103
|
+
"formstr": {
|
|
104
|
+
"command": "npx",
|
|
105
|
+
"args": ["-y", "@formstr/mcp"],
|
|
106
|
+
"env": {
|
|
107
|
+
"FORMSTR_MCP_NCRYPTSEC_PASSPHRASE": "your-passphrase-here"
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The host hands that value to the server as an environment variable at startup — it never
|
|
115
|
+
enters the chat transcript. Each account has its own passphrase, so this unlocks whichever
|
|
116
|
+
one is **active** (set it with `formstr-mcp switch <npub>`).
|
|
117
|
+
|
|
118
|
+
> **Tip:** prefer not to keep a passphrase in a config file? Use a **NIP-46 (bunker)**
|
|
119
|
+
> account instead — it reconnects from its stored session and needs **no** passphrase, so
|
|
120
|
+
> the config can stay secret-free. Run `formstr-mcp switch <npub>` to a bunker account.
|
|
121
|
+
|
|
122
|
+
## Using with Ollama (local models)
|
|
123
|
+
|
|
124
|
+
Ollama isn't an MCP client — it just runs the model. To drive this server with a **local**
|
|
125
|
+
model you need an MCP **host** that uses Ollama as its backend. A good, actively-maintained
|
|
126
|
+
option is [**Goose**](https://block.github.io/goose/) (open-source agent by Block; CLI +
|
|
127
|
+
desktop), which has both first-class Ollama support and native stdio MCP extensions.
|
|
128
|
+
|
|
129
|
+
**1. Pull a tool-calling-capable model and start Ollama:**
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
ollama pull qwen2.5 # llama3.1 / 3.2, mistral, … also work — the model MUST support tools
|
|
133
|
+
ollama serve # serves the API on http://localhost:11434
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
A model **without** tool/function-calling support can chat but can't invoke this server's
|
|
137
|
+
tools (`list_forms`, `create_form`, …), so don't pick one of those.
|
|
138
|
+
|
|
139
|
+
**2. Sign in to formstr once** (stores your key in the keystore):
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
npx -y @formstr/mcp login
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**3. Point Goose at Ollama** — `goose configure` → _Configure Providers_ → **Ollama**, then
|
|
146
|
+
enter the host (`http://localhost:11434`) and pick your model.
|
|
147
|
+
|
|
148
|
+
**4. Add this server as an extension** — `goose configure` → _Add Extension_ → **Command-line
|
|
149
|
+
Extension**, then answer the prompts:
|
|
150
|
+
|
|
151
|
+
| Prompt | Value |
|
|
152
|
+
| --------------------- | ------------------------------------------------------------- |
|
|
153
|
+
| Name | `formstr` |
|
|
154
|
+
| Command | `npx -y @formstr/mcp` (add `--allow-writes` to enable writes) |
|
|
155
|
+
| Timeout (secs) | `300` |
|
|
156
|
+
| Environment variables | `FORMSTR_MCP_NCRYPTSEC_PASSPHRASE` = _your passphrase_ |
|
|
157
|
+
|
|
158
|
+
Goose passes that env var to the server when it spawns it (so it unlocks headlessly), and
|
|
159
|
+
saves the extension to `~/.config/goose/config.yaml`. Then just run `goose` (or `goose
|
|
160
|
+
session`) and ask it to work with your forms.
|
|
161
|
+
|
|
162
|
+
> **Bunker accounts need no passphrase** — skip the env-variable step and `formstr-mcp switch
|
|
163
|
+
<npub>` to a NIP-46 account (see the tip above); the extension then stores no secret.
|
|
164
|
+
|
|
165
|
+
The same approach works with any other Ollama-backed MCP host: point it at
|
|
166
|
+
`npx -y @formstr/mcp` and supply the passphrase through that host's env mechanism.
|
|
167
|
+
|
|
74
168
|
## Headless / unattended
|
|
75
169
|
|
|
76
170
|
Run `formstr-mcp login` once interactively to populate the keystore, then run the server
|
package/dist/index.js
CHANGED
|
@@ -27493,12 +27493,23 @@ async function unlockNcryptsec(signer, account, deps) {
|
|
|
27493
27493
|
}
|
|
27494
27494
|
|
|
27495
27495
|
// src/cli.ts
|
|
27496
|
-
var SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
27496
|
+
var SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
27497
|
+
"login",
|
|
27498
|
+
"logout",
|
|
27499
|
+
"whoami",
|
|
27500
|
+
"accounts",
|
|
27501
|
+
"switch",
|
|
27502
|
+
"help",
|
|
27503
|
+
"version"
|
|
27504
|
+
]);
|
|
27497
27505
|
function parseCli(argv) {
|
|
27498
27506
|
const rest = [...argv];
|
|
27499
27507
|
if (rest.includes("-h") || rest.includes("--help")) {
|
|
27500
27508
|
return { command: "help", allowWrites: false };
|
|
27501
27509
|
}
|
|
27510
|
+
if (rest.includes("-v") || rest.includes("--version")) {
|
|
27511
|
+
return { command: "version", allowWrites: false };
|
|
27512
|
+
}
|
|
27502
27513
|
let command = "run";
|
|
27503
27514
|
if (rest[0] && SUBCOMMANDS.has(rest[0])) {
|
|
27504
27515
|
command = rest.shift();
|
|
@@ -27539,6 +27550,7 @@ function helpText() {
|
|
|
27539
27550
|
" accounts List stored accounts ('*' marks the active one).",
|
|
27540
27551
|
" switch <npub> Set the active account (accepts an npub or hex pubkey).",
|
|
27541
27552
|
" help Show this help (also -h, --help).",
|
|
27553
|
+
" version Print the installed version and check for an update (also -v, --version).",
|
|
27542
27554
|
"",
|
|
27543
27555
|
"Flags:",
|
|
27544
27556
|
" --allow-writes Enable gated write tools (update / delete / share / submit).",
|
|
@@ -27547,7 +27559,28 @@ function helpText() {
|
|
|
27547
27559
|
"",
|
|
27548
27560
|
"Env:",
|
|
27549
27561
|
" FORMSTR_MCP_NCRYPTSEC_PASSPHRASE Unlock the active ncryptsec account at boot.",
|
|
27550
|
-
" FORMSTR_MCP_PASSPHRASE Encrypt the keystore file (keychain-less hosts)."
|
|
27562
|
+
" FORMSTR_MCP_PASSPHRASE Encrypt the keystore file (keychain-less hosts).",
|
|
27563
|
+
"",
|
|
27564
|
+
"Setting the passphrase in your MCP host config:",
|
|
27565
|
+
" When an MCP host (Claude Desktop/Code, Cursor, \u2026) spawns the server, stdin is the",
|
|
27566
|
+
" JSON-RPC channel, so it can't prompt for your ncryptsec passphrase. Pass it via an",
|
|
27567
|
+
' "env" block in the server entry of your MCP config (e.g. mcp_config.json /',
|
|
27568
|
+
" claude_desktop_config.json) so the host hands it to the server at startup:",
|
|
27569
|
+
"",
|
|
27570
|
+
" {",
|
|
27571
|
+
' "mcpServers": {',
|
|
27572
|
+
' "formstr": {',
|
|
27573
|
+
' "command": "npx",',
|
|
27574
|
+
' "args": ["-y", "@formstr/mcp"],',
|
|
27575
|
+
' "env": {',
|
|
27576
|
+
' "FORMSTR_MCP_NCRYPTSEC_PASSPHRASE": "your-passphrase-here"',
|
|
27577
|
+
" }",
|
|
27578
|
+
" }",
|
|
27579
|
+
" }",
|
|
27580
|
+
" }",
|
|
27581
|
+
"",
|
|
27582
|
+
" 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.'
|
|
27551
27584
|
].join("\n");
|
|
27552
27585
|
}
|
|
27553
27586
|
|
|
@@ -32431,19 +32464,27 @@ async function fetchMyForms() {
|
|
|
32431
32464
|
return summary;
|
|
32432
32465
|
}).filter((s) => s !== null);
|
|
32433
32466
|
}
|
|
32467
|
+
var MY_FORMS_LIST_READ_ATTEMPTS = 3;
|
|
32468
|
+
async function readMyFormsEntriesForWrite(signer, userPubkey, relays) {
|
|
32469
|
+
let listEvent = null;
|
|
32470
|
+
for (let attempt = 0; attempt < MY_FORMS_LIST_READ_ATTEMPTS; attempt++) {
|
|
32471
|
+
listEvent = await fetchLatestMyFormsEvent(relays, userPubkey);
|
|
32472
|
+
if (listEvent?.content) break;
|
|
32473
|
+
}
|
|
32474
|
+
if (!listEvent?.content) return [];
|
|
32475
|
+
try {
|
|
32476
|
+
return await decryptListEntries(signer, userPubkey, listEvent.content);
|
|
32477
|
+
} catch (err) {
|
|
32478
|
+
throw new Error(
|
|
32479
|
+
`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)})`
|
|
32480
|
+
);
|
|
32481
|
+
}
|
|
32482
|
+
}
|
|
32434
32483
|
async function appendToMyFormsList(formPubkey, formId, relay, signingKeyHex, viewKeyHex) {
|
|
32435
32484
|
const signer = await signerManager.getSigner();
|
|
32436
32485
|
const userPubkey = await signer.getPublicKey();
|
|
32437
32486
|
const relays = relayManager.getRelaysForModule("forms");
|
|
32438
|
-
|
|
32439
|
-
let entries = [];
|
|
32440
|
-
if (existing?.content) {
|
|
32441
|
-
try {
|
|
32442
|
-
entries = await decryptListEntries(signer, userPubkey, existing.content);
|
|
32443
|
-
} catch {
|
|
32444
|
-
entries = [];
|
|
32445
|
-
}
|
|
32446
|
-
}
|
|
32487
|
+
let entries = await readMyFormsEntriesForWrite(signer, userPubkey, relays);
|
|
32447
32488
|
entries = entries.map(
|
|
32448
32489
|
(e) => e[0] === "f" && e.length < 4 ? ["f", e[1] ?? "", e[2] ?? "", ""] : e
|
|
32449
32490
|
);
|
|
@@ -32627,9 +32668,13 @@ async function fetchFormSummaryFromRef(pubkey, formId) {
|
|
|
32627
32668
|
};
|
|
32628
32669
|
}
|
|
32629
32670
|
async function importForm(summary) {
|
|
32630
|
-
|
|
32631
|
-
|
|
32632
|
-
|
|
32671
|
+
await appendToMyFormsList(
|
|
32672
|
+
summary.pubkey,
|
|
32673
|
+
summary.id,
|
|
32674
|
+
summary.relay ?? "",
|
|
32675
|
+
summary.signingKey,
|
|
32676
|
+
summary.viewKey
|
|
32677
|
+
);
|
|
32633
32678
|
}
|
|
32634
32679
|
function parseFormEvent(event, decryptedRows) {
|
|
32635
32680
|
const merged = decryptedRows ? [...event.tags, ...decryptedRows] : event.tags;
|
|
@@ -46563,6 +46608,95 @@ async function startStdio(ctx) {
|
|
|
46563
46608
|
await server.connect(transport);
|
|
46564
46609
|
}
|
|
46565
46610
|
|
|
46611
|
+
// src/version.ts
|
|
46612
|
+
var import_node_fs2 = require("fs");
|
|
46613
|
+
var import_node_path2 = require("path");
|
|
46614
|
+
var PACKAGE_NAME = "@formstr/mcp";
|
|
46615
|
+
function readInstalledVersion() {
|
|
46616
|
+
try {
|
|
46617
|
+
const pkg = JSON.parse((0, import_node_fs2.readFileSync)((0, import_node_path2.join)(__dirname, "..", "package.json"), "utf8"));
|
|
46618
|
+
return pkg.version ?? "0.0.0";
|
|
46619
|
+
} catch {
|
|
46620
|
+
return "0.0.0";
|
|
46621
|
+
}
|
|
46622
|
+
}
|
|
46623
|
+
function parseSemver(version2) {
|
|
46624
|
+
const cleaned = version2.trim().replace(/^v/i, "").split("+")[0];
|
|
46625
|
+
const [core, pre] = cleaned.split("-", 2);
|
|
46626
|
+
const release = core.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
46627
|
+
while (release.length < 3) release.push(0);
|
|
46628
|
+
return { release, prerelease: pre ? pre.split(".") : [] };
|
|
46629
|
+
}
|
|
46630
|
+
function compareIdentifiers(a, b) {
|
|
46631
|
+
const na = /^\d+$/.test(a);
|
|
46632
|
+
const nb = /^\d+$/.test(b);
|
|
46633
|
+
if (na && nb) return Number(a) - Number(b) < 0 ? -1 : Number(a) === Number(b) ? 0 : 1;
|
|
46634
|
+
if (na) return -1;
|
|
46635
|
+
if (nb) return 1;
|
|
46636
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
46637
|
+
}
|
|
46638
|
+
function compareVersions(a, b) {
|
|
46639
|
+
const va = parseSemver(a);
|
|
46640
|
+
const vb = parseSemver(b);
|
|
46641
|
+
for (let i4 = 0; i4 < 3; i4++) {
|
|
46642
|
+
if (va.release[i4] !== vb.release[i4]) return va.release[i4] < vb.release[i4] ? -1 : 1;
|
|
46643
|
+
}
|
|
46644
|
+
if (va.prerelease.length === 0 && vb.prerelease.length === 0) return 0;
|
|
46645
|
+
if (va.prerelease.length === 0) return 1;
|
|
46646
|
+
if (vb.prerelease.length === 0) return -1;
|
|
46647
|
+
const len = Math.max(va.prerelease.length, vb.prerelease.length);
|
|
46648
|
+
for (let i4 = 0; i4 < len; i4++) {
|
|
46649
|
+
const ia = va.prerelease[i4];
|
|
46650
|
+
const ib = vb.prerelease[i4];
|
|
46651
|
+
if (ia === void 0) return -1;
|
|
46652
|
+
if (ib === void 0) return 1;
|
|
46653
|
+
const c = compareIdentifiers(ia, ib);
|
|
46654
|
+
if (c !== 0) return c < 0 ? -1 : 1;
|
|
46655
|
+
}
|
|
46656
|
+
return 0;
|
|
46657
|
+
}
|
|
46658
|
+
async function fetchLatestVersion(pkgName, opts = {}) {
|
|
46659
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
46660
|
+
const timeoutMs = opts.timeoutMs ?? 3e3;
|
|
46661
|
+
const controller = new AbortController();
|
|
46662
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
46663
|
+
try {
|
|
46664
|
+
const res = await fetchImpl(`https://registry.npmjs.org/${pkgName}/latest`, {
|
|
46665
|
+
signal: controller.signal,
|
|
46666
|
+
// The abbreviated metadata document is far smaller than the full packument.
|
|
46667
|
+
headers: { accept: "application/vnd.npm.install-v1+json" }
|
|
46668
|
+
});
|
|
46669
|
+
if (!res.ok) return null;
|
|
46670
|
+
const body = await res.json();
|
|
46671
|
+
return typeof body.version === "string" ? body.version : null;
|
|
46672
|
+
} catch {
|
|
46673
|
+
return null;
|
|
46674
|
+
} finally {
|
|
46675
|
+
clearTimeout(timer);
|
|
46676
|
+
}
|
|
46677
|
+
}
|
|
46678
|
+
function formatVersionReport(installed, latest) {
|
|
46679
|
+
const head = `${PACKAGE_NAME} ${installed}`;
|
|
46680
|
+
if (!latest) {
|
|
46681
|
+
return `${head}
|
|
46682
|
+
(could not check for updates \u2014 offline or registry unreachable)`;
|
|
46683
|
+
}
|
|
46684
|
+
const cmp = compareVersions(installed, latest);
|
|
46685
|
+
if (cmp < 0) {
|
|
46686
|
+
return [
|
|
46687
|
+
head,
|
|
46688
|
+
`Update available: ${latest} (you have ${installed}).`,
|
|
46689
|
+
`Upgrade: npm install -g ${PACKAGE_NAME}@latest`,
|
|
46690
|
+
`Or just re-run via: npx -y ${PACKAGE_NAME}@latest`
|
|
46691
|
+
].join("\n");
|
|
46692
|
+
}
|
|
46693
|
+
if (cmp > 0) {
|
|
46694
|
+
return `${head}
|
|
46695
|
+
(ahead of the latest published release ${latest})`;
|
|
46696
|
+
}
|
|
46697
|
+
return `${head} (latest)`;
|
|
46698
|
+
}
|
|
46699
|
+
|
|
46566
46700
|
// src/index.ts
|
|
46567
46701
|
async function runServer(cli) {
|
|
46568
46702
|
const cfg = resolveConfig(cli, process.env);
|
|
@@ -46653,6 +46787,12 @@ async function main() {
|
|
|
46653
46787
|
case "help":
|
|
46654
46788
|
console.error(helpText());
|
|
46655
46789
|
return cli.command;
|
|
46790
|
+
case "version": {
|
|
46791
|
+
const installed = readInstalledVersion();
|
|
46792
|
+
const latest = await fetchLatestVersion(PACKAGE_NAME);
|
|
46793
|
+
console.log(formatVersionReport(installed, latest));
|
|
46794
|
+
return cli.command;
|
|
46795
|
+
}
|
|
46656
46796
|
case "run":
|
|
46657
46797
|
default:
|
|
46658
46798
|
await runServer(cli);
|