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 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(path8) {
325
- const slash = path8.lastIndexOf("/");
326
- const tail = slash >= 0 ? path8.slice(slash + 1) : path8;
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, path8) {
1720
- if (!path8)
1728
+ function getElementAtPath(obj, path10) {
1729
+ if (!path10)
1721
1730
  return obj;
1722
- return path8.reduce((acc, key) => acc?.[key], obj);
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(path8, issues) {
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(path8);
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, path8 = []) => {
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 = [...path8, ...issue2.path];
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 path8 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
2335
- for (const seg of path8) {
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 path8 = ref.slice(1).split("/").filter(Boolean);
14313
- if (path8.length === 0) {
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 (path8[0] === defsKey) {
14318
- const key = path8[1];
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 path8 = issue2.path.length > 0 ? issue2.path.join(".") : "<root>";
14922
+ const path10 = issue2.path.length > 0 ? issue2.path.join(".") : "<root>";
14914
14923
  const expected = issue2.expected;
14915
- const formatted = { path: path8, message: issue2.message };
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 path8 = basenameIndex.resolve(normalized);
15083
- if (!path8) continue;
15084
- if (path8 === notePath) continue;
15085
- resolved.add(path8);
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(([path8, similarity]) => ({ path: path8, similarity, via_expansion: true })).sort((a, b) => b.similarity - a.similarity || a.path.localeCompare(b.path)).slice(0, totalLimit);
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 path8 of allowed) {
15948
- const src = sources.get(path8);
15949
- if (src) out.set(path8, src);
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 path8 = pathStdout.trim();
16195
+ const path10 = pathStdout.trim();
16187
16196
  const { stdout } = await this.runCommand("daily:read", []);
16188
16197
  const { frontmatter, content } = splitFrontmatter(stdout);
16189
- return { path: path8, frontmatter, content };
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 path8 = await resolveToPath(input, reader);
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: path8,
16537
+ path: path10,
16522
16538
  find: input.replace,
16523
16539
  content: input.content
16524
16540
  });
16525
16541
  }
16526
- return writer.replaceFullBody({ path: path8, content: input.content });
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 path8 = normalizePath(input.path);
16593
+ const path10 = normalizePath(input.path);
16578
16594
  await graph.ensureFresh();
16579
- return graph.getNoteLinks(path8);
16595
+ return graph.getNoteLinks(path10);
16580
16596
  }
16581
16597
  };
16582
16598
  }
16583
16599
 
16584
- // src/modules/operations/tools/list-properties.ts
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: inputSchema8,
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 inputSchema9 = external_exports.object({});
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: inputSchema9,
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 inputSchema10 = external_exports.object({
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: inputSchema10,
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 inputSchema11 = external_exports.object({});
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: inputSchema11,
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 inputSchema12 = external_exports.object({
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: inputSchema12,
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 inputSchema13 = external_exports.object({
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: inputSchema13,
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 inputSchema14 = external_exports.object({
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: inputSchema14,
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 inputSchema15 = external_exports.object({
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: inputSchema15,
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 path7 from "path";
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 = path7.join(this.vaultRoot, input.path);
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 = path7.join(this.vaultRoot, input.path);
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 { tools: buildOperationsTools(toolDeps) };
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 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.
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
- function defaultServerFactory() {
17002
- return new McpServer(
17003
- { name: SERVER_NAME, version: SERVER_VERSION },
17004
- { instructions: SERVER_INSTRUCTIONS }
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 registrations = [];
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
- registrations.push(...semantic.tools);
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
- registrations.push(...operations.tools);
17206
+ toolRegistrations.push(...operations.tools);
17207
+ resourceRegistrations.push(...operations.resources);
17052
17208
  }
17053
- for (const tool of registrations) {
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);