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 +7 -1
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +209 -65
- 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
|
@@ -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(
|
|
325
|
-
const slash =
|
|
326
|
-
const tail = slash >= 0 ?
|
|
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,
|
|
1720
|
-
if (!
|
|
1721
|
+
function getElementAtPath(obj, path10) {
|
|
1722
|
+
if (!path10)
|
|
1721
1723
|
return obj;
|
|
1722
|
-
return
|
|
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(
|
|
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(
|
|
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,
|
|
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 = [...
|
|
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
|
|
2335
|
-
for (const seg of
|
|
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
|
|
14313
|
-
if (
|
|
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 (
|
|
14318
|
-
const key =
|
|
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
|
|
14915
|
+
const path10 = issue2.path.length > 0 ? issue2.path.join(".") : "<root>";
|
|
14914
14916
|
const expected = issue2.expected;
|
|
14915
|
-
const formatted = { path:
|
|
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
|
|
15083
|
-
if (!
|
|
15084
|
-
if (
|
|
15085
|
-
resolved.add(
|
|
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(([
|
|
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
|
|
15948
|
-
const src = sources.get(
|
|
15949
|
-
if (src) out.set(
|
|
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
|
|
16188
|
+
const path10 = pathStdout.trim();
|
|
16187
16189
|
const { stdout } = await this.runCommand("daily:read", []);
|
|
16188
16190
|
const { frontmatter, content } = splitFrontmatter(stdout);
|
|
16189
|
-
return { path:
|
|
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
|
|
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:
|
|
16523
|
+
path: path10,
|
|
16522
16524
|
find: input.replace,
|
|
16523
16525
|
content: input.content
|
|
16524
16526
|
});
|
|
16525
16527
|
}
|
|
16526
|
-
return writer.replaceFullBody({ path:
|
|
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
|
|
16579
|
+
const path10 = normalizePath(input.path);
|
|
16578
16580
|
await graph.ensureFresh();
|
|
16579
|
-
return graph.getNoteLinks(
|
|
16581
|
+
return graph.getNoteLinks(path10);
|
|
16580
16582
|
}
|
|
16581
16583
|
};
|
|
16582
16584
|
}
|
|
16583
16585
|
|
|
16584
|
-
// src/
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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 =
|
|
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 =
|
|
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 {
|
|
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
|
|
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
|
-
|
|
17002
|
-
|
|
17003
|
-
|
|
17004
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
17191
|
+
toolRegistrations.push(...operations.tools);
|
|
17192
|
+
resourceRegistrations.push(...operations.resources);
|
|
17052
17193
|
}
|
|
17053
|
-
for (const tool of
|
|
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);
|