neuro-vault-mcp 5.3.0 → 5.5.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 +7 -1
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +226 -67
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -126,7 +126,7 @@ User guide lives in [`docs/guide/`](./docs/guide/README.md):
|
|
|
126
126
|
|
|
127
127
|
- [Installation](./docs/guide/installation.md)
|
|
128
128
|
- [Semantic Search](./docs/guide/semantic-search.md) — `search_notes`, `get_similar_notes`, `find_duplicates`, `get_stats`
|
|
129
|
-
- [Vault Operations](./docs/guide/vault-operations.md) — note CRUD, daily notes, properties, tags, structured queries (`query_notes`)
|
|
129
|
+
- [Vault Operations](./docs/guide/vault-operations.md) — note CRUD, daily notes, properties, tags, structured queries (`query_notes`), vault snapshot (`get_vault_overview`)
|
|
130
130
|
- [Routing Between Tools](./docs/guide/routing.md)
|
|
131
131
|
- [Configuration](./docs/guide/configuration.md) — CLI args, troubleshooting, limitations, **migration to 2.0**, development
|
|
132
132
|
|
|
@@ -134,6 +134,12 @@ Architecture / internals: [`docs/architecture/`](./docs/architecture/).
|
|
|
134
134
|
|
|
135
135
|
---
|
|
136
136
|
|
|
137
|
+
### Vault-specific conventions for external agents
|
|
138
|
+
|
|
139
|
+
When the server starts, it looks for `<vault>/.neuro-vault/for-external-agents.md`. If the file exists, its content is appended to the MCP `instructions` that clients receive at `initialize`, under a `## Vault-specific conventions` section. Use this file to teach external agents vault-specific rules that cannot be derived from the snapshot — for example, closed sets of frontmatter `type` values, or folders that are off-limits for writes. The file is optional; without it the server still ships sane defaults plus a pointer to `get_vault_overview`.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
137
143
|
## 📄 License
|
|
138
144
|
|
|
139
145
|
ISC — see [LICENSE](LICENSE).
|
package/dist/cli.d.ts
CHANGED
|
@@ -242,11 +242,11 @@ interface OperationsModuleDeps {
|
|
|
242
242
|
graph?: WikilinkGraphIndex;
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
-
type ToolServer = Pick<McpServer, 'registerTool' | 'connect'>;
|
|
245
|
+
type ToolServer = Pick<McpServer, 'registerTool' | 'registerResource' | 'connect'>;
|
|
246
246
|
interface NeuroVaultStartupDependencies {
|
|
247
247
|
semantic?: SemanticModuleDeps;
|
|
248
248
|
operations?: OperationsModuleDeps;
|
|
249
|
-
serverFactory?: () => ToolServer;
|
|
249
|
+
serverFactory?: (instructions: string) => ToolServer;
|
|
250
250
|
transportFactory?: () => StdioServerTransport;
|
|
251
251
|
}
|
|
252
252
|
|
package/dist/cli.js
CHANGED
|
@@ -33,6 +33,9 @@ async function parseConfig(argv) {
|
|
|
33
33
|
}).option("obsidian-cli", {
|
|
34
34
|
type: "string",
|
|
35
35
|
describe: 'Path to the obsidian CLI binary (default: "obsidian" from PATH)'
|
|
36
|
+
}).option("vault-name", {
|
|
37
|
+
type: "string",
|
|
38
|
+
describe: `Override the Obsidian vault name used in CLI invocations. Defaults to the basename of --vault. Set this only if you renamed the vault in Obsidian's "Manage vaults" UI and the display name differs from the directory name.`
|
|
36
39
|
}).strict().help().version(false).parse();
|
|
37
40
|
if (!path.isAbsolute(args.vault)) {
|
|
38
41
|
throw new Error("--vault must be an absolute path");
|
|
@@ -40,6 +43,9 @@ async function parseConfig(argv) {
|
|
|
40
43
|
if (!args.semantic && !args.operations) {
|
|
41
44
|
throw new Error("At least one module must be enabled (--semantic or --operations)");
|
|
42
45
|
}
|
|
46
|
+
if (args["vault-name"] !== void 0 && args["vault-name"].trim() === "") {
|
|
47
|
+
throw new Error("--vault-name must not be empty");
|
|
48
|
+
}
|
|
43
49
|
const normalizedVaultPath = path.resolve(args.vault);
|
|
44
50
|
return {
|
|
45
51
|
vaultPath: normalizedVaultPath,
|
|
@@ -51,13 +57,16 @@ async function parseConfig(argv) {
|
|
|
51
57
|
},
|
|
52
58
|
operations: {
|
|
53
59
|
enabled: args.operations,
|
|
54
|
-
binaryPath: args["obsidian-cli"]
|
|
60
|
+
binaryPath: args["obsidian-cli"],
|
|
61
|
+
vaultName: args["vault-name"]?.trim() ?? path.basename(normalizedVaultPath)
|
|
55
62
|
}
|
|
56
63
|
};
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
// src/server.ts
|
|
60
67
|
import { createRequire } from "module";
|
|
68
|
+
import fs4 from "fs/promises";
|
|
69
|
+
import path9 from "path";
|
|
61
70
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
62
71
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
63
72
|
|
|
@@ -321,9 +330,9 @@ function buildBasenameIndex(paths) {
|
|
|
321
330
|
resolveAll
|
|
322
331
|
};
|
|
323
332
|
}
|
|
324
|
-
function baseOf(
|
|
325
|
-
const slash =
|
|
326
|
-
const tail = slash >= 0 ?
|
|
333
|
+
function baseOf(path10) {
|
|
334
|
+
const slash = path10.lastIndexOf("/");
|
|
335
|
+
const tail = slash >= 0 ? path10.slice(slash + 1) : path10;
|
|
327
336
|
return tail.endsWith(".md") ? tail.slice(0, -3) : tail;
|
|
328
337
|
}
|
|
329
338
|
|
|
@@ -1716,10 +1725,10 @@ function mergeDefs(...defs) {
|
|
|
1716
1725
|
function cloneDef(schema) {
|
|
1717
1726
|
return mergeDefs(schema._zod.def);
|
|
1718
1727
|
}
|
|
1719
|
-
function getElementAtPath(obj,
|
|
1720
|
-
if (!
|
|
1728
|
+
function getElementAtPath(obj, path10) {
|
|
1729
|
+
if (!path10)
|
|
1721
1730
|
return obj;
|
|
1722
|
-
return
|
|
1731
|
+
return path10.reduce((acc, key) => acc?.[key], obj);
|
|
1723
1732
|
}
|
|
1724
1733
|
function promiseAllObject(promisesObj) {
|
|
1725
1734
|
const keys = Object.keys(promisesObj);
|
|
@@ -2102,11 +2111,11 @@ function aborted(x, startIndex = 0) {
|
|
|
2102
2111
|
}
|
|
2103
2112
|
return false;
|
|
2104
2113
|
}
|
|
2105
|
-
function prefixIssues(
|
|
2114
|
+
function prefixIssues(path10, issues) {
|
|
2106
2115
|
return issues.map((iss) => {
|
|
2107
2116
|
var _a2;
|
|
2108
2117
|
(_a2 = iss).path ?? (_a2.path = []);
|
|
2109
|
-
iss.path.unshift(
|
|
2118
|
+
iss.path.unshift(path10);
|
|
2110
2119
|
return iss;
|
|
2111
2120
|
});
|
|
2112
2121
|
}
|
|
@@ -2289,7 +2298,7 @@ function formatError(error48, mapper = (issue2) => issue2.message) {
|
|
|
2289
2298
|
}
|
|
2290
2299
|
function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
2291
2300
|
const result = { errors: [] };
|
|
2292
|
-
const processError = (error49,
|
|
2301
|
+
const processError = (error49, path10 = []) => {
|
|
2293
2302
|
var _a2, _b;
|
|
2294
2303
|
for (const issue2 of error49.issues) {
|
|
2295
2304
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
@@ -2299,7 +2308,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
|
2299
2308
|
} else if (issue2.code === "invalid_element") {
|
|
2300
2309
|
processError({ issues: issue2.issues }, issue2.path);
|
|
2301
2310
|
} else {
|
|
2302
|
-
const fullpath = [...
|
|
2311
|
+
const fullpath = [...path10, ...issue2.path];
|
|
2303
2312
|
if (fullpath.length === 0) {
|
|
2304
2313
|
result.errors.push(mapper(issue2));
|
|
2305
2314
|
continue;
|
|
@@ -2331,8 +2340,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
|
2331
2340
|
}
|
|
2332
2341
|
function toDotPath(_path) {
|
|
2333
2342
|
const segs = [];
|
|
2334
|
-
const
|
|
2335
|
-
for (const seg of
|
|
2343
|
+
const path10 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
2344
|
+
for (const seg of path10) {
|
|
2336
2345
|
if (typeof seg === "number")
|
|
2337
2346
|
segs.push(`[${seg}]`);
|
|
2338
2347
|
else if (typeof seg === "symbol")
|
|
@@ -14309,13 +14318,13 @@ function resolveRef(ref, ctx) {
|
|
|
14309
14318
|
if (!ref.startsWith("#")) {
|
|
14310
14319
|
throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
|
|
14311
14320
|
}
|
|
14312
|
-
const
|
|
14313
|
-
if (
|
|
14321
|
+
const path10 = ref.slice(1).split("/").filter(Boolean);
|
|
14322
|
+
if (path10.length === 0) {
|
|
14314
14323
|
return ctx.rootSchema;
|
|
14315
14324
|
}
|
|
14316
14325
|
const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
|
|
14317
|
-
if (
|
|
14318
|
-
const key =
|
|
14326
|
+
if (path10[0] === defsKey) {
|
|
14327
|
+
const key = path10[1];
|
|
14319
14328
|
if (!key || !ctx.defs[key]) {
|
|
14320
14329
|
throw new Error(`Reference not found: ${ref}`);
|
|
14321
14330
|
}
|
|
@@ -14910,9 +14919,9 @@ async function invokeTool(handler) {
|
|
|
14910
14919
|
// src/lib/tool-registry.ts
|
|
14911
14920
|
function formatZodError(error48) {
|
|
14912
14921
|
const issues = error48.issues.map((issue2) => {
|
|
14913
|
-
const
|
|
14922
|
+
const path10 = issue2.path.length > 0 ? issue2.path.join(".") : "<root>";
|
|
14914
14923
|
const expected = issue2.expected;
|
|
14915
|
-
const formatted = { path:
|
|
14924
|
+
const formatted = { path: path10, message: issue2.message };
|
|
14916
14925
|
if (typeof expected === "string") formatted.expected = expected;
|
|
14917
14926
|
return formatted;
|
|
14918
14927
|
});
|
|
@@ -15079,10 +15088,10 @@ async function getNoteLinks(opts) {
|
|
|
15079
15088
|
for (const target of rawTargets) {
|
|
15080
15089
|
const normalized = normalizeWikilinkTarget(target);
|
|
15081
15090
|
if (!normalized) continue;
|
|
15082
|
-
const
|
|
15083
|
-
if (!
|
|
15084
|
-
if (
|
|
15085
|
-
resolved.add(
|
|
15091
|
+
const path10 = basenameIndex.resolve(normalized);
|
|
15092
|
+
if (!path10) continue;
|
|
15093
|
+
if (path10 === notePath) continue;
|
|
15094
|
+
resolved.add(path10);
|
|
15086
15095
|
}
|
|
15087
15096
|
return resolved;
|
|
15088
15097
|
}
|
|
@@ -15686,7 +15695,7 @@ function computeExpansion(args) {
|
|
|
15686
15695
|
}
|
|
15687
15696
|
}
|
|
15688
15697
|
}
|
|
15689
|
-
return [...bestByPath.entries()].map(([
|
|
15698
|
+
return [...bestByPath.entries()].map(([path10, similarity]) => ({ path: path10, similarity, via_expansion: true })).sort((a, b) => b.similarity - a.similarity || a.path.localeCompare(b.path)).slice(0, totalLimit);
|
|
15690
15699
|
}
|
|
15691
15700
|
async function executeRetrieval(input) {
|
|
15692
15701
|
const { query, mode, sources, embeddingProvider, searchEngine } = input;
|
|
@@ -15944,9 +15953,9 @@ function isFilterEmpty(filter) {
|
|
|
15944
15953
|
}
|
|
15945
15954
|
function narrowSources(sources, allowed) {
|
|
15946
15955
|
const out = /* @__PURE__ */ new Map();
|
|
15947
|
-
for (const
|
|
15948
|
-
const src = sources.get(
|
|
15949
|
-
if (src) out.set(
|
|
15956
|
+
for (const path10 of allowed) {
|
|
15957
|
+
const src = sources.get(path10);
|
|
15958
|
+
if (src) out.set(path10, src);
|
|
15950
15959
|
}
|
|
15951
15960
|
return out;
|
|
15952
15961
|
}
|
|
@@ -16183,10 +16192,10 @@ var ObsidianCLIProvider = class {
|
|
|
16183
16192
|
}
|
|
16184
16193
|
async readDaily() {
|
|
16185
16194
|
const { stdout: pathStdout } = await this.runCommand("daily:path", []);
|
|
16186
|
-
const
|
|
16195
|
+
const path10 = pathStdout.trim();
|
|
16187
16196
|
const { stdout } = await this.runCommand("daily:read", []);
|
|
16188
16197
|
const { frontmatter, content } = splitFrontmatter(stdout);
|
|
16189
|
-
return { path:
|
|
16198
|
+
return { path: path10, frontmatter, content };
|
|
16190
16199
|
}
|
|
16191
16200
|
async setProperty(input) {
|
|
16192
16201
|
const tokens = [`name=${input.name}`, `value=${this.serializeValue(input.value)}`];
|
|
@@ -16301,6 +16310,13 @@ var ObsidianCLIProvider = class {
|
|
|
16301
16310
|
{ details: { stderr, command }, cause: error48 }
|
|
16302
16311
|
);
|
|
16303
16312
|
}
|
|
16313
|
+
if (/vault (not found|does not exist)/i.test(stderr)) {
|
|
16314
|
+
return new ToolHandlerError(
|
|
16315
|
+
"VAULT_NOT_FOUND",
|
|
16316
|
+
`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>.`,
|
|
16317
|
+
{ details: { stderr, vaultName: this.vaultName }, cause: error48 }
|
|
16318
|
+
);
|
|
16319
|
+
}
|
|
16304
16320
|
if (/not found/i.test(stderr)) {
|
|
16305
16321
|
return new ToolHandlerError("NOT_FOUND", `Note not found: ${stderr.trim() || "unknown"}`, {
|
|
16306
16322
|
details: { stderr, command },
|
|
@@ -16512,18 +16528,18 @@ function buildEditNoteTool(deps) {
|
|
|
16512
16528
|
input.name === void 0 ? "name" : "path"
|
|
16513
16529
|
);
|
|
16514
16530
|
}
|
|
16515
|
-
const
|
|
16531
|
+
const path10 = await resolveToPath(input, reader);
|
|
16516
16532
|
if (input.replace !== void 0) {
|
|
16517
16533
|
if (input.replace === "") {
|
|
16518
16534
|
throw invalidArgument("replace must not be empty", "replace");
|
|
16519
16535
|
}
|
|
16520
16536
|
return writer.replaceInNote({
|
|
16521
|
-
path:
|
|
16537
|
+
path: path10,
|
|
16522
16538
|
find: input.replace,
|
|
16523
16539
|
content: input.content
|
|
16524
16540
|
});
|
|
16525
16541
|
}
|
|
16526
|
-
return writer.replaceFullBody({ path:
|
|
16542
|
+
return writer.replaceFullBody({ path: path10, content: input.content });
|
|
16527
16543
|
}
|
|
16528
16544
|
};
|
|
16529
16545
|
}
|
|
@@ -16574,22 +16590,91 @@ function buildGetNoteLinksTool(deps) {
|
|
|
16574
16590
|
description: DESCRIPTION,
|
|
16575
16591
|
inputSchema: inputSchema7,
|
|
16576
16592
|
handler: async (input) => {
|
|
16577
|
-
const
|
|
16593
|
+
const path10 = normalizePath(input.path);
|
|
16578
16594
|
await graph.ensureFresh();
|
|
16579
|
-
return graph.getNoteLinks(
|
|
16595
|
+
return graph.getNoteLinks(path10);
|
|
16580
16596
|
}
|
|
16581
16597
|
};
|
|
16582
16598
|
}
|
|
16583
16599
|
|
|
16584
|
-
// src/
|
|
16600
|
+
// src/lib/obsidian/top-by-backlinks.ts
|
|
16601
|
+
import path7 from "path";
|
|
16602
|
+
function topByBacklinks(input) {
|
|
16603
|
+
const { paths, graph, limit } = input;
|
|
16604
|
+
return paths.map((p) => ({
|
|
16605
|
+
path: p,
|
|
16606
|
+
title: basenameWithoutExt(p),
|
|
16607
|
+
backlink_count: graph.getBacklinkCount(p)
|
|
16608
|
+
})).sort(
|
|
16609
|
+
(a, b) => b.backlink_count !== a.backlink_count ? b.backlink_count - a.backlink_count : a.path.localeCompare(b.path)
|
|
16610
|
+
).slice(0, limit);
|
|
16611
|
+
}
|
|
16612
|
+
function basenameWithoutExt(notePath) {
|
|
16613
|
+
const base = path7.posix.basename(notePath);
|
|
16614
|
+
return base.endsWith(".md") ? base.slice(0, -3) : base;
|
|
16615
|
+
}
|
|
16616
|
+
|
|
16617
|
+
// src/lib/obsidian/vault-overview.ts
|
|
16618
|
+
var TOP_TAGS_LIMIT = 30;
|
|
16619
|
+
var TOP_PROPERTIES_LIMIT = 30;
|
|
16620
|
+
var TOP_BACKLINKS_LIMIT = 10;
|
|
16621
|
+
var ROOT_FOLDER_SENTINEL = "/";
|
|
16622
|
+
async function computeVaultOverview(deps) {
|
|
16623
|
+
const { reader, provider, graph } = deps;
|
|
16624
|
+
await graph.ensureFresh();
|
|
16625
|
+
const paths = await reader.scan();
|
|
16626
|
+
const [tags, props] = await Promise.all([provider.listTags(), provider.listProperties()]);
|
|
16627
|
+
if (paths.length === 0) {
|
|
16628
|
+
return {
|
|
16629
|
+
total_notes: 0,
|
|
16630
|
+
folders: [],
|
|
16631
|
+
top_tags: tags.slice(0, TOP_TAGS_LIMIT),
|
|
16632
|
+
properties: props.slice(0, TOP_PROPERTIES_LIMIT),
|
|
16633
|
+
top_by_backlinks: []
|
|
16634
|
+
};
|
|
16635
|
+
}
|
|
16636
|
+
const folders = /* @__PURE__ */ new Map();
|
|
16637
|
+
for (const p of paths) {
|
|
16638
|
+
const folder = topLevelFolder(p);
|
|
16639
|
+
folders.set(folder, (folders.get(folder) ?? 0) + 1);
|
|
16640
|
+
}
|
|
16641
|
+
return {
|
|
16642
|
+
total_notes: paths.length,
|
|
16643
|
+
folders: sortFolders(folders),
|
|
16644
|
+
top_tags: tags.slice(0, TOP_TAGS_LIMIT),
|
|
16645
|
+
properties: props.slice(0, TOP_PROPERTIES_LIMIT),
|
|
16646
|
+
top_by_backlinks: topByBacklinks({ paths, graph, limit: TOP_BACKLINKS_LIMIT })
|
|
16647
|
+
};
|
|
16648
|
+
}
|
|
16649
|
+
function topLevelFolder(notePath) {
|
|
16650
|
+
const ix = notePath.indexOf("/");
|
|
16651
|
+
return ix === -1 ? ROOT_FOLDER_SENTINEL : notePath.slice(0, ix);
|
|
16652
|
+
}
|
|
16653
|
+
function sortFolders(map2) {
|
|
16654
|
+
return [...map2.entries()].map(([path10, count]) => ({ path: path10, count })).sort((a, b) => b.count !== a.count ? b.count - a.count : a.path.localeCompare(b.path));
|
|
16655
|
+
}
|
|
16656
|
+
|
|
16657
|
+
// src/modules/operations/tools/get-vault-overview.ts
|
|
16585
16658
|
var inputSchema8 = external_exports.object({});
|
|
16659
|
+
function buildGetVaultOverviewTool(deps) {
|
|
16660
|
+
return {
|
|
16661
|
+
name: "get_vault_overview",
|
|
16662
|
+
title: "Get Vault Overview",
|
|
16663
|
+
description: "Returns a single snapshot of vault structure: top-level folders with note counts, top tags, frontmatter properties, total note count, and the top 10 notes by inbound wikilinks. Call this once at the start of a session to orient yourself before reaching for `list_tags`, `list_properties`, or exploratory `query_notes`.",
|
|
16664
|
+
inputSchema: inputSchema8,
|
|
16665
|
+
handler: async (_input) => computeVaultOverview(deps)
|
|
16666
|
+
};
|
|
16667
|
+
}
|
|
16668
|
+
|
|
16669
|
+
// src/modules/operations/tools/list-properties.ts
|
|
16670
|
+
var inputSchema9 = external_exports.object({});
|
|
16586
16671
|
function buildListPropertiesTool(deps) {
|
|
16587
16672
|
const { provider } = deps;
|
|
16588
16673
|
return {
|
|
16589
16674
|
name: "list_properties",
|
|
16590
16675
|
title: "List Properties",
|
|
16591
16676
|
description: "List all frontmatter properties used across the vault, sorted by occurrence count desc. Returns `[{name, count}]`. Useful for understanding the vault's metadata ontology.",
|
|
16592
|
-
inputSchema:
|
|
16677
|
+
inputSchema: inputSchema9,
|
|
16593
16678
|
handler: async (_input) => {
|
|
16594
16679
|
return provider.listProperties();
|
|
16595
16680
|
}
|
|
@@ -16597,14 +16682,14 @@ function buildListPropertiesTool(deps) {
|
|
|
16597
16682
|
}
|
|
16598
16683
|
|
|
16599
16684
|
// src/modules/operations/tools/list-tags.ts
|
|
16600
|
-
var
|
|
16685
|
+
var inputSchema10 = external_exports.object({});
|
|
16601
16686
|
function buildListTagsTool(deps) {
|
|
16602
16687
|
const { provider } = deps;
|
|
16603
16688
|
return {
|
|
16604
16689
|
name: "list_tags",
|
|
16605
16690
|
title: "List Tags",
|
|
16606
16691
|
description: "List all tags used across the vault, sorted by occurrence count desc. Returns `[{name, count}]`.",
|
|
16607
|
-
inputSchema:
|
|
16692
|
+
inputSchema: inputSchema10,
|
|
16608
16693
|
handler: async (_input) => {
|
|
16609
16694
|
return provider.listTags();
|
|
16610
16695
|
}
|
|
@@ -16616,7 +16701,7 @@ var queryNotesSortSchema = external_exports.object({
|
|
|
16616
16701
|
field: external_exports.string().min(1),
|
|
16617
16702
|
order: external_exports.enum(["asc", "desc"])
|
|
16618
16703
|
});
|
|
16619
|
-
var
|
|
16704
|
+
var inputSchema11 = external_exports.object({
|
|
16620
16705
|
filter: external_exports.record(external_exports.string(), external_exports.unknown()),
|
|
16621
16706
|
path_prefix: external_exports.string().optional(),
|
|
16622
16707
|
sort: queryNotesSortSchema.optional(),
|
|
@@ -16629,7 +16714,7 @@ function buildQueryNotesTool(deps) {
|
|
|
16629
16714
|
name: "query_notes",
|
|
16630
16715
|
title: "Query Notes",
|
|
16631
16716
|
description: 'Run a structured MongoDB-style query against the vault\'s frontmatter, tags, and wikilink graph. `filter` is a sift/MongoDB filter object evaluated against `NoteRecord` shape `{ path, frontmatter, tags, backlink_count }` \u2014 `tags` is an array of strings (no leading `#`) extracted from the `tags:` frontmatter field; `backlink_count` is the number of vault-wide wikilinks (and `![[embeds]]`) that point at the note. Reference frontmatter keys with the dotted prefix `frontmatter.<key>`. Supported operators: `$eq`, `$ne`, `$in`, `$nin`, `$gt`, `$gte`, `$lt`, `$lte`, `$exists`, `$regex`, `$options`, `$and`, `$or`, `$nor`, `$not`. `$regex` is case-insensitive by default; pass `$options` (e.g. `\'\'` for case-sensitive, `\'m\'` for multiline-only) to override. Optional `path_prefix` restricts the scan to a vault subtree (vault-relative POSIX, no leading slash). Optional `sort` is `{ field, order }` \u2014 `field` must be `"path"`, `"backlink_count"`, or start with `"frontmatter."`. Optional `limit` defaults to 100, max 1000. Optional `include_content` (default false) \u2014 when true, each result also carries `content` (note body). Returns `{ results, count, truncated }`; each result item carries `backlink_count`. `truncated` is true when more notes matched than `limit` allowed. Reads directly from disk and does not require Obsidian to be running.',
|
|
16632
|
-
inputSchema:
|
|
16717
|
+
inputSchema: inputSchema11,
|
|
16633
16718
|
handler: async (input) => {
|
|
16634
16719
|
return runQueryNotes(input, reader, graph);
|
|
16635
16720
|
}
|
|
@@ -16639,14 +16724,14 @@ function buildQueryNotesTool(deps) {
|
|
|
16639
16724
|
// src/modules/operations/tools/read-daily.ts
|
|
16640
16725
|
var NOTES_TODAY_CAP = 200;
|
|
16641
16726
|
var DAILY_BASENAME_RE = /(\d{4}-\d{2}-\d{2})\.md$/;
|
|
16642
|
-
var
|
|
16727
|
+
var inputSchema12 = external_exports.object({});
|
|
16643
16728
|
function buildReadDailyTool(deps) {
|
|
16644
16729
|
const { provider, reader, graph } = deps;
|
|
16645
16730
|
return {
|
|
16646
16731
|
name: "read_daily",
|
|
16647
16732
|
title: "Read Daily",
|
|
16648
16733
|
description: 'Read today\'s daily note. Returns `{ path, frontmatter, content, notes_today }` where `frontmatter` is the parsed YAML object (or `null` if absent/malformed), `content` is the body without the YAML block, and `notes_today` lists vault notes created today (matched by `frontmatter.created`) excluding daily notes themselves \u2014 metadata only, sorted by path ascending, capped at 200 entries. Useful for "what\'s on my agenda?" / "what happened today?" questions without a separate `query_notes` call.',
|
|
16649
|
-
inputSchema:
|
|
16734
|
+
inputSchema: inputSchema12,
|
|
16650
16735
|
handler: async (_input) => {
|
|
16651
16736
|
const daily = await provider.readDaily();
|
|
16652
16737
|
const today = deriveToday(daily.path);
|
|
@@ -16690,7 +16775,7 @@ function formatLocalDate(d) {
|
|
|
16690
16775
|
|
|
16691
16776
|
// src/modules/operations/tools/read-notes.ts
|
|
16692
16777
|
var readNotesFieldSchema = external_exports.enum(["frontmatter", "content"]);
|
|
16693
|
-
var
|
|
16778
|
+
var inputSchema13 = external_exports.object({
|
|
16694
16779
|
paths: external_exports.union([external_exports.string().min(1), external_exports.array(external_exports.string()).min(1).max(50)]),
|
|
16695
16780
|
fields: external_exports.array(readNotesFieldSchema).min(1).optional()
|
|
16696
16781
|
});
|
|
@@ -16700,7 +16785,7 @@ function buildReadNotesTool(deps) {
|
|
|
16700
16785
|
name: "read_notes",
|
|
16701
16786
|
title: "Read Notes",
|
|
16702
16787
|
description: "Read one or more notes in one call. `paths` is a vault-relative POSIX path string or an array of 1\u201350 such paths; duplicates are de-duplicated and results returned in input order. `fields` projects which parts of each note to return \u2014 choose from `frontmatter` and `content`; default `['frontmatter','content']`. One missing or unreadable path does not fail the others \u2014 per-item errors come back inline. A single MCP roundtrip with parallel disk reads. Reads are direct from disk and do not require Obsidian to be running.",
|
|
16703
|
-
inputSchema:
|
|
16788
|
+
inputSchema: inputSchema13,
|
|
16704
16789
|
handler: async (input) => {
|
|
16705
16790
|
const { paths, fields } = validateReadNotesInput(input);
|
|
16706
16791
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -16752,7 +16837,7 @@ function buildReadNotesTool(deps) {
|
|
|
16752
16837
|
}
|
|
16753
16838
|
|
|
16754
16839
|
// src/modules/operations/tools/read-property.ts
|
|
16755
|
-
var
|
|
16840
|
+
var inputSchema14 = external_exports.object({
|
|
16756
16841
|
name: external_exports.string().optional(),
|
|
16757
16842
|
path: external_exports.string().optional(),
|
|
16758
16843
|
key: external_exports.string()
|
|
@@ -16763,7 +16848,7 @@ function buildReadPropertyTool(deps) {
|
|
|
16763
16848
|
name: "read_property",
|
|
16764
16849
|
title: "Read Property",
|
|
16765
16850
|
description: "Read a frontmatter property value from a note. Provide `name` or `path`, plus `key`. Returns `{ value }`. Use `read_notes` if you need the full frontmatter or accurate type information.",
|
|
16766
|
-
inputSchema:
|
|
16851
|
+
inputSchema: inputSchema14,
|
|
16767
16852
|
handler: async (input) => {
|
|
16768
16853
|
const identifier = resolveIdentifier(input.name, input.path);
|
|
16769
16854
|
if (!input.key || input.key.trim() === "") {
|
|
@@ -16775,7 +16860,7 @@ function buildReadPropertyTool(deps) {
|
|
|
16775
16860
|
}
|
|
16776
16861
|
|
|
16777
16862
|
// src/modules/operations/tools/remove-property.ts
|
|
16778
|
-
var
|
|
16863
|
+
var inputSchema15 = external_exports.object({
|
|
16779
16864
|
name: external_exports.string().optional(),
|
|
16780
16865
|
path: external_exports.string().optional(),
|
|
16781
16866
|
key: external_exports.string()
|
|
@@ -16786,7 +16871,7 @@ function buildRemovePropertyTool(deps) {
|
|
|
16786
16871
|
name: "remove_property",
|
|
16787
16872
|
title: "Remove Property",
|
|
16788
16873
|
description: "Remove a frontmatter property from a note. Provide `name` or `path`, plus `key`. Idempotent \u2014 succeeds whether or not the property existed.",
|
|
16789
|
-
inputSchema:
|
|
16874
|
+
inputSchema: inputSchema15,
|
|
16790
16875
|
handler: async (input) => {
|
|
16791
16876
|
const identifier = resolveIdentifier(input.name, input.path);
|
|
16792
16877
|
if (!input.key || input.key.trim() === "") {
|
|
@@ -16799,7 +16884,7 @@ function buildRemovePropertyTool(deps) {
|
|
|
16799
16884
|
}
|
|
16800
16885
|
|
|
16801
16886
|
// src/modules/operations/tools/set-property.ts
|
|
16802
|
-
var
|
|
16887
|
+
var inputSchema16 = external_exports.object({
|
|
16803
16888
|
name: external_exports.string().optional(),
|
|
16804
16889
|
path: external_exports.string().optional(),
|
|
16805
16890
|
key: external_exports.string(),
|
|
@@ -16812,7 +16897,7 @@ function buildSetPropertyTool(deps) {
|
|
|
16812
16897
|
name: "set_property",
|
|
16813
16898
|
title: "Set Property",
|
|
16814
16899
|
description: "Set a frontmatter property on a note. Provide either `name` (wikilink-style) or `path` (vault-relative). `key` is the frontmatter property name (e.g. `status`, `due`). `value` may be string/number/boolean/array \u2014 `type` is inferred from the JS type unless given. For `date`/`datetime` you MUST pass `type` explicitly AND use ISO format (`YYYY-MM-DD` for date, `YYYY-MM-DDTHH:mm:ss[.sss][Z|\xB1HH:mm]` for datetime) \u2014 non-ISO values are silently dropped by obsidian-cli, so this tool rejects them up front. List items must not contain commas (obsidian-cli limitation). Existing properties are overwritten.",
|
|
16815
|
-
inputSchema:
|
|
16900
|
+
inputSchema: inputSchema16,
|
|
16816
16901
|
handler: async (input) => {
|
|
16817
16902
|
const identifier = resolveIdentifier(input.name, input.path);
|
|
16818
16903
|
if (!input.key || input.key.trim() === "") {
|
|
@@ -16838,13 +16923,58 @@ function buildOperationsTools(deps) {
|
|
|
16838
16923
|
registerTool(buildRemovePropertyTool(deps)),
|
|
16839
16924
|
registerTool(buildListPropertiesTool(deps)),
|
|
16840
16925
|
registerTool(buildListTagsTool(deps)),
|
|
16841
|
-
registerTool(buildGetNoteLinksTool(deps))
|
|
16926
|
+
registerTool(buildGetNoteLinksTool(deps)),
|
|
16927
|
+
registerTool(buildGetVaultOverviewTool(deps))
|
|
16842
16928
|
];
|
|
16843
16929
|
}
|
|
16844
16930
|
|
|
16931
|
+
// src/lib/resource-registry.ts
|
|
16932
|
+
function registerResource(resource) {
|
|
16933
|
+
const mimeType = resource.mimeType ?? "application/json";
|
|
16934
|
+
return {
|
|
16935
|
+
name: resource.name,
|
|
16936
|
+
uri: resource.uri,
|
|
16937
|
+
metadata: {
|
|
16938
|
+
...resource.title !== void 0 ? { title: resource.title } : {},
|
|
16939
|
+
description: resource.description,
|
|
16940
|
+
mimeType
|
|
16941
|
+
},
|
|
16942
|
+
handler: async (uri) => {
|
|
16943
|
+
const payload = await resource.handler(uri);
|
|
16944
|
+
const text = typeof payload === "string" ? payload : JSON.stringify(payload);
|
|
16945
|
+
return {
|
|
16946
|
+
contents: [
|
|
16947
|
+
{
|
|
16948
|
+
uri: resource.uri,
|
|
16949
|
+
mimeType,
|
|
16950
|
+
text
|
|
16951
|
+
}
|
|
16952
|
+
]
|
|
16953
|
+
};
|
|
16954
|
+
}
|
|
16955
|
+
};
|
|
16956
|
+
}
|
|
16957
|
+
|
|
16958
|
+
// src/modules/operations/resources/vault-overview.ts
|
|
16959
|
+
function buildVaultOverviewResource(deps) {
|
|
16960
|
+
return {
|
|
16961
|
+
name: "vault-overview",
|
|
16962
|
+
uri: "vault://overview",
|
|
16963
|
+
title: "Vault Overview",
|
|
16964
|
+
description: "Snapshot of vault structure (folders, tags, properties, top-10 notes by backlinks). Same payload as the `get_vault_overview` tool; exposed as a resource so clients that auto-load resources can pull it into context without an explicit tool call.",
|
|
16965
|
+
mimeType: "application/json",
|
|
16966
|
+
handler: async () => computeVaultOverview(deps)
|
|
16967
|
+
};
|
|
16968
|
+
}
|
|
16969
|
+
|
|
16970
|
+
// src/modules/operations/resources/index.ts
|
|
16971
|
+
function buildOperationsResources(deps) {
|
|
16972
|
+
return [registerResource(buildVaultOverviewResource(deps))];
|
|
16973
|
+
}
|
|
16974
|
+
|
|
16845
16975
|
// src/lib/obsidian/vault-writer.ts
|
|
16846
16976
|
import { readFile as fsReadFile2, writeFile as fsWriteFile } from "fs/promises";
|
|
16847
|
-
import
|
|
16977
|
+
import path8 from "path";
|
|
16848
16978
|
var FsVaultWriter = class {
|
|
16849
16979
|
vaultRoot;
|
|
16850
16980
|
readFile;
|
|
@@ -16855,7 +16985,7 @@ var FsVaultWriter = class {
|
|
|
16855
16985
|
this.writeFile = opts.writeFile ?? ((p, d, enc) => fsWriteFile(p, d, enc));
|
|
16856
16986
|
}
|
|
16857
16987
|
async replaceInNote(input) {
|
|
16858
|
-
const absPath =
|
|
16988
|
+
const absPath = path8.join(this.vaultRoot, input.path);
|
|
16859
16989
|
const raw = await this.readRaw(absPath, input.path);
|
|
16860
16990
|
const { prefix, body } = splitRawFrontmatter(raw);
|
|
16861
16991
|
const result = applyReplace(body, input.find, input.content);
|
|
@@ -16874,7 +17004,7 @@ var FsVaultWriter = class {
|
|
|
16874
17004
|
await this.writeFile(absPath, prefix + result.body, "utf8");
|
|
16875
17005
|
}
|
|
16876
17006
|
async replaceFullBody(input) {
|
|
16877
|
-
const absPath =
|
|
17007
|
+
const absPath = path8.join(this.vaultRoot, input.path);
|
|
16878
17008
|
const raw = await this.readRaw(absPath, input.path);
|
|
16879
17009
|
const { prefix } = splitRawFrontmatter(raw);
|
|
16880
17010
|
await this.writeFile(absPath, prefix + input.content, "utf8");
|
|
@@ -16904,18 +17034,31 @@ function createOperationsModule(config2, deps = {}) {
|
|
|
16904
17034
|
const providerFactory = deps.vaultProviderFactory ?? ((opts) => new ObsidianCLIProvider(opts));
|
|
16905
17035
|
const readerFactory = deps.vaultReaderFactory ?? ((opts) => new FsVaultReader({ vaultRoot: opts.vaultRoot }));
|
|
16906
17036
|
const writerFactory = deps.vaultWriterFactory ?? ((opts) => new FsVaultWriter({ vaultRoot: opts.vaultRoot }));
|
|
16907
|
-
const provider = providerFactory({ binaryPath: config2.binaryPath });
|
|
17037
|
+
const provider = providerFactory({ binaryPath: config2.binaryPath, vaultName: config2.vaultName });
|
|
16908
17038
|
const reader = readerFactory({ vaultRoot: config2.vaultPath });
|
|
16909
17039
|
const writer = writerFactory({ vaultRoot: config2.vaultPath });
|
|
16910
17040
|
const graph = deps.graph ?? new WikilinkGraphIndex({ reader });
|
|
16911
17041
|
const toolDeps = { provider, reader, writer, graph };
|
|
16912
|
-
return {
|
|
17042
|
+
return {
|
|
17043
|
+
tools: buildOperationsTools(toolDeps),
|
|
17044
|
+
resources: buildOperationsResources({ reader, provider, graph })
|
|
17045
|
+
};
|
|
16913
17046
|
}
|
|
16914
17047
|
|
|
16915
17048
|
// src/server.ts
|
|
16916
17049
|
var require2 = createRequire(import.meta.url);
|
|
16917
17050
|
var { name: SERVER_NAME, version: SERVER_VERSION } = require2("../package.json");
|
|
16918
|
-
var
|
|
17051
|
+
var EXTERNAL_AGENT_INSTRUCTIONS_PATH = ".neuro-vault/for-external-agents.md";
|
|
17052
|
+
async function readExternalAgentInstructions(vaultPath) {
|
|
17053
|
+
const filePath = path9.join(vaultPath, EXTERNAL_AGENT_INSTRUCTIONS_PATH);
|
|
17054
|
+
try {
|
|
17055
|
+
const raw = await fs4.readFile(filePath, "utf8");
|
|
17056
|
+
return raw.trim();
|
|
17057
|
+
} catch {
|
|
17058
|
+
return null;
|
|
17059
|
+
}
|
|
17060
|
+
}
|
|
17061
|
+
var STATIC_SERVER_INSTRUCTIONS = `This server provides two capability sets for an Obsidian vault: semantic search (when enabled) and direct vault operations (when enabled). Use the right one based on the user's intent.
|
|
16919
17062
|
|
|
16920
17063
|
## Role: a second brain alongside the project
|
|
16921
17064
|
|
|
@@ -16998,11 +17141,20 @@ If the user gives an exact anchor (note path, daily note, tag, frontmatter field
|
|
|
16998
17141
|
|
|
16999
17142
|
For tag-driven questions ("which notes are tagged X?", "show me everything in #ai") use \`query_notes\` with \`{ filter: { tags: '<name>' } }\`, not \`search_notes\` \u2014 the answer is exact, not fuzzy.
|
|
17000
17143
|
`;
|
|
17001
|
-
|
|
17002
|
-
|
|
17003
|
-
|
|
17004
|
-
|
|
17005
|
-
|
|
17144
|
+
var GET_VAULT_OVERVIEW_HINT = `## Orientation
|
|
17145
|
+
|
|
17146
|
+
Before reaching for \`list_tags\`, \`list_properties\`, or exploratory \`query_notes\`, call \`get_vault_overview\` once at the start of a session. It returns the top-level folder layout with counts, the top tags, frontmatter properties with inferred types, the total note count, and the top 10 notes by inbound wikilinks \u2014 enough to orient yourself in a single call. The same payload is available as the MCP resource \`vault://overview\` for clients that auto-load resources.`;
|
|
17147
|
+
async function buildServerInstructions(vaultPath) {
|
|
17148
|
+
let result = STATIC_SERVER_INSTRUCTIONS;
|
|
17149
|
+
result += "\n\n" + GET_VAULT_OVERVIEW_HINT;
|
|
17150
|
+
const extra = await readExternalAgentInstructions(vaultPath);
|
|
17151
|
+
if (extra !== null && extra !== "") {
|
|
17152
|
+
result += "\n\n## Vault-specific conventions\n\n" + extra;
|
|
17153
|
+
}
|
|
17154
|
+
return result;
|
|
17155
|
+
}
|
|
17156
|
+
function defaultServerFactory(instructions) {
|
|
17157
|
+
return new McpServer({ name: SERVER_NAME, version: SERVER_VERSION }, { instructions });
|
|
17006
17158
|
}
|
|
17007
17159
|
function defaultTransportFactory() {
|
|
17008
17160
|
return new StdioServerTransport();
|
|
@@ -17011,16 +17163,18 @@ async function startNeuroVaultServer(config2, deps = {}) {
|
|
|
17011
17163
|
if (!config2.semantic.enabled && !config2.operations.enabled) {
|
|
17012
17164
|
throw new Error("No modules enabled \u2014 pass --semantic or --operations");
|
|
17013
17165
|
}
|
|
17166
|
+
const instructions = await buildServerInstructions(config2.vaultPath);
|
|
17014
17167
|
const serverFactory = deps.serverFactory ?? defaultServerFactory;
|
|
17015
17168
|
const transportFactory = deps.transportFactory ?? defaultTransportFactory;
|
|
17016
|
-
const server = serverFactory();
|
|
17169
|
+
const server = serverFactory(instructions);
|
|
17017
17170
|
const sharedReader = new FsVaultReader({ vaultRoot: config2.vaultPath });
|
|
17018
17171
|
const sharedGraph = new WikilinkGraphIndex({ reader: sharedReader });
|
|
17019
17172
|
const sharedListMatchingPaths = createListMatchingPaths({
|
|
17020
17173
|
reader: sharedReader,
|
|
17021
17174
|
graph: sharedGraph
|
|
17022
17175
|
});
|
|
17023
|
-
const
|
|
17176
|
+
const toolRegistrations = [];
|
|
17177
|
+
const resourceRegistrations = [];
|
|
17024
17178
|
let warmup = async () => {
|
|
17025
17179
|
};
|
|
17026
17180
|
if (config2.semantic.enabled) {
|
|
@@ -17037,22 +17191,27 @@ async function startNeuroVaultServer(config2, deps = {}) {
|
|
|
17037
17191
|
...deps.semantic
|
|
17038
17192
|
}
|
|
17039
17193
|
);
|
|
17040
|
-
|
|
17194
|
+
toolRegistrations.push(...semantic.tools);
|
|
17041
17195
|
warmup = semantic.warmup;
|
|
17042
17196
|
}
|
|
17043
17197
|
if (config2.operations.enabled) {
|
|
17044
17198
|
const operations = createOperationsModule(
|
|
17045
17199
|
{
|
|
17046
17200
|
vaultPath: config2.vaultPath,
|
|
17201
|
+
vaultName: config2.operations.vaultName,
|
|
17047
17202
|
binaryPath: config2.operations.binaryPath
|
|
17048
17203
|
},
|
|
17049
17204
|
{ graph: sharedGraph, ...deps.operations }
|
|
17050
17205
|
);
|
|
17051
|
-
|
|
17206
|
+
toolRegistrations.push(...operations.tools);
|
|
17207
|
+
resourceRegistrations.push(...operations.resources);
|
|
17052
17208
|
}
|
|
17053
|
-
for (const tool of
|
|
17209
|
+
for (const tool of toolRegistrations) {
|
|
17054
17210
|
server.registerTool(tool.name, tool.spec, tool.handler);
|
|
17055
17211
|
}
|
|
17212
|
+
for (const resource of resourceRegistrations) {
|
|
17213
|
+
server.registerResource(resource.name, resource.uri, resource.metadata, resource.handler);
|
|
17214
|
+
}
|
|
17056
17215
|
await server.connect(transportFactory());
|
|
17057
17216
|
void warmup().catch((error48) => {
|
|
17058
17217
|
const message = error48 instanceof Error ? error48.message : String(error48);
|