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 +2 -2
- package/dist/cli.js +106 -69
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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
|
[](https://www.npmjs.com/package/neuro-vault-mcp)
|
|
8
8
|
[](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 `
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
15732
|
-
|
|
15733
|
-
)
|
|
15734
|
-
|
|
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
|
|
15740
|
-
|
|
15741
|
-
)
|
|
15742
|
-
|
|
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(
|
|
16234
|
+
(entry2) => runSearchForEntry(
|
|
16235
|
+
entry2,
|
|
16236
|
+
input,
|
|
16237
|
+
entryDeps
|
|
16238
|
+
)
|
|
16183
16239
|
);
|
|
16184
16240
|
}
|
|
16185
|
-
const entry =
|
|
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 =
|
|
17012
|
-
const provider =
|
|
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}'.
|
|
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 \`
|
|
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.
|
|
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
|
-
|
|
17556
|
-
|
|
17557
|
-
|
|
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
|
}
|