@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 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-mcp --sidecar`); on Windows, which has no sidecar, it names the command to run
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). Only you make anything more visible, in the SafeKB app; `set_sealed_credential_llm_visibility`
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-mcp --sidecar`), hidden and twice;
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-mcp --sidecar`, then `add <name or id>` for a
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
@@ -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: safekb-mcp --authorize`);
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: safekb-mcp --authorize`);
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 (safekb-mcp --export-keys): a lost key cannot be recovered.
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: "The system keyring is not set up for this client; run: safekb-mcp --configure",
11694
- file: "No key file directory is set up; start the client with --keyring <dir>, or run: safekb-mcp --configure",
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", ": run safekb-mcp --configure or start the client with --keyring <dir>, or give a file holding the secret with provide_seal_secret (secret_file)");
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 (safekb-mcp --sidecar, then: add ${seal2.rootId}) and the call is repeated, or a file holding it is given with provide_seal_secret (secret_file)`);
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", "; run: safekb-mcp --configure. Or pass store (system, file, sidecar), key_out_path or passphrase.");
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 = "The user types it at the running sidecar (safekb-mcp --sidecar) while the call waits: tell them to look there. It never enters the conversation.";
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}. ` + "Check the URL and the token (safekb-mcp --authorize), or start with --sealed-only.");
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": ": run safekb-mcp --configure, or start the client with --keyring <dir> for a directory of key files",
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": "; run safekb-mcp --configure, or pass store system|file"
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": ": run safekb-mcp --configure, or pass --keyring <dir> for a directory of key files",
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": "; run safekb-mcp --configure, or pass --store system|file"
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: safekb-mcp --sidecar`) : error48);
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: safekb-mcp --configure`);
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 = [`safekb-mcp --${mode2} ${path2}`];
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", "Restore from a file written by export_backup (or safekb-mcp --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}`, {
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("Key exports (export_seal_keys, safekb-mcp --export-keys) whose keys open the seal roots the file holds as stored. Used for this import only; not kept.").optional()
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. " + "default: the store chosen by safekb-mcp --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 (safekb-mcp --sidecar), held in its memory for a limited time; the call waits for them to type it.";
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(`safekb-mcp --delete-key ${args.root_id}`));
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(`safekb-mcp --export-keys ${args.path}`));
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", "Keep the keys of a file written by export_seal_keys (or safekb-mcp --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) => {
35988
- const { imported, store, present } = await transfer.importWith(args.path, questions(`safekb-mcp --import-keys ${args.path}`));
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. Whether an LLM may reveal its secrets follows the default the user chose for the tenant; only the user changes it, in the SafeKB app. 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}`, {
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 (safekb-mcp --sidecar, then: add <root>), after which the call is repeated, or as a file given to provide_seal_secret (secret_file).
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 (safekb-mcp --sidecar) while the call waits: tell them to look there.
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 (safekb-mcp --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.
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 (safekb-mcp --sidecar) while the call waits. Or they enter it in the SafeKB web app.
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: safekb-mcp --authorize --url ${options.url}`);
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.14.1",
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": "0.1.0",
38
- "@safekb/sealed-crypto": "0.1.0",
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",