neuro-vault-mcp 5.3.0 → 5.4.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
@@ -58,6 +58,8 @@ async function parseConfig(argv) {
58
58
 
59
59
  // src/server.ts
60
60
  import { createRequire } from "module";
61
+ import fs4 from "fs/promises";
62
+ import path9 from "path";
61
63
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
62
64
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
63
65
 
@@ -321,9 +323,9 @@ function buildBasenameIndex(paths) {
321
323
  resolveAll
322
324
  };
323
325
  }
324
- function baseOf(path8) {
325
- const slash = path8.lastIndexOf("/");
326
- const tail = slash >= 0 ? path8.slice(slash + 1) : path8;
326
+ function baseOf(path10) {
327
+ const slash = path10.lastIndexOf("/");
328
+ const tail = slash >= 0 ? path10.slice(slash + 1) : path10;
327
329
  return tail.endsWith(".md") ? tail.slice(0, -3) : tail;
328
330
  }
329
331
 
@@ -1716,10 +1718,10 @@ function mergeDefs(...defs) {
1716
1718
  function cloneDef(schema) {
1717
1719
  return mergeDefs(schema._zod.def);
1718
1720
  }
1719
- function getElementAtPath(obj, path8) {
1720
- if (!path8)
1721
+ function getElementAtPath(obj, path10) {
1722
+ if (!path10)
1721
1723
  return obj;
1722
- return path8.reduce((acc, key) => acc?.[key], obj);
1724
+ return path10.reduce((acc, key) => acc?.[key], obj);
1723
1725
  }
1724
1726
  function promiseAllObject(promisesObj) {
1725
1727
  const keys = Object.keys(promisesObj);
@@ -2102,11 +2104,11 @@ function aborted(x, startIndex = 0) {
2102
2104
  }
2103
2105
  return false;
2104
2106
  }
2105
- function prefixIssues(path8, issues) {
2107
+ function prefixIssues(path10, issues) {
2106
2108
  return issues.map((iss) => {
2107
2109
  var _a2;
2108
2110
  (_a2 = iss).path ?? (_a2.path = []);
2109
- iss.path.unshift(path8);
2111
+ iss.path.unshift(path10);
2110
2112
  return iss;
2111
2113
  });
2112
2114
  }
@@ -2289,7 +2291,7 @@ function formatError(error48, mapper = (issue2) => issue2.message) {
2289
2291
  }
2290
2292
  function treeifyError(error48, mapper = (issue2) => issue2.message) {
2291
2293
  const result = { errors: [] };
2292
- const processError = (error49, path8 = []) => {
2294
+ const processError = (error49, path10 = []) => {
2293
2295
  var _a2, _b;
2294
2296
  for (const issue2 of error49.issues) {
2295
2297
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -2299,7 +2301,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
2299
2301
  } else if (issue2.code === "invalid_element") {
2300
2302
  processError({ issues: issue2.issues }, issue2.path);
2301
2303
  } else {
2302
- const fullpath = [...path8, ...issue2.path];
2304
+ const fullpath = [...path10, ...issue2.path];
2303
2305
  if (fullpath.length === 0) {
2304
2306
  result.errors.push(mapper(issue2));
2305
2307
  continue;
@@ -2331,8 +2333,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
2331
2333
  }
2332
2334
  function toDotPath(_path) {
2333
2335
  const segs = [];
2334
- const path8 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
2335
- for (const seg of path8) {
2336
+ const path10 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
2337
+ for (const seg of path10) {
2336
2338
  if (typeof seg === "number")
2337
2339
  segs.push(`[${seg}]`);
2338
2340
  else if (typeof seg === "symbol")
@@ -14309,13 +14311,13 @@ function resolveRef(ref, ctx) {
14309
14311
  if (!ref.startsWith("#")) {
14310
14312
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
14311
14313
  }
14312
- const path8 = ref.slice(1).split("/").filter(Boolean);
14313
- if (path8.length === 0) {
14314
+ const path10 = ref.slice(1).split("/").filter(Boolean);
14315
+ if (path10.length === 0) {
14314
14316
  return ctx.rootSchema;
14315
14317
  }
14316
14318
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
14317
- if (path8[0] === defsKey) {
14318
- const key = path8[1];
14319
+ if (path10[0] === defsKey) {
14320
+ const key = path10[1];
14319
14321
  if (!key || !ctx.defs[key]) {
14320
14322
  throw new Error(`Reference not found: ${ref}`);
14321
14323
  }
@@ -14910,9 +14912,9 @@ async function invokeTool(handler) {
14910
14912
  // src/lib/tool-registry.ts
14911
14913
  function formatZodError(error48) {
14912
14914
  const issues = error48.issues.map((issue2) => {
14913
- const path8 = issue2.path.length > 0 ? issue2.path.join(".") : "<root>";
14915
+ const path10 = issue2.path.length > 0 ? issue2.path.join(".") : "<root>";
14914
14916
  const expected = issue2.expected;
14915
- const formatted = { path: path8, message: issue2.message };
14917
+ const formatted = { path: path10, message: issue2.message };
14916
14918
  if (typeof expected === "string") formatted.expected = expected;
14917
14919
  return formatted;
14918
14920
  });
@@ -15079,10 +15081,10 @@ async function getNoteLinks(opts) {
15079
15081
  for (const target of rawTargets) {
15080
15082
  const normalized = normalizeWikilinkTarget(target);
15081
15083
  if (!normalized) continue;
15082
- const path8 = basenameIndex.resolve(normalized);
15083
- if (!path8) continue;
15084
- if (path8 === notePath) continue;
15085
- resolved.add(path8);
15084
+ const path10 = basenameIndex.resolve(normalized);
15085
+ if (!path10) continue;
15086
+ if (path10 === notePath) continue;
15087
+ resolved.add(path10);
15086
15088
  }
15087
15089
  return resolved;
15088
15090
  }
@@ -15686,7 +15688,7 @@ function computeExpansion(args) {
15686
15688
  }
15687
15689
  }
15688
15690
  }
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);
15691
+ 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
15692
  }
15691
15693
  async function executeRetrieval(input) {
15692
15694
  const { query, mode, sources, embeddingProvider, searchEngine } = input;
@@ -15944,9 +15946,9 @@ function isFilterEmpty(filter) {
15944
15946
  }
15945
15947
  function narrowSources(sources, allowed) {
15946
15948
  const out = /* @__PURE__ */ new Map();
15947
- for (const path8 of allowed) {
15948
- const src = sources.get(path8);
15949
- if (src) out.set(path8, src);
15949
+ for (const path10 of allowed) {
15950
+ const src = sources.get(path10);
15951
+ if (src) out.set(path10, src);
15950
15952
  }
15951
15953
  return out;
15952
15954
  }
@@ -16183,10 +16185,10 @@ var ObsidianCLIProvider = class {
16183
16185
  }
16184
16186
  async readDaily() {
16185
16187
  const { stdout: pathStdout } = await this.runCommand("daily:path", []);
16186
- const path8 = pathStdout.trim();
16188
+ const path10 = pathStdout.trim();
16187
16189
  const { stdout } = await this.runCommand("daily:read", []);
16188
16190
  const { frontmatter, content } = splitFrontmatter(stdout);
16189
- return { path: path8, frontmatter, content };
16191
+ return { path: path10, frontmatter, content };
16190
16192
  }
16191
16193
  async setProperty(input) {
16192
16194
  const tokens = [`name=${input.name}`, `value=${this.serializeValue(input.value)}`];
@@ -16512,18 +16514,18 @@ function buildEditNoteTool(deps) {
16512
16514
  input.name === void 0 ? "name" : "path"
16513
16515
  );
16514
16516
  }
16515
- const path8 = await resolveToPath(input, reader);
16517
+ const path10 = await resolveToPath(input, reader);
16516
16518
  if (input.replace !== void 0) {
16517
16519
  if (input.replace === "") {
16518
16520
  throw invalidArgument("replace must not be empty", "replace");
16519
16521
  }
16520
16522
  return writer.replaceInNote({
16521
- path: path8,
16523
+ path: path10,
16522
16524
  find: input.replace,
16523
16525
  content: input.content
16524
16526
  });
16525
16527
  }
16526
- return writer.replaceFullBody({ path: path8, content: input.content });
16528
+ return writer.replaceFullBody({ path: path10, content: input.content });
16527
16529
  }
16528
16530
  };
16529
16531
  }
@@ -16574,22 +16576,91 @@ function buildGetNoteLinksTool(deps) {
16574
16576
  description: DESCRIPTION,
16575
16577
  inputSchema: inputSchema7,
16576
16578
  handler: async (input) => {
16577
- const path8 = normalizePath(input.path);
16579
+ const path10 = normalizePath(input.path);
16578
16580
  await graph.ensureFresh();
16579
- return graph.getNoteLinks(path8);
16581
+ return graph.getNoteLinks(path10);
16580
16582
  }
16581
16583
  };
16582
16584
  }
16583
16585
 
16584
- // src/modules/operations/tools/list-properties.ts
16586
+ // src/lib/obsidian/top-by-backlinks.ts
16587
+ import path7 from "path";
16588
+ function topByBacklinks(input) {
16589
+ const { paths, graph, limit } = input;
16590
+ return paths.map((p) => ({
16591
+ path: p,
16592
+ title: basenameWithoutExt(p),
16593
+ backlink_count: graph.getBacklinkCount(p)
16594
+ })).sort(
16595
+ (a, b) => b.backlink_count !== a.backlink_count ? b.backlink_count - a.backlink_count : a.path.localeCompare(b.path)
16596
+ ).slice(0, limit);
16597
+ }
16598
+ function basenameWithoutExt(notePath) {
16599
+ const base = path7.posix.basename(notePath);
16600
+ return base.endsWith(".md") ? base.slice(0, -3) : base;
16601
+ }
16602
+
16603
+ // src/lib/obsidian/vault-overview.ts
16604
+ var TOP_TAGS_LIMIT = 30;
16605
+ var TOP_PROPERTIES_LIMIT = 30;
16606
+ var TOP_BACKLINKS_LIMIT = 10;
16607
+ var ROOT_FOLDER_SENTINEL = "/";
16608
+ async function computeVaultOverview(deps) {
16609
+ const { reader, provider, graph } = deps;
16610
+ await graph.ensureFresh();
16611
+ const paths = await reader.scan();
16612
+ const [tags, props] = await Promise.all([provider.listTags(), provider.listProperties()]);
16613
+ if (paths.length === 0) {
16614
+ return {
16615
+ total_notes: 0,
16616
+ folders: [],
16617
+ top_tags: tags.slice(0, TOP_TAGS_LIMIT),
16618
+ properties: props.slice(0, TOP_PROPERTIES_LIMIT),
16619
+ top_by_backlinks: []
16620
+ };
16621
+ }
16622
+ const folders = /* @__PURE__ */ new Map();
16623
+ for (const p of paths) {
16624
+ const folder = topLevelFolder(p);
16625
+ folders.set(folder, (folders.get(folder) ?? 0) + 1);
16626
+ }
16627
+ return {
16628
+ total_notes: paths.length,
16629
+ folders: sortFolders(folders),
16630
+ top_tags: tags.slice(0, TOP_TAGS_LIMIT),
16631
+ properties: props.slice(0, TOP_PROPERTIES_LIMIT),
16632
+ top_by_backlinks: topByBacklinks({ paths, graph, limit: TOP_BACKLINKS_LIMIT })
16633
+ };
16634
+ }
16635
+ function topLevelFolder(notePath) {
16636
+ const ix = notePath.indexOf("/");
16637
+ return ix === -1 ? ROOT_FOLDER_SENTINEL : notePath.slice(0, ix);
16638
+ }
16639
+ function sortFolders(map2) {
16640
+ 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));
16641
+ }
16642
+
16643
+ // src/modules/operations/tools/get-vault-overview.ts
16585
16644
  var inputSchema8 = external_exports.object({});
16645
+ function buildGetVaultOverviewTool(deps) {
16646
+ return {
16647
+ name: "get_vault_overview",
16648
+ title: "Get Vault Overview",
16649
+ 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`.",
16650
+ inputSchema: inputSchema8,
16651
+ handler: async (_input) => computeVaultOverview(deps)
16652
+ };
16653
+ }
16654
+
16655
+ // src/modules/operations/tools/list-properties.ts
16656
+ var inputSchema9 = external_exports.object({});
16586
16657
  function buildListPropertiesTool(deps) {
16587
16658
  const { provider } = deps;
16588
16659
  return {
16589
16660
  name: "list_properties",
16590
16661
  title: "List Properties",
16591
16662
  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,
16663
+ inputSchema: inputSchema9,
16593
16664
  handler: async (_input) => {
16594
16665
  return provider.listProperties();
16595
16666
  }
@@ -16597,14 +16668,14 @@ function buildListPropertiesTool(deps) {
16597
16668
  }
16598
16669
 
16599
16670
  // src/modules/operations/tools/list-tags.ts
16600
- var inputSchema9 = external_exports.object({});
16671
+ var inputSchema10 = external_exports.object({});
16601
16672
  function buildListTagsTool(deps) {
16602
16673
  const { provider } = deps;
16603
16674
  return {
16604
16675
  name: "list_tags",
16605
16676
  title: "List Tags",
16606
16677
  description: "List all tags used across the vault, sorted by occurrence count desc. Returns `[{name, count}]`.",
16607
- inputSchema: inputSchema9,
16678
+ inputSchema: inputSchema10,
16608
16679
  handler: async (_input) => {
16609
16680
  return provider.listTags();
16610
16681
  }
@@ -16616,7 +16687,7 @@ var queryNotesSortSchema = external_exports.object({
16616
16687
  field: external_exports.string().min(1),
16617
16688
  order: external_exports.enum(["asc", "desc"])
16618
16689
  });
16619
- var inputSchema10 = external_exports.object({
16690
+ var inputSchema11 = external_exports.object({
16620
16691
  filter: external_exports.record(external_exports.string(), external_exports.unknown()),
16621
16692
  path_prefix: external_exports.string().optional(),
16622
16693
  sort: queryNotesSortSchema.optional(),
@@ -16629,7 +16700,7 @@ function buildQueryNotesTool(deps) {
16629
16700
  name: "query_notes",
16630
16701
  title: "Query Notes",
16631
16702
  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,
16703
+ inputSchema: inputSchema11,
16633
16704
  handler: async (input) => {
16634
16705
  return runQueryNotes(input, reader, graph);
16635
16706
  }
@@ -16639,14 +16710,14 @@ function buildQueryNotesTool(deps) {
16639
16710
  // src/modules/operations/tools/read-daily.ts
16640
16711
  var NOTES_TODAY_CAP = 200;
16641
16712
  var DAILY_BASENAME_RE = /(\d{4}-\d{2}-\d{2})\.md$/;
16642
- var inputSchema11 = external_exports.object({});
16713
+ var inputSchema12 = external_exports.object({});
16643
16714
  function buildReadDailyTool(deps) {
16644
16715
  const { provider, reader, graph } = deps;
16645
16716
  return {
16646
16717
  name: "read_daily",
16647
16718
  title: "Read Daily",
16648
16719
  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,
16720
+ inputSchema: inputSchema12,
16650
16721
  handler: async (_input) => {
16651
16722
  const daily = await provider.readDaily();
16652
16723
  const today = deriveToday(daily.path);
@@ -16690,7 +16761,7 @@ function formatLocalDate(d) {
16690
16761
 
16691
16762
  // src/modules/operations/tools/read-notes.ts
16692
16763
  var readNotesFieldSchema = external_exports.enum(["frontmatter", "content"]);
16693
- var inputSchema12 = external_exports.object({
16764
+ var inputSchema13 = external_exports.object({
16694
16765
  paths: external_exports.union([external_exports.string().min(1), external_exports.array(external_exports.string()).min(1).max(50)]),
16695
16766
  fields: external_exports.array(readNotesFieldSchema).min(1).optional()
16696
16767
  });
@@ -16700,7 +16771,7 @@ function buildReadNotesTool(deps) {
16700
16771
  name: "read_notes",
16701
16772
  title: "Read Notes",
16702
16773
  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,
16774
+ inputSchema: inputSchema13,
16704
16775
  handler: async (input) => {
16705
16776
  const { paths, fields } = validateReadNotesInput(input);
16706
16777
  const seen = /* @__PURE__ */ new Set();
@@ -16752,7 +16823,7 @@ function buildReadNotesTool(deps) {
16752
16823
  }
16753
16824
 
16754
16825
  // src/modules/operations/tools/read-property.ts
16755
- var inputSchema13 = external_exports.object({
16826
+ var inputSchema14 = external_exports.object({
16756
16827
  name: external_exports.string().optional(),
16757
16828
  path: external_exports.string().optional(),
16758
16829
  key: external_exports.string()
@@ -16763,7 +16834,7 @@ function buildReadPropertyTool(deps) {
16763
16834
  name: "read_property",
16764
16835
  title: "Read Property",
16765
16836
  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,
16837
+ inputSchema: inputSchema14,
16767
16838
  handler: async (input) => {
16768
16839
  const identifier = resolveIdentifier(input.name, input.path);
16769
16840
  if (!input.key || input.key.trim() === "") {
@@ -16775,7 +16846,7 @@ function buildReadPropertyTool(deps) {
16775
16846
  }
16776
16847
 
16777
16848
  // src/modules/operations/tools/remove-property.ts
16778
- var inputSchema14 = external_exports.object({
16849
+ var inputSchema15 = external_exports.object({
16779
16850
  name: external_exports.string().optional(),
16780
16851
  path: external_exports.string().optional(),
16781
16852
  key: external_exports.string()
@@ -16786,7 +16857,7 @@ function buildRemovePropertyTool(deps) {
16786
16857
  name: "remove_property",
16787
16858
  title: "Remove Property",
16788
16859
  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,
16860
+ inputSchema: inputSchema15,
16790
16861
  handler: async (input) => {
16791
16862
  const identifier = resolveIdentifier(input.name, input.path);
16792
16863
  if (!input.key || input.key.trim() === "") {
@@ -16799,7 +16870,7 @@ function buildRemovePropertyTool(deps) {
16799
16870
  }
16800
16871
 
16801
16872
  // src/modules/operations/tools/set-property.ts
16802
- var inputSchema15 = external_exports.object({
16873
+ var inputSchema16 = external_exports.object({
16803
16874
  name: external_exports.string().optional(),
16804
16875
  path: external_exports.string().optional(),
16805
16876
  key: external_exports.string(),
@@ -16812,7 +16883,7 @@ function buildSetPropertyTool(deps) {
16812
16883
  name: "set_property",
16813
16884
  title: "Set Property",
16814
16885
  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,
16886
+ inputSchema: inputSchema16,
16816
16887
  handler: async (input) => {
16817
16888
  const identifier = resolveIdentifier(input.name, input.path);
16818
16889
  if (!input.key || input.key.trim() === "") {
@@ -16838,13 +16909,58 @@ function buildOperationsTools(deps) {
16838
16909
  registerTool(buildRemovePropertyTool(deps)),
16839
16910
  registerTool(buildListPropertiesTool(deps)),
16840
16911
  registerTool(buildListTagsTool(deps)),
16841
- registerTool(buildGetNoteLinksTool(deps))
16912
+ registerTool(buildGetNoteLinksTool(deps)),
16913
+ registerTool(buildGetVaultOverviewTool(deps))
16842
16914
  ];
16843
16915
  }
16844
16916
 
16917
+ // src/lib/resource-registry.ts
16918
+ function registerResource(resource) {
16919
+ const mimeType = resource.mimeType ?? "application/json";
16920
+ return {
16921
+ name: resource.name,
16922
+ uri: resource.uri,
16923
+ metadata: {
16924
+ ...resource.title !== void 0 ? { title: resource.title } : {},
16925
+ description: resource.description,
16926
+ mimeType
16927
+ },
16928
+ handler: async (uri) => {
16929
+ const payload = await resource.handler(uri);
16930
+ const text = typeof payload === "string" ? payload : JSON.stringify(payload);
16931
+ return {
16932
+ contents: [
16933
+ {
16934
+ uri: resource.uri,
16935
+ mimeType,
16936
+ text
16937
+ }
16938
+ ]
16939
+ };
16940
+ }
16941
+ };
16942
+ }
16943
+
16944
+ // src/modules/operations/resources/vault-overview.ts
16945
+ function buildVaultOverviewResource(deps) {
16946
+ return {
16947
+ name: "vault-overview",
16948
+ uri: "vault://overview",
16949
+ title: "Vault Overview",
16950
+ 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.",
16951
+ mimeType: "application/json",
16952
+ handler: async () => computeVaultOverview(deps)
16953
+ };
16954
+ }
16955
+
16956
+ // src/modules/operations/resources/index.ts
16957
+ function buildOperationsResources(deps) {
16958
+ return [registerResource(buildVaultOverviewResource(deps))];
16959
+ }
16960
+
16845
16961
  // src/lib/obsidian/vault-writer.ts
16846
16962
  import { readFile as fsReadFile2, writeFile as fsWriteFile } from "fs/promises";
16847
- import path7 from "path";
16963
+ import path8 from "path";
16848
16964
  var FsVaultWriter = class {
16849
16965
  vaultRoot;
16850
16966
  readFile;
@@ -16855,7 +16971,7 @@ var FsVaultWriter = class {
16855
16971
  this.writeFile = opts.writeFile ?? ((p, d, enc) => fsWriteFile(p, d, enc));
16856
16972
  }
16857
16973
  async replaceInNote(input) {
16858
- const absPath = path7.join(this.vaultRoot, input.path);
16974
+ const absPath = path8.join(this.vaultRoot, input.path);
16859
16975
  const raw = await this.readRaw(absPath, input.path);
16860
16976
  const { prefix, body } = splitRawFrontmatter(raw);
16861
16977
  const result = applyReplace(body, input.find, input.content);
@@ -16874,7 +16990,7 @@ var FsVaultWriter = class {
16874
16990
  await this.writeFile(absPath, prefix + result.body, "utf8");
16875
16991
  }
16876
16992
  async replaceFullBody(input) {
16877
- const absPath = path7.join(this.vaultRoot, input.path);
16993
+ const absPath = path8.join(this.vaultRoot, input.path);
16878
16994
  const raw = await this.readRaw(absPath, input.path);
16879
16995
  const { prefix } = splitRawFrontmatter(raw);
16880
16996
  await this.writeFile(absPath, prefix + input.content, "utf8");
@@ -16909,13 +17025,26 @@ function createOperationsModule(config2, deps = {}) {
16909
17025
  const writer = writerFactory({ vaultRoot: config2.vaultPath });
16910
17026
  const graph = deps.graph ?? new WikilinkGraphIndex({ reader });
16911
17027
  const toolDeps = { provider, reader, writer, graph };
16912
- return { tools: buildOperationsTools(toolDeps) };
17028
+ return {
17029
+ tools: buildOperationsTools(toolDeps),
17030
+ resources: buildOperationsResources({ reader, provider, graph })
17031
+ };
16913
17032
  }
16914
17033
 
16915
17034
  // src/server.ts
16916
17035
  var require2 = createRequire(import.meta.url);
16917
17036
  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.
17037
+ var EXTERNAL_AGENT_INSTRUCTIONS_PATH = ".neuro-vault/for-external-agents.md";
17038
+ async function readExternalAgentInstructions(vaultPath) {
17039
+ const filePath = path9.join(vaultPath, EXTERNAL_AGENT_INSTRUCTIONS_PATH);
17040
+ try {
17041
+ const raw = await fs4.readFile(filePath, "utf8");
17042
+ return raw.trim();
17043
+ } catch {
17044
+ return null;
17045
+ }
17046
+ }
17047
+ 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
17048
 
16920
17049
  ## Role: a second brain alongside the project
16921
17050
 
@@ -16998,11 +17127,20 @@ If the user gives an exact anchor (note path, daily note, tag, frontmatter field
16998
17127
 
16999
17128
  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
17129
  `;
17001
- function defaultServerFactory() {
17002
- return new McpServer(
17003
- { name: SERVER_NAME, version: SERVER_VERSION },
17004
- { instructions: SERVER_INSTRUCTIONS }
17005
- );
17130
+ var GET_VAULT_OVERVIEW_HINT = `## Orientation
17131
+
17132
+ 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.`;
17133
+ async function buildServerInstructions(vaultPath) {
17134
+ let result = STATIC_SERVER_INSTRUCTIONS;
17135
+ result += "\n\n" + GET_VAULT_OVERVIEW_HINT;
17136
+ const extra = await readExternalAgentInstructions(vaultPath);
17137
+ if (extra !== null && extra !== "") {
17138
+ result += "\n\n## Vault-specific conventions\n\n" + extra;
17139
+ }
17140
+ return result;
17141
+ }
17142
+ function defaultServerFactory(instructions) {
17143
+ return new McpServer({ name: SERVER_NAME, version: SERVER_VERSION }, { instructions });
17006
17144
  }
17007
17145
  function defaultTransportFactory() {
17008
17146
  return new StdioServerTransport();
@@ -17011,16 +17149,18 @@ async function startNeuroVaultServer(config2, deps = {}) {
17011
17149
  if (!config2.semantic.enabled && !config2.operations.enabled) {
17012
17150
  throw new Error("No modules enabled \u2014 pass --semantic or --operations");
17013
17151
  }
17152
+ const instructions = await buildServerInstructions(config2.vaultPath);
17014
17153
  const serverFactory = deps.serverFactory ?? defaultServerFactory;
17015
17154
  const transportFactory = deps.transportFactory ?? defaultTransportFactory;
17016
- const server = serverFactory();
17155
+ const server = serverFactory(instructions);
17017
17156
  const sharedReader = new FsVaultReader({ vaultRoot: config2.vaultPath });
17018
17157
  const sharedGraph = new WikilinkGraphIndex({ reader: sharedReader });
17019
17158
  const sharedListMatchingPaths = createListMatchingPaths({
17020
17159
  reader: sharedReader,
17021
17160
  graph: sharedGraph
17022
17161
  });
17023
- const registrations = [];
17162
+ const toolRegistrations = [];
17163
+ const resourceRegistrations = [];
17024
17164
  let warmup = async () => {
17025
17165
  };
17026
17166
  if (config2.semantic.enabled) {
@@ -17037,7 +17177,7 @@ async function startNeuroVaultServer(config2, deps = {}) {
17037
17177
  ...deps.semantic
17038
17178
  }
17039
17179
  );
17040
- registrations.push(...semantic.tools);
17180
+ toolRegistrations.push(...semantic.tools);
17041
17181
  warmup = semantic.warmup;
17042
17182
  }
17043
17183
  if (config2.operations.enabled) {
@@ -17048,11 +17188,15 @@ async function startNeuroVaultServer(config2, deps = {}) {
17048
17188
  },
17049
17189
  { graph: sharedGraph, ...deps.operations }
17050
17190
  );
17051
- registrations.push(...operations.tools);
17191
+ toolRegistrations.push(...operations.tools);
17192
+ resourceRegistrations.push(...operations.resources);
17052
17193
  }
17053
- for (const tool of registrations) {
17194
+ for (const tool of toolRegistrations) {
17054
17195
  server.registerTool(tool.name, tool.spec, tool.handler);
17055
17196
  }
17197
+ for (const resource of resourceRegistrations) {
17198
+ server.registerResource(resource.name, resource.uri, resource.metadata, resource.handler);
17199
+ }
17056
17200
  await server.connect(transportFactory());
17057
17201
  void warmup().catch((error48) => {
17058
17202
  const message = error48 instanceof Error ? error48.message : String(error48);