neuro-vault-mcp 6.0.0 → 8.0.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
@@ -2,7 +2,7 @@
2
2
 
3
3
  > 🧠💾 **Make your personal vault usable by agents.** Low-token retrieval, explicit provenance, and safe writes for your Obsidian notes — in Claude Code, Cursor, Windsurf, and any MCP client.
4
4
 
5
- https://github.com/user-attachments/assets/25c1bafb-7b90-43ac-aa50-50e85705fb5b
5
+ [https://github.com/user-attachments/assets/25c1bafb-7b90-43ac-aa50-50e85705fb5b](https://github.com/user-attachments/assets/5ac86cd7-2d67-420d-b940-dba4ae1a2ccb)
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/neuro-vault-mcp)](https://www.npmjs.com/package/neuro-vault-mcp)
8
8
  [![Node.js](https://img.shields.io/node/v/neuro-vault-mcp)](https://nodejs.org)
@@ -140,7 +140,7 @@ Two vaults cannot share the same directory basename — the basename doubles as
140
140
  With multiple vaults registered:
141
141
 
142
142
  - **Every tool** accepts an optional `vault: "<name>"` parameter to target a specific vault.
143
- - **`search_notes`, `query_notes`, and `get_vault_overview`** fan out across all registered vaults when `vault` is omitted. The response shape switches to `results_by_vault: [...]` (one entry per vault) plus `skipped_vaults: [...]` for any vault the tool could not reach.
143
+ - **`search_notes`, `query_notes`, `get_vault_overview`, `list_tags`, and `list_properties`** fan out across all registered vaults when `vault` is omitted. The response shape switches to `results_by_vault: [...]` (one entry per vault) plus `skipped_vaults: [...]` for any vault the tool could not reach and `failed_vaults: [...]` for per-vault runtime errors (`{ vault, error: { code, message, details? } }`). A single failed vault does not abort the whole call.
144
144
  - **All other tools** (writes, reads of specific paths, single-vault diagnostics) require an explicit `vault` in multi-vault mode. Omitting it returns `VAULT_REQUIRED`.
145
145
  - **Semantic fan-out** silently skips vaults whose Smart Connections `.smart-env/multi/` index is unavailable. Targeting such a vault explicitly with `vault: "<name>"` returns `SEMANTIC_INDEX_NOT_FOUND`.
146
146
 
package/dist/cli.js CHANGED
@@ -59,10 +59,6 @@ async function parseConfig(argv) {
59
59
  type: "boolean",
60
60
  default: true,
61
61
  describe: "Enable semantic search module (Smart Connections embeddings)"
62
- }).option("operations", {
63
- type: "boolean",
64
- default: true,
65
- describe: "Enable vault operations module (Obsidian CLI required at call time)"
66
62
  }).option("obsidian-cli", {
67
63
  type: "string",
68
64
  describe: 'Path to the obsidian CLI binary (default: "obsidian" from PATH)'
@@ -71,9 +67,6 @@ async function parseConfig(argv) {
71
67
  if (rawVaults.length === 0) {
72
68
  throw new Error("--vault is required: provide at least one vault with --vault <path>");
73
69
  }
74
- if (!args.semantic && !args.operations) {
75
- throw new Error("At least one module must be enabled (--semantic or --operations)");
76
- }
77
70
  const vaults = rawVaults.map(buildVaultConfig);
78
71
  const seen = /* @__PURE__ */ new Set();
79
72
  for (const v of vaults) {
@@ -92,10 +85,7 @@ async function parseConfig(argv) {
92
85
  modelKey: DEFAULT_MODEL_KEY,
93
86
  modelId: DEFAULT_MODEL_ID
94
87
  },
95
- operations: {
96
- enabled: args.operations,
97
- binaryPath: args["obsidian-cli"]
98
- }
88
+ obsidianCli: args["obsidian-cli"]
99
89
  };
100
90
  }
101
91
 
@@ -14238,6 +14228,16 @@ var ToolHandlerError = class extends Error {
14238
14228
  }
14239
14229
  }
14240
14230
  };
14231
+ var FATAL_TOOL_ERROR_CODES = /* @__PURE__ */ new Set([
14232
+ "INVALID_ARGUMENT",
14233
+ "INVALID_PARAMS",
14234
+ "INVALID_FILTER",
14235
+ "VAULT_REQUIRED",
14236
+ "VAULT_NOT_FOUND"
14237
+ ]);
14238
+ function isFatalToolError(error48) {
14239
+ return error48 instanceof ToolHandlerError && FATAL_TOOL_ERROR_CODES.has(error48.code);
14240
+ }
14241
14241
  function isPlainRecord(value) {
14242
14242
  return typeof value === "object" && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
14243
14243
  }
@@ -14315,19 +14315,21 @@ function registerTool(tool) {
14315
14315
 
14316
14316
  // src/lib/resolve-vault.ts
14317
14317
  function resolveVault(input, registry2, opts) {
14318
- let entry;
14319
14318
  if (input.vault !== void 0 && input.vault !== "") {
14320
- entry = registry2.require(input.vault);
14321
- } else if (!registry2.isMulti()) {
14322
- entry = registry2.list()[0];
14323
- } else {
14324
- throw new ToolHandlerError(
14325
- "VAULT_REQUIRED",
14326
- `Tool "${opts.tool}" requires a vault: parameter in multi-vault mode`,
14327
- { details: { tool: opts.tool, registered_vaults: registry2.names() } }
14328
- );
14319
+ return registry2.require(input.vault);
14329
14320
  }
14330
- if (opts.requireSemantic && !entry.semanticAvailable) {
14321
+ if (!registry2.isMulti()) {
14322
+ return registry2.list()[0];
14323
+ }
14324
+ throw new ToolHandlerError(
14325
+ "VAULT_REQUIRED",
14326
+ `Tool "${opts.tool}" requires a vault: parameter in multi-vault mode`,
14327
+ { details: { tool: opts.tool, registered_vaults: registry2.names() } }
14328
+ );
14329
+ }
14330
+ function resolveSemanticVault(input, registry2, opts) {
14331
+ const entry = resolveVault(input, registry2, opts);
14332
+ if (!entry.semanticAvailable) {
14331
14333
  throw new ToolHandlerError(
14332
14334
  "SEMANTIC_INDEX_NOT_FOUND",
14333
14335
  `Semantic index for vault "${entry.name}" is unavailable: ${entry.semanticUnavailableReason ?? "unknown reason"}`,
@@ -14511,9 +14513,8 @@ function buildFindDuplicatesTool(deps) {
14511
14513
  ),
14512
14514
  inputSchema,
14513
14515
  handler: async (input) => {
14514
- const entry = resolveVault(input, registry2, {
14515
- tool: "find_duplicates",
14516
- requireSemantic: true
14516
+ const entry = resolveSemanticVault(input, registry2, {
14517
+ tool: "find_duplicates"
14517
14518
  });
14518
14519
  const corpus = entry.corpus;
14519
14520
  const threshold = readThreshold(input.threshold, DEFAULT_DUPLICATE_THRESHOLD, "threshold");
@@ -15612,9 +15613,8 @@ function buildGetSimilarNotesTool(deps) {
15612
15613
  ),
15613
15614
  inputSchema,
15614
15615
  handler: async (input) => {
15615
- const entry = resolveVault(input, registry2, {
15616
- tool: "get_similar_notes",
15617
- requireSemantic: true
15616
+ const entry = resolveSemanticVault(input, registry2, {
15617
+ tool: "get_similar_notes"
15618
15618
  });
15619
15619
  const corpus = entry.corpus;
15620
15620
  const notePath = normalizeNotePath(input.path);
@@ -15696,9 +15696,8 @@ function buildGetStatsTool(deps) {
15696
15696
  ),
15697
15697
  inputSchema,
15698
15698
  handler: async (input) => {
15699
- const entry = resolveVault(input, registry2, {
15700
- tool: "get_stats",
15701
- requireSemantic: true
15699
+ const entry = resolveSemanticVault(input, registry2, {
15700
+ tool: "get_stats"
15702
15701
  });
15703
15702
  const corpus = entry.corpus;
15704
15703
  try {
@@ -15726,20 +15725,73 @@ function buildGetStatsTool(deps) {
15726
15725
  }
15727
15726
 
15728
15727
  // src/lib/fan-out.ts
15728
+ function findFatalRejection(settled) {
15729
+ for (const outcome of settled) {
15730
+ if (outcome.status === "rejected" && isFatalToolError(outcome.reason)) {
15731
+ return outcome.reason;
15732
+ }
15733
+ }
15734
+ return void 0;
15735
+ }
15736
+ function mapRejectionToFailedVault(vault, reason) {
15737
+ if (reason instanceof ToolHandlerError) {
15738
+ return {
15739
+ vault,
15740
+ error: {
15741
+ code: reason.code,
15742
+ message: reason.message,
15743
+ ...reason.details ? { details: reason.details } : {}
15744
+ }
15745
+ };
15746
+ }
15747
+ return {
15748
+ vault,
15749
+ error: {
15750
+ code: "INTERNAL_ERROR",
15751
+ message: reason instanceof Error ? reason.message : String(reason)
15752
+ }
15753
+ };
15754
+ }
15729
15755
  async function runFanOut(registry2, fn) {
15730
15756
  const entries = registry2.list();
15731
- const results = await Promise.all(
15732
- entries.map(async (entry) => ({ vault: entry.name, ...await fn(entry) }))
15733
- );
15734
- return { results_by_vault: results, skipped_vaults: [] };
15757
+ const settled = await Promise.allSettled(entries.map((entry) => fn(entry)));
15758
+ const fatalError = findFatalRejection(settled);
15759
+ if (fatalError) {
15760
+ throw fatalError;
15761
+ }
15762
+ const results = [];
15763
+ const failed = [];
15764
+ for (let i = 0; i < settled.length; i++) {
15765
+ const outcome = settled[i];
15766
+ const vault = entries[i].name;
15767
+ if (outcome.status === "fulfilled") {
15768
+ results.push({ vault, ...outcome.value });
15769
+ } else {
15770
+ failed.push(mapRejectionToFailedVault(vault, outcome.reason));
15771
+ }
15772
+ }
15773
+ return { results_by_vault: results, skipped_vaults: [], failed_vaults: failed };
15735
15774
  }
15736
15775
  async function runSemanticFanOut(registry2, fn) {
15737
15776
  const eligible = registry2.semanticAvailableEntries();
15738
15777
  const skipped = registry2.list().filter((e) => !e.semanticAvailable).map((e) => ({ vault: e.name, reason: "SEMANTIC_INDEX_NOT_FOUND" }));
15739
- const results = await Promise.all(
15740
- eligible.map(async (entry) => ({ vault: entry.name, ...await fn(entry) }))
15741
- );
15742
- return { results_by_vault: results, skipped_vaults: skipped };
15778
+ const settled = await Promise.allSettled(eligible.map((entry) => fn(entry)));
15779
+ const fatalError = findFatalRejection(settled);
15780
+ if (fatalError) {
15781
+ throw fatalError;
15782
+ }
15783
+ const results = [];
15784
+ const failed = [];
15785
+ for (let i = 0; i < settled.length; i++) {
15786
+ const outcome = settled[i];
15787
+ const vault = eligible[i].name;
15788
+ if (outcome.status === "fulfilled") {
15789
+ results.push({ vault, ...outcome.value });
15790
+ } else {
15791
+ failed.push(mapRejectionToFailedVault(vault, outcome.reason));
15792
+ }
15793
+ }
15794
+ return { results_by_vault: results, skipped_vaults: skipped, failed_vaults: failed };
15743
15795
  }
15744
15796
 
15745
15797
  // src/modules/semantic/retrieval-policy.ts
@@ -16179,12 +16231,15 @@ function buildSearchNotesTool(deps) {
16179
16231
  if (input.vault === void 0 && registry2.isMulti()) {
16180
16232
  return await runSemanticFanOut(
16181
16233
  registry2,
16182
- (entry2) => runSearchForEntry(entry2, input, entryDeps)
16234
+ (entry2) => runSearchForEntry(
16235
+ entry2,
16236
+ input,
16237
+ entryDeps
16238
+ )
16183
16239
  );
16184
16240
  }
16185
- const entry = resolveVault(input, registry2, {
16186
- tool: "search_notes",
16187
- requireSemantic: true
16241
+ const entry = resolveSemanticVault(input, registry2, {
16242
+ tool: "search_notes"
16188
16243
  });
16189
16244
  return runSearchForEntry(entry, input, entryDeps);
16190
16245
  }
@@ -16368,9 +16423,6 @@ function buildCreateNoteTool(deps) {
16368
16423
  inputSchema,
16369
16424
  handler: async (input) => {
16370
16425
  const entry = resolveVault(input, registry2, { tool: "create_note" });
16371
- if (!entry.provider) {
16372
- throw invalidArgument("operations module is disabled", "vault");
16373
- }
16374
16426
  if (input.name === void 0 && input.path === void 0) {
16375
16427
  throw invalidArgument("Provide name or path", "name");
16376
16428
  }
@@ -16948,11 +17000,6 @@ function registerResource(resource) {
16948
17000
  // src/modules/operations/resources/vault-overview.ts
16949
17001
  function buildVaultOverviewResource(opts) {
16950
17002
  const { uri, entry } = opts;
16951
- if (!entry.provider) {
16952
- throw new Error(
16953
- `buildVaultOverviewResource: vault "${entry.name}" has no provider \u2014 operations module must be enabled`
16954
- );
16955
- }
16956
17003
  return {
16957
17004
  name: uri === "vault://overview" ? "vault-overview" : `vault-overview-${entry.name}`,
16958
17005
  uri,
@@ -17008,8 +17055,8 @@ var VaultRegistry = class _VaultRegistry {
17008
17055
  const reader = deps.readerFactory({ vaultRoot: v.path });
17009
17056
  const graph = deps.graphFactory({ reader });
17010
17057
  const listMatchingPaths = deps.listMatchingPathsFactory({ reader, graph });
17011
- const writer = config2.operationsEnabled ? deps.writerFactory({ vaultRoot: v.path }) : void 0;
17012
- const provider = config2.operationsEnabled ? deps.providerFactory({ vaultName: v.name, binaryPath: config2.binaryPath }) : void 0;
17058
+ const writer = deps.writerFactory({ vaultRoot: v.path });
17059
+ const provider = deps.providerFactory({ vaultName: v.name, binaryPath: config2.binaryPath });
17013
17060
  let corpus;
17014
17061
  let semanticAvailable = false;
17015
17062
  let semanticUnavailableReason;
@@ -17304,7 +17351,7 @@ var ObsidianCLIProvider = class {
17304
17351
  if (/vault (not found|does not exist)/i.test(stderr)) {
17305
17352
  return new ToolHandlerError(
17306
17353
  "VAULT_NOT_FOUND",
17307
- `Obsidian does not recognize a vault named '${this.vaultName}'. This usually means the vault was renamed in Obsidian's "Manage vaults" UI and the display name no longer matches the directory basename. Re-launch the server with --vault-name <exact-name-in-obsidian>.`,
17354
+ `Obsidian does not recognize a vault named '${this.vaultName}'. The MCP-side alias is derived from the vault directory's basename and must match the display name Obsidian shows in "Manage vaults". Rename either the directory (and re-launch with the matching --vault path) or the vault in Obsidian so the two agree.`,
17308
17355
  { details: { stderr, vaultName: this.vaultName }, cause: error48 }
17309
17356
  );
17310
17357
  }
@@ -17489,7 +17536,7 @@ async function buildServerInstructions(registry2) {
17489
17536
 
17490
17537
  ## Multi-vault mode
17491
17538
 
17492
- This server is registered with multiple vaults: ${names}. Every tool accepts an optional \`vault\` parameter. For broad recall, \`search_notes\`, \`query_notes\`, and \`get_vault_overview\` fan out across all vaults when \`vault\` is omitted; other tools (reads of specific paths, writes, single-vault diagnostics) require an explicit \`vault\` \u2014 omitting it returns \`VAULT_REQUIRED\`. Path-shaped parameters (\`path\`, \`paths\`, \`path_prefix\`) remain vault-relative; vault identity is always carried by \`vault\`.`;
17539
+ This server is registered with multiple vaults: ${names}. Every tool accepts an optional \`vault\` parameter. For broad recall, \`search_notes\`, \`query_notes\`, \`get_vault_overview\`, \`list_tags\`, and \`list_properties\` fan out across all vaults when \`vault\` is omitted; other tools (reads of specific paths, writes, single-vault diagnostics) require an explicit \`vault\` \u2014 omitting it returns \`VAULT_REQUIRED\`. Fan-out responses include \`results_by_vault\`, \`skipped_vaults\` (vaults pre-filtered out, e.g. missing semantic index), and \`failed_vaults\` (per-vault runtime errors with \`{ code, message, details? }\`) \u2014 a single failed vault does not abort the whole call. Path-shaped parameters (\`path\`, \`paths\`, \`path_prefix\`) remain vault-relative; vault identity is always carried by \`vault\`.`;
17493
17540
  }
17494
17541
  for (const entry of registry2.list()) {
17495
17542
  const extra = await readExternalAgentInstructions(entry.path);
@@ -17522,16 +17569,12 @@ function buildDefaultVaultEntryDeps(overrides = {}) {
17522
17569
  };
17523
17570
  }
17524
17571
  async function startNeuroVaultServer(config2, deps = {}) {
17525
- if (!config2.semantic.enabled && !config2.operations.enabled) {
17526
- throw new Error("No modules enabled \u2014 pass --semantic or --operations");
17527
- }
17528
17572
  const registry2 = await VaultRegistry.create(
17529
17573
  {
17530
17574
  vaults: config2.vaults,
17531
- operationsEnabled: config2.operations.enabled,
17532
17575
  semanticEnabled: config2.semantic.enabled,
17533
17576
  modelKey: config2.semantic.modelKey,
17534
- binaryPath: config2.operations.binaryPath
17577
+ binaryPath: config2.obsidianCli
17535
17578
  },
17536
17579
  buildDefaultVaultEntryDeps(deps.vaultEntryDeps)
17537
17580
  );
@@ -17552,15 +17595,9 @@ async function startNeuroVaultServer(config2, deps = {}) {
17552
17595
  toolRegistrations.push(...semantic.tools);
17553
17596
  warmup = semantic.warmup;
17554
17597
  }
17555
- if (config2.operations.enabled) {
17556
- const operations = createOperationsModule(
17557
- registry2,
17558
- { binaryPath: config2.operations.binaryPath },
17559
- deps.operations
17560
- );
17561
- toolRegistrations.push(...operations.tools);
17562
- resourceRegistrations.push(...operations.resources);
17563
- }
17598
+ const operations = createOperationsModule(registry2, {}, deps.operations);
17599
+ toolRegistrations.push(...operations.tools);
17600
+ resourceRegistrations.push(...operations.resources);
17564
17601
  for (const tool of toolRegistrations) {
17565
17602
  server.registerTool(tool.name, tool.spec, tool.handler);
17566
17603
  }