@safekb/mcp 0.14.1 → 0.15.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 +5 -4
- package/dist/safekb-mcp.js +93 -78
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -333,7 +333,7 @@ and `import_seal_keys`, and for backups `export_backup` and
|
|
|
333
333
|
What you type at the terminal for the command line, you type at the [sidecar](#the-sidecar) for a
|
|
334
334
|
tool, while the call waits: the passphrase of an export file or a key export (twice for a new one),
|
|
335
335
|
and the name or id that confirms `delete_seal_key`. It reaches this client only, never the conversation. Without a running sidecar such a tool fails and says
|
|
336
|
-
to start one (`safekb
|
|
336
|
+
to start one (`npx -y @safekb/mcp --sidecar`); on Windows, which has no sidecar, it names the command to run
|
|
337
337
|
instead. File paths given to the tools must be absolute.
|
|
338
338
|
|
|
339
339
|
### Moving keys to another machine
|
|
@@ -375,11 +375,12 @@ custom fields and the key name of every custom field (sealed ones too) stay read
|
|
|
375
375
|
field (`llm_field_visibility`, e.g. a card's number and CVC hidden, its expiry date visible).
|
|
376
376
|
`reveal_sealed_credential` and `get_sealed_totp` work only on a `visible` field. New and migrated
|
|
377
377
|
sealed credentials start with the tenant default (Settings → Sealed credentials; `use_only` until
|
|
378
|
-
set).
|
|
378
|
+
set). `insert_sealed_credential` requires `llm_visibility` (`use_only` or `visible`), which the agent
|
|
379
|
+
asks you for; an agent's `visible` is refused unless it is the tenant default. Only you make anything more visible, in the SafeKB app; `set_sealed_credential_llm_visibility`
|
|
379
380
|
only tightens. The client enforces this: a client older than 0.11.0 reveals every sealed credential, and 0.11.0 ignores field overrides.
|
|
380
381
|
- Entering a secret without the agent seeing it: `insert_sealed_credential` and
|
|
381
382
|
`update_sealed_credential` take `typed_at_sidecar`, e.g. `["custom:number", "custom:cvc", "password"]`.
|
|
382
|
-
The call waits while you type each field at the sidecar (`safekb
|
|
383
|
+
The call waits while you type each field at the sidecar (`npx -y @safekb/mcp --sidecar`), hidden and twice;
|
|
383
384
|
the agent passes only the visible fields. Or enter it in the SafeKB web app.
|
|
384
385
|
- Using a secret without showing it: SafeKB only provides the value. `secret_exec` runs your own
|
|
385
386
|
program with it in env, stdin or a temp file (`files`, mode 0600, deleted afterwards), never argv;
|
|
@@ -441,7 +442,7 @@ What an export holds:
|
|
|
441
442
|
- Credentials with every secret field. Each secret of a server-encrypted credential is one audited
|
|
442
443
|
reveal on the server.
|
|
443
444
|
- Sealed folders, documents, attachments and credentials of every seal root that opens with a secret
|
|
444
|
-
this machine keeps or its sidecar holds (`safekb
|
|
445
|
+
this machine keeps or its sidecar holds (`npx -y @safekb/mcp --sidecar`, then `add <name or id>` for a
|
|
445
446
|
passphrase root), decrypted on this machine.
|
|
446
447
|
- Every other seal root as the server stores it, still sealed: the machine that makes the backup needs
|
|
447
448
|
none of their keys. The export names these roots before it writes. Pending deposits in them are left
|
package/dist/safekb-mcp.js
CHANGED
|
@@ -2578,51 +2578,6 @@ var require_punycode = __commonJS((exports, module) => {
|
|
|
2578
2578
|
// src/main.ts
|
|
2579
2579
|
import { existsSync as existsSync5 } from "node:fs";
|
|
2580
2580
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2581
|
-
// package.json
|
|
2582
|
-
var package_default = {
|
|
2583
|
-
name: "@safekb/mcp",
|
|
2584
|
-
version: "0.14.1",
|
|
2585
|
-
description: "SafeKB local MCP client: proxies the hosted SafeKB tools and adds zero-knowledge sealed documents.",
|
|
2586
|
-
license: "SEE LICENSE IN LICENSE",
|
|
2587
|
-
author: "Mustafa Öztürk",
|
|
2588
|
-
type: "module",
|
|
2589
|
-
bin: {
|
|
2590
|
-
"safekb-mcp": "dist/safekb-mcp.js"
|
|
2591
|
-
},
|
|
2592
|
-
files: [
|
|
2593
|
-
"dist",
|
|
2594
|
-
"README.md",
|
|
2595
|
-
"LICENSE",
|
|
2596
|
-
"THIRD-PARTY-NOTICES.md"
|
|
2597
|
-
],
|
|
2598
|
-
engines: {
|
|
2599
|
-
node: ">=20"
|
|
2600
|
-
},
|
|
2601
|
-
publishConfig: {
|
|
2602
|
-
access: "public"
|
|
2603
|
-
},
|
|
2604
|
-
scripts: {
|
|
2605
|
-
build: "bun build ./src/cli.ts --target=node --outfile=dist/safekb-mcp.js --external '@modelcontextprotocol/sdk/*' --external '@napi-rs/keyring'",
|
|
2606
|
-
"build:mcpb": "bun scripts/build-mcpb.ts",
|
|
2607
|
-
prepack: "bun run build",
|
|
2608
|
-
test: "bun test",
|
|
2609
|
-
typecheck: "tsc --noEmit",
|
|
2610
|
-
lint: "biome check ."
|
|
2611
|
-
},
|
|
2612
|
-
dependencies: {
|
|
2613
|
-
"@modelcontextprotocol/sdk": "1.32.1",
|
|
2614
|
-
"@napi-rs/keyring": "2.1.0"
|
|
2615
|
-
},
|
|
2616
|
-
devDependencies: {
|
|
2617
|
-
"@biomejs/biome": "2.4.6",
|
|
2618
|
-
"@safekb/canvas-markdown": "workspace:*",
|
|
2619
|
-
"@safekb/sealed-crypto": "workspace:*",
|
|
2620
|
-
"@types/bun": "1.3.14",
|
|
2621
|
-
"@types/node": "20.19.43",
|
|
2622
|
-
typescript: "5.9.3",
|
|
2623
|
-
zod: "4.3.6"
|
|
2624
|
-
}
|
|
2625
|
-
};
|
|
2626
2581
|
|
|
2627
2582
|
// src/errors.ts
|
|
2628
2583
|
class ClientError extends Error {
|
|
@@ -6623,12 +6578,63 @@ async function writePrivateFile(path, content) {
|
|
|
6623
6578
|
import { existsSync } from "node:fs";
|
|
6624
6579
|
import { homedir } from "node:os";
|
|
6625
6580
|
import { join as join2 } from "node:path";
|
|
6581
|
+
// package.json
|
|
6582
|
+
var package_default = {
|
|
6583
|
+
name: "@safekb/mcp",
|
|
6584
|
+
version: "0.15.0",
|
|
6585
|
+
description: "SafeKB local MCP client: proxies the hosted SafeKB tools and adds zero-knowledge sealed documents.",
|
|
6586
|
+
license: "SEE LICENSE IN LICENSE",
|
|
6587
|
+
author: "Mustafa Öztürk",
|
|
6588
|
+
type: "module",
|
|
6589
|
+
bin: {
|
|
6590
|
+
"safekb-mcp": "dist/safekb-mcp.js"
|
|
6591
|
+
},
|
|
6592
|
+
files: [
|
|
6593
|
+
"dist",
|
|
6594
|
+
"README.md",
|
|
6595
|
+
"LICENSE",
|
|
6596
|
+
"THIRD-PARTY-NOTICES.md"
|
|
6597
|
+
],
|
|
6598
|
+
engines: {
|
|
6599
|
+
node: ">=20"
|
|
6600
|
+
},
|
|
6601
|
+
publishConfig: {
|
|
6602
|
+
access: "public"
|
|
6603
|
+
},
|
|
6604
|
+
scripts: {
|
|
6605
|
+
build: "bun build ./src/cli.ts --target=node --outfile=dist/safekb-mcp.js --external '@modelcontextprotocol/sdk/*' --external '@napi-rs/keyring'",
|
|
6606
|
+
"build:mcpb": "bun scripts/build-mcpb.ts",
|
|
6607
|
+
prepack: "bun run build",
|
|
6608
|
+
test: "bun test",
|
|
6609
|
+
typecheck: "tsc --noEmit",
|
|
6610
|
+
lint: "biome check ."
|
|
6611
|
+
},
|
|
6612
|
+
dependencies: {
|
|
6613
|
+
"@modelcontextprotocol/sdk": "1.32.1",
|
|
6614
|
+
"@napi-rs/keyring": "2.1.0"
|
|
6615
|
+
},
|
|
6616
|
+
devDependencies: {
|
|
6617
|
+
"@biomejs/biome": "2.4.6",
|
|
6618
|
+
"@safekb/canvas-markdown": "workspace:*",
|
|
6619
|
+
"@safekb/sealed-crypto": "workspace:*",
|
|
6620
|
+
"@types/bun": "1.3.14",
|
|
6621
|
+
"@types/node": "20.19.43",
|
|
6622
|
+
typescript: "5.9.3",
|
|
6623
|
+
zod: "4.3.6"
|
|
6624
|
+
}
|
|
6625
|
+
};
|
|
6626
|
+
|
|
6627
|
+
// src/commandLine.ts
|
|
6628
|
+
var VERSION = package_default.version;
|
|
6629
|
+
var CLI = `npx -y @safekb/mcp@${VERSION}`;
|
|
6630
|
+
|
|
6631
|
+
// src/tokenConfig.ts
|
|
6626
6632
|
function defaultTokenConfigPath() {
|
|
6627
6633
|
return join2(homedir(), ".config", "safekb", "mcp.json");
|
|
6628
6634
|
}
|
|
6629
6635
|
function requireString(value, field, path) {
|
|
6630
6636
|
if (typeof value !== "string" || value.length === 0) {
|
|
6631
|
-
throw new StartupError(`Token file ${path} has no '${field}'; run:
|
|
6637
|
+
throw new StartupError(`Token file ${path} has no '${field}'; run: ${CLI} --authorize`);
|
|
6632
6638
|
}
|
|
6633
6639
|
return value;
|
|
6634
6640
|
}
|
|
@@ -6650,7 +6656,7 @@ function assertTokenConfig(value, source) {
|
|
|
6650
6656
|
}
|
|
6651
6657
|
async function loadTokenConfig(path) {
|
|
6652
6658
|
if (!existsSync(path)) {
|
|
6653
|
-
throw new StartupError(`No token file at ${path}; run:
|
|
6659
|
+
throw new StartupError(`No token file at ${path}; run: ${CLI} --authorize`);
|
|
6654
6660
|
}
|
|
6655
6661
|
let text;
|
|
6656
6662
|
try {
|
|
@@ -8854,7 +8860,7 @@ ${countLines(report.created)}`);
|
|
|
8854
8860
|
this.writeNotes("Seal roots the export holds as stored that had no key here, so everything in them was left out (to restore them, import again with --keys <their key export>; that creates everything else again too)", report.unopenedRoots.map(({ id, name, reason }) => `${name === null ? id : `"${name}" (${id})`}: ${reason}`));
|
|
8855
8861
|
if (report.sealRoots.length === 0)
|
|
8856
8862
|
return;
|
|
8857
|
-
this.reader.write(`New seal roots, each with a newly generated key. Back the keys up (
|
|
8863
|
+
this.reader.write(`New seal roots, each with a newly generated key. Back the keys up (${CLI} --export-keys): a lost key cannot be recovered.
|
|
8858
8864
|
`);
|
|
8859
8865
|
for (const root of report.sealRoots) {
|
|
8860
8866
|
this.reader.write(` "${printable2(root.name)}" (${root.id}), key in ${root.kept}
|
|
@@ -10079,6 +10085,7 @@ class CredentialService {
|
|
|
10079
10085
|
visible: save.visible,
|
|
10080
10086
|
enc_fields: save.encFields,
|
|
10081
10087
|
enc_dek: encDek,
|
|
10088
|
+
llm_visibility: args.llm_visibility,
|
|
10082
10089
|
sigs,
|
|
10083
10090
|
...args.display
|
|
10084
10091
|
}));
|
|
@@ -11690,8 +11697,8 @@ class RawSealedExporter {
|
|
|
11690
11697
|
|
|
11691
11698
|
// src/stores/secretStores.ts
|
|
11692
11699
|
var HOW_TO_ENABLE = {
|
|
11693
|
-
system:
|
|
11694
|
-
file:
|
|
11700
|
+
system: `The system keyring is not set up for this client; run: ${CLI} --configure`,
|
|
11701
|
+
file: `No key file directory is set up; start the client with --keyring <dir>, or run: ${CLI} --configure`,
|
|
11695
11702
|
sidecar: "The sidecar is not available on this platform"
|
|
11696
11703
|
};
|
|
11697
11704
|
|
|
@@ -11722,7 +11729,7 @@ class SecretStores {
|
|
|
11722
11729
|
}
|
|
11723
11730
|
async findIn(stores, seal2) {
|
|
11724
11731
|
if (stores.length === 0) {
|
|
11725
|
-
throw new SecretLookupError("no-store", "No secret given and no secret store is set up",
|
|
11732
|
+
throw new SecretLookupError("no-store", "No secret given and no secret store is set up", `: run ${CLI} --configure or start the client with --keyring <dir>, or give a file holding the secret with provide_seal_secret (secret_file)`);
|
|
11726
11733
|
}
|
|
11727
11734
|
const found = [];
|
|
11728
11735
|
const notes = [];
|
|
@@ -11735,7 +11742,7 @@ class SecretStores {
|
|
|
11735
11742
|
}
|
|
11736
11743
|
const [first, ...others] = found;
|
|
11737
11744
|
if (first === undefined) {
|
|
11738
|
-
throw new SecretLookupError("not-found", `No secret for seal root ${seal2.rootId} was found: ${[...new Set(notes)].join("; ")}`, `. To open it, the user types its secret at the sidecar (
|
|
11745
|
+
throw new SecretLookupError("not-found", `No secret for seal root ${seal2.rootId} was found: ${[...new Set(notes)].join("; ")}`, `. To open it, the user types its secret at the sidecar (${CLI} --sidecar, then: add ${seal2.rootId}) and the call is repeated, or a file holding it is given with provide_seal_secret (secret_file)`);
|
|
11739
11746
|
}
|
|
11740
11747
|
if (others.length > 0) {
|
|
11741
11748
|
throw new SecretLookupError("ambiguous", `Seal root ${seal2.rootId} has a secret in more than one store (${found.map(({ store }) => store).join(", ")})`, "; name the one to use with provide_seal_secret (store)");
|
|
@@ -11747,7 +11754,7 @@ class SecretStores {
|
|
|
11747
11754
|
return this.require(name);
|
|
11748
11755
|
const { defaultKeyStore } = this.set;
|
|
11749
11756
|
if (defaultKeyStore === null) {
|
|
11750
|
-
throw new SecretLookupError("no-default", "No default store is set up for new keys",
|
|
11757
|
+
throw new SecretLookupError("no-default", "No default store is set up for new keys", `; run: ${CLI} --configure. Or pass store (system, file, sidecar), key_out_path or passphrase.`);
|
|
11751
11758
|
}
|
|
11752
11759
|
return this.require(defaultKeyStore);
|
|
11753
11760
|
}
|
|
@@ -25907,7 +25914,7 @@ var EXPOSURE_NOTE = "Everything this tool returns enters the conversation transc
|
|
|
25907
25914
|
var PLAINTEXT_ARGUMENTS_NOTE = "The text you pass as arguments is plaintext: it is kept in the conversation transcript on this machine and has already been sent to the model provider. Sealing protects it on the server only.";
|
|
25908
25915
|
var objectId2 = (what) => exports_external.string().regex(/^[0-9a-f]{24}$/, "must be a 24-character id").describe(what);
|
|
25909
25916
|
var rootRef = (what) => exports_external.string().min(1).describe(`${what} Its id, or the name this machine knows it by (a name is accepted directly: no lookup needed).`);
|
|
25910
|
-
var SIDECAR_TYPING =
|
|
25917
|
+
var SIDECAR_TYPING = `The user types it at the running sidecar (${CLI} --sidecar) while the call waits: tell them to look there. It never enters the conversation.`;
|
|
25911
25918
|
var absolutePath = (what) => exports_external.string().min(1).refine(isAbsolute, "must be an absolute path").describe(what);
|
|
25912
25919
|
var expectedSeq = exports_external.number().int().min(0).describe("The seal_seq you last read for this node (from list_sealed, read_sealed_document or the result of your last write). The write fails if it changed since; never guess it.");
|
|
25913
25920
|
function shownError(error48) {
|
|
@@ -33407,7 +33414,7 @@ class HostedProxy {
|
|
|
33407
33414
|
const session = await HostedProxy.open(endpoint, settings);
|
|
33408
33415
|
return new HostedProxy(endpoint, settings, session);
|
|
33409
33416
|
} catch (error48) {
|
|
33410
|
-
throw new StartupError(`Cannot reach the hosted SafeKB MCP endpoint ${endpoint.href}: ${error48.message}. ` +
|
|
33417
|
+
throw new StartupError(`Cannot reach the hosted SafeKB MCP endpoint ${endpoint.href}: ${error48.message}. ` + `Check the URL and the token (${CLI} --authorize), or start with --sealed-only.`);
|
|
33411
33418
|
}
|
|
33412
33419
|
}
|
|
33413
33420
|
instructions() {
|
|
@@ -33670,10 +33677,10 @@ var TOOL_VOCABULARY = {
|
|
|
33670
33677
|
listKeys: "list_seal_keys",
|
|
33671
33678
|
store: "store",
|
|
33672
33679
|
howToGoOn: {
|
|
33673
|
-
"no-store":
|
|
33680
|
+
"no-store": `: run ${CLI} --configure, or start the client with --keyring <dir> for a directory of key files`,
|
|
33674
33681
|
"not-found": ". The key tools work with a key kept in the system keyring or in the key file directory (see list_seal_keys); a root opened by a passphrase is changed with change_seal_password.",
|
|
33675
33682
|
ambiguous: "; run move_seal_key with store system or file to leave one copy",
|
|
33676
|
-
"no-default":
|
|
33683
|
+
"no-default": `; run ${CLI} --configure, or pass store system|file`
|
|
33677
33684
|
}
|
|
33678
33685
|
};
|
|
33679
33686
|
|
|
@@ -33855,10 +33862,10 @@ var COMMAND_LINE_VOCABULARY = {
|
|
|
33855
33862
|
listKeys: "--list-keys",
|
|
33856
33863
|
store: "--store",
|
|
33857
33864
|
howToGoOn: {
|
|
33858
|
-
"no-store":
|
|
33865
|
+
"no-store": `: run ${CLI} --configure, or pass --keyring <dir> for a directory of key files`,
|
|
33859
33866
|
"not-found": ". The key modes work with a key kept in the system keyring or in the key file directory (see --list-keys); a root opened by a passphrase is changed with the MCP tools.",
|
|
33860
33867
|
ambiguous: "; run --move-key with --store system or --store file to leave one copy",
|
|
33861
|
-
"no-default":
|
|
33868
|
+
"no-default": `; run ${CLI} --configure, or pass --store system|file`
|
|
33862
33869
|
}
|
|
33863
33870
|
};
|
|
33864
33871
|
function lines(...text2) {
|
|
@@ -34565,7 +34572,7 @@ function send(socketPath, request, timeoutMs) {
|
|
|
34565
34572
|
resolve(received);
|
|
34566
34573
|
});
|
|
34567
34574
|
socket.once("error", (error48) => {
|
|
34568
|
-
reject(NOT_RUNNING_CODES.has(error48.code ?? "") ? new SidecarNotRunningError(`No sidecar is running at ${socketPath}; start one in a terminal with:
|
|
34575
|
+
reject(NOT_RUNNING_CODES.has(error48.code ?? "") ? new SidecarNotRunningError(`No sidecar is running at ${socketPath}; start one in a terminal with: ${CLI} --sidecar`) : error48);
|
|
34569
34576
|
});
|
|
34570
34577
|
});
|
|
34571
34578
|
}
|
|
@@ -35309,7 +35316,7 @@ function assertStoreConfig(value, path2) {
|
|
|
35309
35316
|
const systemKeyring = raw?.system_keyring;
|
|
35310
35317
|
const keyDir = raw?.key_dir ?? null;
|
|
35311
35318
|
if (typeof raw !== "object" || raw === null || !KEY_STORE_NAMES.includes(store) || typeof systemKeyring !== "boolean" || !(keyDir === null || typeof keyDir === "string" && keyDir.length > 0) || store === "file" && keyDir === null || store === "system" && !systemKeyring) {
|
|
35312
|
-
throw new StartupError(`Store settings ${path2} are malformed; run:
|
|
35319
|
+
throw new StartupError(`Store settings ${path2} are malformed; run: ${CLI} --configure`);
|
|
35313
35320
|
}
|
|
35314
35321
|
return {
|
|
35315
35322
|
default_store: store,
|
|
@@ -35781,7 +35788,7 @@ function reportOf(report) {
|
|
|
35781
35788
|
};
|
|
35782
35789
|
}
|
|
35783
35790
|
function commandLine(mode2, path2, flags) {
|
|
35784
|
-
const words2 = [
|
|
35791
|
+
const words2 = [`${CLI} --${mode2} ${path2}`];
|
|
35785
35792
|
for (const [flag, value] of flags) {
|
|
35786
35793
|
if (value === undefined || value === false)
|
|
35787
35794
|
continue;
|
|
@@ -35824,11 +35831,11 @@ function backupTools(parts) {
|
|
|
35824
35831
|
note: "The file opens only with the passphrase the user typed: they should keep both, apart."
|
|
35825
35832
|
};
|
|
35826
35833
|
}),
|
|
35827
|
-
defineTool("import_backup",
|
|
35834
|
+
defineTool("import_backup", `Restore from a file written by export_backup (or ${CLI} --export): create what it holds in this token's account, as new things with new ids; nothing that exists is changed. There is no dry run. ` + "Each exported space becomes a new space, each seal root a new seal root with a newly generated key. A seal root the file holds as stored is opened with a key from key_exports or one this machine keeps; without one, everything in it is left out and named in the result. " + `The passphrase of the file, and of each key export, is asked of the user: ${SIDECAR_TYPING} ${LONG_RUN}`, {
|
|
35828
35835
|
path: absolutePath("The export file."),
|
|
35829
35836
|
into_space_id: exports_external.string().min(1).describe("Put everything into this one existing space instead of creating the exported spaces again.").optional(),
|
|
35830
35837
|
store: exports_external.enum([...KEY_STORE_NAMES, "default"]).default("default").describe("Where the keys of the new seal roots are kept: system (the operating system's keyring), file (the key file directory), or the default store."),
|
|
35831
|
-
key_exports: exports_external.array(absolutePath("A key export file.")).min(1).describe(
|
|
35838
|
+
key_exports: exports_external.array(absolutePath("A key export file.")).min(1).describe(`Key exports (export_seal_keys, ${CLI} --export-keys) whose keys open the seal roots the file holds as stored. Used for this import only; not kept.`).optional()
|
|
35832
35839
|
}, async (args, progress) => {
|
|
35833
35840
|
const keyExports = args.key_exports ?? [];
|
|
35834
35841
|
const files = new BackupFiles(questions(commandLine("import", args.path, [
|
|
@@ -35860,7 +35867,7 @@ Imported before it stopped: ${JSON.stringify(reportOf(error48.report))}`);
|
|
|
35860
35867
|
// src/tools/rootTools.ts
|
|
35861
35868
|
var KEY_OUT_PATH = "Path for a new key file (created with mode 0600; must not exist). A key is generated and written there; it is never returned.";
|
|
35862
35869
|
var PASSPHRASE_WARNING = "A passphrase passed here enters the conversation transcript and reaches the model provider; prefer store or key_out_path.";
|
|
35863
|
-
var NEW_SECRET_STORE = "Where this machine keeps the new secret, which is never returned. " +
|
|
35870
|
+
var NEW_SECRET_STORE = "Where this machine keeps the new secret, which is never returned. " + `default: the store chosen by ${CLI} --configure. ` + "system: a generated key in the operating system's keyring. " + "file: a generated key as a file (mode 0600) in the keyring directory. " + `sidecar: a passphrase the user types at the running sidecar (${CLI} --sidecar), held in its memory for a limited time; the call waits for them to type it.`;
|
|
35864
35871
|
var newSecretStore = exports_external.enum(NEW_SECRET_STORES).describe(NEW_SECRET_STORE).optional();
|
|
35865
35872
|
function rootTools(roots, names) {
|
|
35866
35873
|
const resolved = async (reference2) => reference2 === undefined ? undefined : names.resolve(reference2);
|
|
@@ -35967,7 +35974,7 @@ function keyTools(parts) {
|
|
|
35967
35974
|
};
|
|
35968
35975
|
})),
|
|
35969
35976
|
defineTool("delete_seal_key", "Remove every key of a seal root from this machine's key stores and forget its name. The root and its content on the server are not touched (to delete the root itself, use delete_sealed_document). " + "The user is shown the keys and what removing them costs, and must type the root's name or id at the sidecar to go ahead; nothing is removed otherwise. " + `${SIDECAR_TYPING} A key that exists nowhere else is lost for good: content it alone opens can never be read again.`, { root_id: rootRef("The seal root.") }, (args) => managers.run(async (manager) => {
|
|
35970
|
-
const deleted = await manager.delete(args.root_id, questions(
|
|
35977
|
+
const deleted = await manager.delete(args.root_id, questions(`${CLI} --delete-key ${args.root_id}`));
|
|
35971
35978
|
return {
|
|
35972
35979
|
root_id: deleted.rootId,
|
|
35973
35980
|
removed_from: deleted.removed,
|
|
@@ -35976,7 +35983,7 @@ function keyTools(parts) {
|
|
|
35976
35983
|
};
|
|
35977
35984
|
})),
|
|
35978
35985
|
defineTool("export_seal_keys", "Write every generated key this machine keeps to a new file, encrypted under a passphrase the user chooses: for moving the keys to another machine or keeping them safe. It holds no documents (for those: export_backup). " + `${SIDECAR_TYPING} Returns the seal roots whose keys are in the file, never a key. A seal root opened by a passphrase has no key here and is not in the file.`, { path: absolutePath("The new file; must not exist.") }, async (args) => {
|
|
35979
|
-
const keys = await transfer.exportWith(args.path, questions(
|
|
35986
|
+
const keys = await transfer.exportWith(args.path, questions(`${CLI} --export-keys ${args.path}`));
|
|
35980
35987
|
return {
|
|
35981
35988
|
path: args.path,
|
|
35982
35989
|
keys: keys.length,
|
|
@@ -35984,8 +35991,8 @@ function keyTools(parts) {
|
|
|
35984
35991
|
note: "The file opens only with the passphrase the user typed: they should keep both, apart."
|
|
35985
35992
|
};
|
|
35986
35993
|
}),
|
|
35987
|
-
defineTool("import_seal_keys",
|
|
35988
|
-
const { imported, store, present } = await transfer.importWith(args.path, questions(
|
|
35994
|
+
defineTool("import_seal_keys", `Keep the keys of a file written by export_seal_keys (or ${CLI} --export-keys) on this machine, in the default key store, so the sealed tools open the roots they belong to here. A key this machine already keeps is left as it is. To restore documents from a backup, use import_backup. ` + `The file's passphrase is asked of the user: ${SIDECAR_TYPING}`, { path: absolutePath("The key export file.") }, async (args) => {
|
|
35995
|
+
const { imported, store, present } = await transfer.importWith(args.path, questions(`${CLI} --import-keys ${args.path}`));
|
|
35989
35996
|
return {
|
|
35990
35997
|
imported: await rootsOf(imported, names),
|
|
35991
35998
|
kept_in: store === null ? null : KEY_STORE_LABELS[store],
|
|
@@ -36169,6 +36176,7 @@ var credentialId = objectId2("Id of the sealed credential.");
|
|
|
36169
36176
|
var expectedVersion = exports_external.number().int().min(1).describe("The version you last read for this credential. The write fails if it changed since.");
|
|
36170
36177
|
var CLEARABLE = "An empty string clears the field.";
|
|
36171
36178
|
var SECRET_ARGUMENT_NOTE = "Secret values passed here are part of the conversation transcript and reach the model provider; the server never sees them.";
|
|
36179
|
+
var TYPED_NOTE = "For a secret you must not see (one the user has not given you, e.g. a card number, PIN or bank password), pass typed_at_sidecar with the field names instead of the value: the user types it at the sidecar while the call waits. Then use it with secret_exec, never by revealing it.";
|
|
36172
36180
|
var visibleFields = {
|
|
36173
36181
|
username: exports_external.string().describe(`Visible to the server. ${CLEARABLE}`).optional(),
|
|
36174
36182
|
url: exports_external.string().describe(`Visible to the server. ${CLEARABLE}`).optional(),
|
|
@@ -36189,7 +36197,7 @@ var sealedFields = {
|
|
|
36189
36197
|
function credentialTools(credentials, migration, names, entry) {
|
|
36190
36198
|
const atSidecar = (fields2) => fields2 === undefined ? undefined : { fields: fields2, entry };
|
|
36191
36199
|
return [
|
|
36192
|
-
defineTool("reveal_sealed_credential", `Decrypt one sealed field of a sealed credential on this machine. The server only sees that the ciphertext was fetched. Find credentials and their visible fields with the standard credential tools (search_credentials, get_credential). Refused unless its llm_visibility is visible. ${EXPOSURE_NOTE}`, {
|
|
36200
|
+
defineTool("reveal_sealed_credential", `Decrypt one sealed field of a sealed credential on this machine. The server only sees that the ciphertext was fetched. Find credentials and their visible fields with the standard credential tools (search_credentials, get_credential). Refused unless its llm_visibility is visible. To use a secret (log in, call an API, type it on a device), use secret_exec instead: it never shows the value. ${EXPOSURE_NOTE}`, {
|
|
36193
36201
|
credential_id: credentialId,
|
|
36194
36202
|
field: exports_external.string().min(1).describe("password, totp, sealed_notes, or custom:{key}.")
|
|
36195
36203
|
}, (args) => credentials.reveal(args.credential_id, args.field)),
|
|
@@ -36199,10 +36207,11 @@ function credentialTools(credentials, migration, names, entry) {
|
|
|
36199
36207
|
llm_visibility: exports_external.enum(["use_only", "human_only"]).describe("human_only is the more restrictive."),
|
|
36200
36208
|
field: exports_external.string().min(1).describe("One sealed field: password, totp, sealed_notes or custom:{key}. Omit for the whole credential; a field without its own value follows the credential.").optional()
|
|
36201
36209
|
}, (args) => credentials.setLlmVisibility(args.credential_id, args.llm_visibility, args.field)),
|
|
36202
|
-
defineTool("insert_sealed_credential", `Create a sealed credential in a seal root. It is kept in the space of that root.
|
|
36210
|
+
defineTool("insert_sealed_credential", `Create a sealed credential in a seal root. ${TYPED_NOTE} It is kept in the space of that root. Ask the user for llm_visibility before the call. password, totp, sealed_notes and the values of sealed custom fields are encrypted on this machine; title, username, url, tags, notes, visible custom fields and the key name of every custom field (sealed ones too) stay readable by the server so search and duplicate detection work. ${SECRET_ARGUMENT_NOTE}`, {
|
|
36203
36211
|
root_id: rootRef("The seal root that will own it."),
|
|
36204
36212
|
title: exports_external.string().min(1).describe("Visible to the server."),
|
|
36205
36213
|
folder_id: exports_external.string().min(1).describe("Group it belongs to.").optional(),
|
|
36214
|
+
llm_visibility: exports_external.enum(["use_only", "visible"]).describe("What an LLM may do with its secrets. Ask the user before the call and never choose for them. use_only: never shown to an LLM; only secret_exec hands it to a program. visible: an LLM may also reveal it into the conversation (reveal_sealed_credential, get_sealed_totp). An agent may pick visible only when that is the account default; otherwise the call fails and nothing is created: tell the user, who makes it visible in the SafeKB app after you create it use_only."),
|
|
36206
36215
|
...visibleFields,
|
|
36207
36216
|
...sealedFields
|
|
36208
36217
|
}, async (args) => credentials.insert({
|
|
@@ -36210,7 +36219,7 @@ function credentialTools(credentials, migration, names, entry) {
|
|
|
36210
36219
|
root_id: await names.resolve(args.root_id),
|
|
36211
36220
|
typed_at_sidecar: atSidecar(args.typed_at_sidecar)
|
|
36212
36221
|
})),
|
|
36213
|
-
defineTool("update_sealed_credential", `Change a sealed credential with its seal root's signature. Pass only the fields to change; the save also attests the visible fields as they are now. If visible fields were changed without the key since the last sealed save, the call fails and names them until accept_visible_changes is true. ${SECRET_ARGUMENT_NOTE}`, {
|
|
36222
|
+
defineTool("update_sealed_credential", `Change a sealed credential with its seal root's signature. Pass only the fields to change; the save also attests the visible fields as they are now. If visible fields were changed without the key since the last sealed save, the call fails and names them until accept_visible_changes is true. ${TYPED_NOTE} ${SECRET_ARGUMENT_NOTE}`, {
|
|
36214
36223
|
credential_id: credentialId,
|
|
36215
36224
|
expected_version: expectedVersion,
|
|
36216
36225
|
accept_visible_changes: exports_external.boolean().describe("Attest visible fields that were changed without the key, after reviewing them.").optional(),
|
|
@@ -36399,9 +36408,9 @@ var SEALED_INSTRUCTIONS = `# Sealed documents (local, zero-knowledge)
|
|
|
36399
36408
|
Sealed documents and folders are encrypted on this machine; the SafeKB server stores only ciphertext. Only the *_sealed* tools can read or write them. The standard tools show them as locked placeholders.
|
|
36400
36409
|
|
|
36401
36410
|
- Each sealed tool opens the seal root (a sealed folder, or a standalone sealed document) it needs by itself, with the secret the local store that has it keeps: the system keyring, a key file, or the sidecar the user typed it into. There is no unlock step. Any session on this machine that uses these key stores can read every root whose secret is kept here.
|
|
36402
|
-
- A root whose secret no store holds fails with an error that says how the user supplies it: typed at the sidecar (
|
|
36411
|
+
- A root whose secret no store holds fails with an error that says how the user supplies it: typed at the sidecar (${CLI} --sidecar, then: add <root>), after which the call is repeated, or as a file given to provide_seal_secret (secret_file).
|
|
36403
36412
|
- Never ask the user to paste a secret and never pass one as text: text arguments and results are kept in the local transcript and sent to the model provider.
|
|
36404
|
-
- A new seal root gets its secret from store (default, system, file or sidecar) or key_out_path. With store "sidecar" the user types a passphrase in the sidecar's terminal (
|
|
36413
|
+
- A new seal root gets its secret from store (default, system, file or sidecar) or key_out_path. With store "sidecar" the user types a passphrase in the sidecar's terminal (${CLI} --sidecar) while the call waits: tell them to look there.
|
|
36405
36414
|
- A seal root is named by its id or by the name this machine knows it by (the title it was created or last opened with): every root_id and new_root_id takes either. Documents and folders inside a root are named by id only.
|
|
36406
36415
|
- Finding roots and ids: list_seal_keys shows the roots this machine holds a key for (id, name, status). The standard tools (list_documents, get_space_tree) show sealed folders and documents as locked placeholders, whose ids work as root_id, parent_id or document_id here. Ids are never guessed: look them up.
|
|
36407
36416
|
- Workflow: search_sealed or list_sealed → get_sealed_section / read_sealed_document → update_sealed_document or replace_in_sealed_document with the seal_seq you read. Writes and deletes of a node take the seal_seq of your last read: read first.
|
|
@@ -36413,7 +36422,7 @@ Sealed documents and folders are encrypted on this machine; the SafeKB server st
|
|
|
36413
36422
|
|
|
36414
36423
|
The keys of this machine, and backups, are handled by tools that never take or return a secret: list_seal_keys, rename_seal_key, rotate_seal_key, move_seal_key, delete_seal_key, export_seal_keys, import_seal_keys, export_backup and import_backup.
|
|
36415
36424
|
|
|
36416
|
-
- Every passphrase they need (of an export file or a key export), and the confirmation of delete_seal_key, are typed by the user at the sidecar (
|
|
36425
|
+
- Every passphrase they need (of an export file or a key export), and the confirmation of delete_seal_key, are typed by the user at the sidecar (${CLI} --sidecar) while the call waits: tell them to look there, and to start the sidecar first if the call says none is running. Never ask for a passphrase in the conversation.
|
|
36417
36426
|
- File paths must be absolute. An export never overwrites a file.
|
|
36418
36427
|
- seal_document and unseal_document copy a document between hosted and sealed form without its text passing through you: pass ids, never read a document and retype it with insert_sealed_document. Both default to dry_run: true (report only) and delete_original: false: call once as is, read problems and not_carried, then call again with dry_run: false. Pass delete_original: true only when the user asked to move the document or to delete the original; the original is deleted only after the copy has been verified. They check the copy against the original before reporting it, and say what exists when something fails. When the target folder is in another space than the document, or several documents match the title the user gave, ask the user: never move a document to another space or pick one of several yourself. seal_document is not available with --sealed-only.
|
|
36419
36428
|
- unseal_document is not a backup: it turns a sealed document into a plain one the server can read. export_backup writes an encrypted copy of everything the token reaches, and no key; export_seal_keys writes the keys.
|
|
@@ -36421,9 +36430,16 @@ The keys of this machine, and backups, are handled by tools that never take or r
|
|
|
36421
36430
|
|
|
36422
36431
|
## Sealed credentials
|
|
36423
36432
|
|
|
36433
|
+
Every credential is stored sealed (insert_sealed_credential). Use the standard insert_credential only when the user asks for an unsealed one. When the sidecar is needed and not running, ask the user to start it (${CLI} --sidecar) and repeat the call: never store the credential unsealed instead.
|
|
36434
|
+
|
|
36435
|
+
Secrets in short:
|
|
36436
|
+
1. Ask the user whether LLMs may see its secrets: use_only (only secret_exec uses them) or visible (an LLM may also reveal them). Pass the answer as llm_visibility; never choose for them.
|
|
36437
|
+
2. Store it: insert_sealed_credential with the visible fields and, for a secret you must not see, typed_at_sidecar: ["password"] (or custom:{key}, totp, sealed_notes). The user types it at the sidecar (${CLI} --sidecar, started from a folder outside the knowledge-base repo) while the call waits. Never ask for it in the conversation.
|
|
36438
|
+
3. Use it: secret_exec with a handle {{secret://<credential_id>/<field>}} in env, stdin or files; your own program does the rest (HTTP, browser, adb, ssh, ...).
|
|
36439
|
+
|
|
36424
36440
|
A credential with \`sealed: true\` belongs to a seal root. Its title, username, url, tags, notes and visible custom fields are readable by the server: find it with the standard credential tools. Its password, TOTP seed, sealed_notes and sealed custom fields open only here: reveal_sealed_credential, get_sealed_totp, insert_sealed_credential, update_sealed_credential.
|
|
36425
36441
|
|
|
36426
|
-
- A secret the user has not given you yet (a card number, a PIN, a new password): never ask for it in the conversation. Call insert_sealed_credential or update_sealed_credential with typed_at_sidecar naming the fields; the user types them at the sidecar (
|
|
36442
|
+
- A secret the user has not given you yet (a card number, a PIN, a new password): never ask for it in the conversation. Call insert_sealed_credential or update_sealed_credential with typed_at_sidecar naming the fields; the user types them at the sidecar (${CLI} --sidecar) while the call waits. Or they enter it in the SafeKB web app.
|
|
36427
36443
|
- Reveal a secret only when the task needs it, and prefer get_sealed_totp over revealing a TOTP seed.
|
|
36428
36444
|
- llm_visibility is set per credential, and llm_field_visibility overrides it for single sealed fields. reveal_sealed_credential and get_sealed_totp work only on a field that is visible. Only the user makes anything visible, in the SafeKB app.
|
|
36429
36445
|
- To use a secret without seeing it (visible or use_only fields): secret_exec runs your own program with it in env, stdin or a temp file; every integration (HTTP, browser, adb, ssh, ...) is that program. Name it by handle, {{secret://<credential_id>/<field>}}; a totp handle becomes the current code. Prefer this over revealing, even for a visible field. A human_only field is never used by a tool: ask the user to enter it.
|
|
@@ -36566,7 +36582,6 @@ class KeyTransfer {
|
|
|
36566
36582
|
}
|
|
36567
36583
|
|
|
36568
36584
|
// src/main.ts
|
|
36569
|
-
var VERSION = package_default.version;
|
|
36570
36585
|
function parsed(value) {
|
|
36571
36586
|
if (value === null) {
|
|
36572
36587
|
throw new Error("an option its mode requires was not parsed");
|
|
@@ -36711,7 +36726,7 @@ function apiUrlFor(options, config3) {
|
|
|
36711
36726
|
const issuedFor = new URL(config3.url).origin;
|
|
36712
36727
|
const requested = new URL(options.url).origin;
|
|
36713
36728
|
if (requested !== issuedFor) {
|
|
36714
|
-
throw new StartupError(`The token in ${options.configPath} was issued for ${issuedFor} and is not sent to ${requested}. To use ${requested} instead, run:
|
|
36729
|
+
throw new StartupError(`The token in ${options.configPath} was issued for ${issuedFor} and is not sent to ${requested}. To use ${requested} instead, run: ${CLI} --authorize --url ${options.url}`);
|
|
36715
36730
|
}
|
|
36716
36731
|
return options.url;
|
|
36717
36732
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@safekb/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "SafeKB local MCP client: proxies the hosted SafeKB tools and adds zero-knowledge sealed documents.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "Mustafa Öztürk",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@biomejs/biome": "2.4.6",
|
|
37
|
-
"@safekb/canvas-markdown": "
|
|
38
|
-
"@safekb/sealed-crypto": "
|
|
37
|
+
"@safekb/canvas-markdown": "workspace:*",
|
|
38
|
+
"@safekb/sealed-crypto": "workspace:*",
|
|
39
39
|
"@types/bun": "1.3.14",
|
|
40
40
|
"@types/node": "20.19.43",
|
|
41
41
|
"typescript": "5.9.3",
|