@bitbonsai/mcpvault 0.14.0 → 0.15.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 +22 -1
- package/dist/server.js +9 -5
- package/dist/src/cli.d.ts +11 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +36 -0
- package/dist/src/createServer.d.ts +2 -0
- package/dist/src/createServer.d.ts.map +1 -1
- package/dist/src/createServer.js +259 -238
- package/dist/src/pathfilter.d.ts.map +1 -1
- package/dist/src/pathfilter.js +5 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -253,6 +253,27 @@ Add to your Claude Desktop configuration file:
|
|
|
253
253
|
}
|
|
254
254
|
```
|
|
255
255
|
|
|
256
|
+
**Read-only mode:**
|
|
257
|
+
|
|
258
|
+
Add `--read-only` after the vault path to expose only read tools. Mutating tools are omitted from discovery and rejected if called directly.
|
|
259
|
+
|
|
260
|
+
```json
|
|
261
|
+
{
|
|
262
|
+
"mcpServers": {
|
|
263
|
+
"obsidian-read-only": {
|
|
264
|
+
"command": "npx",
|
|
265
|
+
"args": [
|
|
266
|
+
"@bitbonsai/mcpvault@latest",
|
|
267
|
+
"/Users/yourname/Documents/ResearchVault",
|
|
268
|
+
"--read-only"
|
|
269
|
+
]
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The CLI also accepts `--read-only true` and `--read-only=true` for configuration systems that require explicit values. Omit the option, or set it to `false`, to keep normal read/write access.
|
|
276
|
+
|
|
256
277
|
**Configuration File Locations:**
|
|
257
278
|
|
|
258
279
|
- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
@@ -961,7 +982,7 @@ This MCP server implements several security measures to protect your Obsidian va
|
|
|
961
982
|
### Best Practices
|
|
962
983
|
|
|
963
984
|
- **Least Privilege:** Server only accesses the specified vault directory
|
|
964
|
-
- **Read-Only
|
|
985
|
+
- **Read-Only Mode:** Run with `--read-only` for sensitive vaults; mutating tools are hidden and rejected
|
|
965
986
|
- **Backup Recommended:** Always backup your vault before using write operations
|
|
966
987
|
- **Network Isolation:** Server uses stdio transport (no network exposure)
|
|
967
988
|
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
3
|
import { createServer } from "./src/createServer.js";
|
|
4
|
+
import { parseCliArgs } from "./src/cli.js";
|
|
4
5
|
import { readFileSync } from "fs";
|
|
5
6
|
import { fileURLToPath } from "url";
|
|
6
7
|
import { dirname, join, resolve } from "path";
|
|
@@ -23,7 +24,7 @@ mcpvault v${VERSION}
|
|
|
23
24
|
Universal AI bridge for Obsidian vaults - connect any MCP-compatible assistant
|
|
24
25
|
|
|
25
26
|
Usage:
|
|
26
|
-
npx @bitbonsai/mcpvault [vault-path]
|
|
27
|
+
npx @bitbonsai/mcpvault [vault-path] [--read-only[=true|false]]
|
|
27
28
|
|
|
28
29
|
Arguments:
|
|
29
30
|
[vault-path] Optional path to your Obsidian vault directory
|
|
@@ -32,21 +33,24 @@ Arguments:
|
|
|
32
33
|
Options:
|
|
33
34
|
--version, -v Show version number
|
|
34
35
|
--help, -h Show this help message
|
|
36
|
+
--read-only Expose read tools only and reject all vault mutations
|
|
37
|
+
May be passed alone, with true/false, or as --read-only=true
|
|
35
38
|
|
|
36
39
|
Examples:
|
|
37
40
|
npx @bitbonsai/mcpvault
|
|
38
41
|
npx @bitbonsai/mcpvault ~/Documents/MyVault
|
|
39
42
|
npx @bitbonsai/mcpvault ./Vault
|
|
43
|
+
npx @bitbonsai/mcpvault ./Vault --read-only
|
|
40
44
|
npx @bitbonsai/mcpvault /path/to/obsidian/vault
|
|
41
45
|
npx @bitbonsai/mcpvault "/path/with spaces/Obsidian Vault"
|
|
42
46
|
`);
|
|
43
47
|
process.exit(0);
|
|
44
48
|
}
|
|
45
|
-
//
|
|
46
|
-
// When omitted,
|
|
47
|
-
const vaultPathArg = cliArgs
|
|
49
|
+
// Remove runtime options before joining trailing args, preserving support for
|
|
50
|
+
// unquoted vault paths with spaces. When omitted, use the current directory.
|
|
51
|
+
const { vaultPathArg, readOnly } = parseCliArgs(cliArgs);
|
|
48
52
|
const vaultPath = resolve(vaultPathArg || process.cwd());
|
|
49
|
-
const server = createServer(vaultPath, { version: VERSION });
|
|
53
|
+
const server = createServer(vaultPath, { version: VERSION, readOnly });
|
|
50
54
|
const transport = new StdioServerTransport();
|
|
51
55
|
await server.connect(transport);
|
|
52
56
|
// Exit when the client disconnects (stdin EOF) or the process is asked to
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface ParsedCliArgs {
|
|
2
|
+
vaultPathArg: string;
|
|
3
|
+
readOnly: boolean;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Parse runtime options without importing server.ts, which starts the MCP
|
|
7
|
+
* server as a side effect. Unknown positional arguments remain part of the
|
|
8
|
+
* vault path so unquoted paths with spaces continue to work.
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseCliArgs(args: string[]): ParsedCliArgs;
|
|
11
|
+
//# sourceMappingURL=cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,aAAa,CAkC1D"}
|
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse runtime options without importing server.ts, which starts the MCP
|
|
3
|
+
* server as a side effect. Unknown positional arguments remain part of the
|
|
4
|
+
* vault path so unquoted paths with spaces continue to work.
|
|
5
|
+
*/
|
|
6
|
+
export function parseCliArgs(args) {
|
|
7
|
+
const pathArgs = [];
|
|
8
|
+
let readOnly = false;
|
|
9
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
10
|
+
const arg = args[index];
|
|
11
|
+
if (arg === "--read-only") {
|
|
12
|
+
const next = args[index + 1]?.toLowerCase();
|
|
13
|
+
if (next === "true" || next === "false") {
|
|
14
|
+
readOnly = next === "true";
|
|
15
|
+
index += 1;
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
readOnly = true;
|
|
19
|
+
}
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (arg.startsWith("--read-only=")) {
|
|
23
|
+
const value = arg.slice("--read-only=".length).toLowerCase();
|
|
24
|
+
if (value !== "true" && value !== "false") {
|
|
25
|
+
throw new Error("--read-only must be true or false");
|
|
26
|
+
}
|
|
27
|
+
readOnly = value === "true";
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
pathArgs.push(arg);
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
vaultPathArg: pathArgs.join(" ").trim(),
|
|
34
|
+
readOnly,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -6,6 +6,8 @@ export interface CreateServerOptions {
|
|
|
6
6
|
version?: string;
|
|
7
7
|
pathFilter?: PathFilter;
|
|
8
8
|
frontmatterHandler?: FrontmatterHandler;
|
|
9
|
+
/** Expose read tools only and reject direct calls to mutating tools. */
|
|
10
|
+
readOnly?: boolean;
|
|
9
11
|
}
|
|
10
12
|
export declare function createServer(vaultPath: string, options?: CreateServerOptions): Server;
|
|
11
13
|
//# sourceMappingURL=createServer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createServer.d.ts","sourceRoot":"","sources":["../../src/createServer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAMnE,OAAO,EAAE,kBAAkB,EAAoB,MAAM,kBAAkB,CAAC;AACxE,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAK7C,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"createServer.d.ts","sourceRoot":"","sources":["../../src/createServer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAMnE,OAAO,EAAE,kBAAkB,EAAoB,MAAM,kBAAkB,CAAC;AACxE,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAK7C,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC,wEAAwE;IACxE,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAYD,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,MAAM,CAsezF"}
|
package/dist/src/createServer.js
CHANGED
|
@@ -6,8 +6,17 @@ import { PathFilter } from "./pathfilter.js";
|
|
|
6
6
|
import { SearchService } from "./search.js";
|
|
7
7
|
import { handleWikiLinkTool } from "./wikilink/index.js";
|
|
8
8
|
import { resolve } from "path";
|
|
9
|
+
const MUTATING_TOOLS = new Set([
|
|
10
|
+
"write_note",
|
|
11
|
+
"patch_note",
|
|
12
|
+
"delete_note",
|
|
13
|
+
"move_note",
|
|
14
|
+
"move_file",
|
|
15
|
+
"update_frontmatter",
|
|
16
|
+
"manage_tags",
|
|
17
|
+
]);
|
|
9
18
|
export function createServer(vaultPath, options = {}) {
|
|
10
|
-
const { name = "mcpvault", version = "0.0.0", pathFilter = new PathFilter(), frontmatterHandler = new FrontmatterHandler(), } = options;
|
|
19
|
+
const { name = "mcpvault", version = "0.0.0", pathFilter = new PathFilter(), frontmatterHandler = new FrontmatterHandler(), readOnly = false, } = options;
|
|
11
20
|
const resolvedVaultPath = resolve(vaultPath);
|
|
12
21
|
const fileSystem = new FileSystemService(resolvedVaultPath, pathFilter, frontmatterHandler);
|
|
13
22
|
const searchService = new SearchService(resolvedVaultPath, pathFilter);
|
|
@@ -15,254 +24,266 @@ export function createServer(vaultPath, options = {}) {
|
|
|
15
24
|
capabilities: { tools: {} },
|
|
16
25
|
});
|
|
17
26
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
prettyPrint: { type: "boolean", description: "Format JSON response with indentation (default: false)", default: false }
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
name: "delete_note",
|
|
73
|
-
description: "Delete a note from the Obsidian vault (requires confirmation). Supports permanent delete, vault trash, or system trash.",
|
|
74
|
-
inputSchema: {
|
|
75
|
-
type: "object",
|
|
76
|
-
properties: {
|
|
77
|
-
path: { type: "string", description: "Path to the note relative to vault root" },
|
|
78
|
-
confirmPath: { type: "string", description: "Confirmation: must exactly match the path parameter to proceed with deletion" },
|
|
79
|
-
trashMode: { type: "string", enum: ["none", "local", "system"], description: "Deletion mode: 'none' = permanent delete (default), 'local' = move to .trash inside vault, 'system' = move to OS trash", default: "none" }
|
|
80
|
-
},
|
|
81
|
-
required: ["path", "confirmPath"]
|
|
82
|
-
}
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
name: "search_notes",
|
|
86
|
-
description: "Search for notes in the vault by content or frontmatter",
|
|
87
|
-
inputSchema: {
|
|
88
|
-
type: "object",
|
|
89
|
-
properties: {
|
|
90
|
-
query: { type: "string", description: "Search query text" },
|
|
91
|
-
limit: { type: "number", description: "Maximum number of results (default: 5, max: 20)", default: 5 },
|
|
92
|
-
searchContent: { type: "boolean", description: "Search in note content (default: true)", default: true },
|
|
93
|
-
searchFrontmatter: { type: "boolean", description: "Search in frontmatter (default: false)", default: false },
|
|
94
|
-
caseSensitive: { type: "boolean", description: "Case sensitive search (default: false)", default: false },
|
|
95
|
-
pathPrefix: { type: "string", description: "Restrict the search to a vault subtree, e.g. \"Projects/2026\" (directory prefix)" },
|
|
96
|
-
excludePaths: { type: "array", items: { type: "string" }, description: "Skip files under these subtrees, e.g. [\"Archive\", \"meta\"] (directory prefixes)" },
|
|
97
|
-
prettyPrint: { type: "boolean", description: "Format JSON response with indentation (default: false)", default: false }
|
|
98
|
-
},
|
|
99
|
-
required: ["query"]
|
|
100
|
-
}
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
name: "move_note",
|
|
104
|
-
description: "Move or rename a note in the vault",
|
|
105
|
-
inputSchema: {
|
|
106
|
-
type: "object",
|
|
107
|
-
properties: {
|
|
108
|
-
oldPath: { type: "string", description: "Current path of the note" },
|
|
109
|
-
newPath: { type: "string", description: "New path for the note" },
|
|
110
|
-
overwrite: { type: "boolean", description: "Allow overwriting existing file (default: false)", default: false }
|
|
111
|
-
},
|
|
112
|
-
required: ["oldPath", "newPath"]
|
|
113
|
-
}
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
name: "move_file",
|
|
117
|
-
description: "Move or rename any file in the vault (binary-safe, file-only, requires confirmation)",
|
|
118
|
-
inputSchema: {
|
|
119
|
-
type: "object",
|
|
120
|
-
properties: {
|
|
121
|
-
oldPath: { type: "string", description: "Current path of the file" },
|
|
122
|
-
newPath: { type: "string", description: "New path for the file" },
|
|
123
|
-
confirmOldPath: { type: "string", description: "Confirmation: must exactly match oldPath" },
|
|
124
|
-
confirmNewPath: { type: "string", description: "Confirmation: must exactly match newPath" },
|
|
125
|
-
overwrite: { type: "boolean", description: "Allow overwriting existing file (default: false)", default: false }
|
|
126
|
-
},
|
|
127
|
-
required: ["oldPath", "newPath", "confirmOldPath", "confirmNewPath"]
|
|
128
|
-
}
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
name: "read_multiple_notes",
|
|
132
|
-
description: "Read multiple notes in a batch (max 10 files)",
|
|
133
|
-
inputSchema: {
|
|
134
|
-
type: "object",
|
|
135
|
-
properties: {
|
|
136
|
-
paths: { type: "array", items: { type: "string" }, description: "Array of note paths to read", maxItems: 10 },
|
|
137
|
-
includeContent: { type: "boolean", description: "Include note content (default: true)", default: true },
|
|
138
|
-
includeFrontmatter: { type: "boolean", description: "Include frontmatter (default: true)", default: true },
|
|
139
|
-
prettyPrint: { type: "boolean", description: "Format JSON response with indentation (default: false)", default: false }
|
|
140
|
-
},
|
|
141
|
-
required: ["paths"]
|
|
142
|
-
}
|
|
143
|
-
},
|
|
144
|
-
{
|
|
145
|
-
name: "update_frontmatter",
|
|
146
|
-
description: "Update frontmatter of a note without changing content",
|
|
147
|
-
inputSchema: {
|
|
148
|
-
type: "object",
|
|
149
|
-
properties: {
|
|
150
|
-
path: { type: "string", description: "Path to the note" },
|
|
151
|
-
frontmatter: { type: "object", description: "Frontmatter object to update" },
|
|
152
|
-
merge: { type: "boolean", description: "Merge with existing frontmatter (default: true)", default: true }
|
|
153
|
-
},
|
|
154
|
-
required: ["path", "frontmatter"]
|
|
27
|
+
const tools = [
|
|
28
|
+
{
|
|
29
|
+
name: "read_note",
|
|
30
|
+
description: "Read a note from the Obsidian vault",
|
|
31
|
+
inputSchema: {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: {
|
|
34
|
+
path: { type: "string", description: "Path to the note relative to vault root" },
|
|
35
|
+
prettyPrint: { type: "boolean", description: "Format JSON response with indentation (default: false)", default: false }
|
|
36
|
+
},
|
|
37
|
+
required: ["path"]
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "write_note",
|
|
42
|
+
description: "Write a note to the Obsidian vault",
|
|
43
|
+
inputSchema: {
|
|
44
|
+
type: "object",
|
|
45
|
+
properties: {
|
|
46
|
+
path: { type: "string", description: "Path to the note relative to vault root" },
|
|
47
|
+
content: { type: "string", description: "Content of the note" },
|
|
48
|
+
frontmatter: { type: "object", description: "Frontmatter object (optional)" },
|
|
49
|
+
mode: { type: "string", enum: ["overwrite", "append", "prepend"], description: "Write mode: 'overwrite' (default), 'append', or 'prepend'", default: "overwrite" }
|
|
50
|
+
},
|
|
51
|
+
required: ["path", "content"]
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "patch_note",
|
|
56
|
+
description: "Efficiently update part of a note by replacing a specific string. This is more efficient than rewriting the entire note for small changes.",
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: "object",
|
|
59
|
+
properties: {
|
|
60
|
+
path: { type: "string", description: "Path to the note relative to vault root" },
|
|
61
|
+
oldString: { type: "string", description: "The exact string to replace. Must match exactly including whitespace and line breaks." },
|
|
62
|
+
newString: { type: "string", description: "The new string to insert in place of oldString" },
|
|
63
|
+
replaceAll: { type: "boolean", description: "If true, replace all occurrences. If false (default), the operation will fail if multiple matches are found to prevent unintended replacements.", default: false }
|
|
64
|
+
},
|
|
65
|
+
required: ["path", "oldString", "newString"]
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "list_directory",
|
|
70
|
+
description: "List files and directories in the vault (includes non-note filenames, while read/write tools remain note-only)",
|
|
71
|
+
inputSchema: {
|
|
72
|
+
type: "object",
|
|
73
|
+
properties: {
|
|
74
|
+
path: { type: "string", description: "Path relative to vault root (default: '/')", default: "/" },
|
|
75
|
+
prettyPrint: { type: "boolean", description: "Format JSON response with indentation (default: false)", default: false }
|
|
155
76
|
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
},
|
|
166
|
-
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "delete_note",
|
|
81
|
+
description: "Delete a note from the Obsidian vault (requires confirmation). Supports permanent delete, vault trash, or system trash.",
|
|
82
|
+
inputSchema: {
|
|
83
|
+
type: "object",
|
|
84
|
+
properties: {
|
|
85
|
+
path: { type: "string", description: "Path to the note relative to vault root" },
|
|
86
|
+
confirmPath: { type: "string", description: "Confirmation: must exactly match the path parameter to proceed with deletion" },
|
|
87
|
+
trashMode: { type: "string", enum: ["none", "local", "system"], description: "Deletion mode: 'none' = permanent delete (default), 'local' = move to .trash inside vault, 'system' = move to OS trash", default: "none" }
|
|
88
|
+
},
|
|
89
|
+
required: ["path", "confirmPath"]
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "search_notes",
|
|
94
|
+
description: "Search for notes in the vault by content or frontmatter",
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: "object",
|
|
97
|
+
properties: {
|
|
98
|
+
query: { type: "string", description: "Search query text" },
|
|
99
|
+
limit: { type: "number", description: "Maximum number of results (default: 5, max: 20)", default: 5 },
|
|
100
|
+
searchContent: { type: "boolean", description: "Search in note content (default: true)", default: true },
|
|
101
|
+
searchFrontmatter: { type: "boolean", description: "Search in frontmatter (default: false)", default: false },
|
|
102
|
+
caseSensitive: { type: "boolean", description: "Case sensitive search (default: false)", default: false },
|
|
103
|
+
pathPrefix: { type: "string", description: "Restrict the search to a vault subtree, e.g. \"Projects/2026\" (directory prefix)" },
|
|
104
|
+
excludePaths: { type: "array", items: { type: "string" }, description: "Skip files under these subtrees, e.g. [\"Archive\", \"meta\"] (directory prefixes)" },
|
|
105
|
+
prettyPrint: { type: "boolean", description: "Format JSON response with indentation (default: false)", default: false }
|
|
106
|
+
},
|
|
107
|
+
required: ["query"]
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "move_note",
|
|
112
|
+
description: "Move or rename a note in the vault",
|
|
113
|
+
inputSchema: {
|
|
114
|
+
type: "object",
|
|
115
|
+
properties: {
|
|
116
|
+
oldPath: { type: "string", description: "Current path of the note" },
|
|
117
|
+
newPath: { type: "string", description: "New path for the note" },
|
|
118
|
+
overwrite: { type: "boolean", description: "Allow overwriting existing file (default: false)", default: false }
|
|
119
|
+
},
|
|
120
|
+
required: ["oldPath", "newPath"]
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: "move_file",
|
|
125
|
+
description: "Move or rename any file in the vault (binary-safe, file-only, requires confirmation)",
|
|
126
|
+
inputSchema: {
|
|
127
|
+
type: "object",
|
|
128
|
+
properties: {
|
|
129
|
+
oldPath: { type: "string", description: "Current path of the file" },
|
|
130
|
+
newPath: { type: "string", description: "New path for the file" },
|
|
131
|
+
confirmOldPath: { type: "string", description: "Confirmation: must exactly match oldPath" },
|
|
132
|
+
confirmNewPath: { type: "string", description: "Confirmation: must exactly match newPath" },
|
|
133
|
+
overwrite: { type: "boolean", description: "Allow overwriting existing file (default: false)", default: false }
|
|
134
|
+
},
|
|
135
|
+
required: ["oldPath", "newPath", "confirmOldPath", "confirmNewPath"]
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: "read_multiple_notes",
|
|
140
|
+
description: "Read multiple notes in a batch (max 10 files)",
|
|
141
|
+
inputSchema: {
|
|
142
|
+
type: "object",
|
|
143
|
+
properties: {
|
|
144
|
+
paths: { type: "array", items: { type: "string" }, description: "Array of note paths to read", maxItems: 10 },
|
|
145
|
+
includeContent: { type: "boolean", description: "Include note content (default: true)", default: true },
|
|
146
|
+
includeFrontmatter: { type: "boolean", description: "Include frontmatter (default: true)", default: true },
|
|
147
|
+
prettyPrint: { type: "boolean", description: "Format JSON response with indentation (default: false)", default: false }
|
|
148
|
+
},
|
|
149
|
+
required: ["paths"]
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: "update_frontmatter",
|
|
154
|
+
description: "Update frontmatter of a note without changing content",
|
|
155
|
+
inputSchema: {
|
|
156
|
+
type: "object",
|
|
157
|
+
properties: {
|
|
158
|
+
path: { type: "string", description: "Path to the note" },
|
|
159
|
+
frontmatter: { type: "object", description: "Frontmatter object to update" },
|
|
160
|
+
merge: { type: "boolean", description: "Merge with existing frontmatter (default: true)", default: true }
|
|
161
|
+
},
|
|
162
|
+
required: ["path", "frontmatter"]
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: "get_notes_info",
|
|
167
|
+
description: "Get metadata for notes without reading full content",
|
|
168
|
+
inputSchema: {
|
|
169
|
+
type: "object",
|
|
170
|
+
properties: {
|
|
171
|
+
paths: { type: "array", items: { type: "string" }, description: "Array of note paths to get info for" },
|
|
172
|
+
prettyPrint: { type: "boolean", description: "Format JSON response with indentation (default: false)", default: false }
|
|
173
|
+
},
|
|
174
|
+
required: ["paths"]
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: "get_frontmatter",
|
|
179
|
+
description: "Extract frontmatter from a note without reading the content",
|
|
180
|
+
inputSchema: {
|
|
181
|
+
type: "object",
|
|
182
|
+
properties: {
|
|
183
|
+
path: { type: "string", description: "Path to the note relative to vault root" },
|
|
184
|
+
prettyPrint: { type: "boolean", description: "Format JSON response with indentation (default: false)", default: false }
|
|
185
|
+
},
|
|
186
|
+
required: ["path"]
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: "manage_tags",
|
|
191
|
+
description: "Add, remove, or list tags in a note",
|
|
192
|
+
inputSchema: {
|
|
193
|
+
type: "object",
|
|
194
|
+
properties: {
|
|
195
|
+
path: { type: "string", description: "Path to the note relative to vault root" },
|
|
196
|
+
operation: { type: "string", enum: ["add", "remove", "list"], description: "Operation to perform: 'add', 'remove', or 'list'" },
|
|
197
|
+
tags: { type: "array", items: { type: "string" }, description: "Array of tags (required for 'add' and 'remove' operations)" }
|
|
198
|
+
},
|
|
199
|
+
required: ["path", "operation"]
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: "get_vault_stats",
|
|
204
|
+
description: "Get vault statistics including total notes, folders, size, and recently modified files. Useful for understanding vault scope before batch operations.",
|
|
205
|
+
inputSchema: {
|
|
206
|
+
type: "object",
|
|
207
|
+
properties: {
|
|
208
|
+
recentCount: { type: "number", description: "Number of recently modified files to return (default: 5, max: 20)", default: 5 },
|
|
209
|
+
prettyPrint: { type: "boolean", description: "Format JSON response with indentation (default: false)", default: false }
|
|
167
210
|
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
},
|
|
178
|
-
required: ["path"]
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: "list_all_tags",
|
|
215
|
+
description: "List all tags across the vault with occurrence counts. Returns both frontmatter tags and inline #hashtags, deduplicated and sorted by frequency. Useful for discovering existing tags before creating or organizing notes.",
|
|
216
|
+
inputSchema: {
|
|
217
|
+
type: "object",
|
|
218
|
+
properties: {
|
|
219
|
+
prettyPrint: { type: "boolean", description: "Format JSON response with indentation (default: false)", default: false }
|
|
179
220
|
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: "wiki_link",
|
|
225
|
+
description: "Read an Obsidian wiki link. Accepts the same syntax as Obsidian: [[Document Name]] or [[Document Name|Display Text]], including table-authored escapes like [[Document Name\\|Display]] and path-qualified links like [[folder/Document Name]]. A #fragment suffix in the input is ignored. Searches the vault for an exact basename match (or exact vault-relative path match when the name contains '/') and returns the file's content. When multiple files share the basename, picks the first (vault root first, then alphabetical by path) and lists the other paths in structuredContent.alternatives. Content is returned bare — ready for direct use in context.",
|
|
226
|
+
inputSchema: {
|
|
227
|
+
type: "object",
|
|
228
|
+
properties: {
|
|
229
|
+
document: {
|
|
230
|
+
type: "string",
|
|
231
|
+
description: "The document name — what goes inside [[ ]]. e.g. 'My-Document'. Brackets and display text (|...) are stripped if present. The .md extension is always appended (never include it)."
|
|
190
232
|
},
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
name: "get_vault_stats",
|
|
196
|
-
description: "Get vault statistics including total notes, folders, size, and recently modified files. Useful for understanding vault scope before batch operations.",
|
|
197
|
-
inputSchema: {
|
|
198
|
-
type: "object",
|
|
199
|
-
properties: {
|
|
200
|
-
recentCount: { type: "number", description: "Number of recently modified files to return (default: 5, max: 20)", default: 5 },
|
|
201
|
-
prettyPrint: { type: "boolean", description: "Format JSON response with indentation (default: false)", default: false }
|
|
233
|
+
prettyPrint: {
|
|
234
|
+
type: "boolean",
|
|
235
|
+
description: "Format JSON response with indentation (default: false)",
|
|
236
|
+
default: false
|
|
202
237
|
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
description: "Read an Obsidian wiki link. Accepts the same syntax as Obsidian: [[Document Name]] or [[Document Name|Display Text]], including table-authored escapes like [[Document Name\\|Display]] and path-qualified links like [[folder/Document Name]]. A #fragment suffix in the input is ignored. Searches the vault for an exact basename match (or exact vault-relative path match when the name contains '/') and returns the file's content. When multiple files share the basename, picks the first (vault root first, then alphabetical by path) and lists the other paths in structuredContent.alternatives. Content is returned bare — ready for direct use in context.",
|
|
218
|
-
inputSchema: {
|
|
219
|
-
type: "object",
|
|
220
|
-
properties: {
|
|
221
|
-
document: {
|
|
222
|
-
type: "string",
|
|
223
|
-
description: "The document name — what goes inside [[ ]]. e.g. 'My-Document'. Brackets and display text (|...) are stripped if present. The .md extension is always appended (never include it)."
|
|
224
|
-
},
|
|
225
|
-
prettyPrint: {
|
|
226
|
-
type: "boolean",
|
|
227
|
-
description: "Format JSON response with indentation (default: false)",
|
|
228
|
-
default: false
|
|
229
|
-
}
|
|
230
|
-
},
|
|
231
|
-
required: ["document"]
|
|
232
|
-
}
|
|
233
|
-
},
|
|
234
|
-
{
|
|
235
|
-
name: "get_note_outline",
|
|
236
|
-
description: "Get the heading structure of a note without reading its full content. Returns headings with level, text, and line number. Use this first to navigate large notes efficiently, then call read_note_lines to read only the section you need.",
|
|
237
|
-
inputSchema: {
|
|
238
|
-
type: "object",
|
|
239
|
-
properties: {
|
|
240
|
-
path: { type: "string", description: "Path to the note relative to vault root" },
|
|
241
|
-
prettyPrint: { type: "boolean", description: "Format JSON response with indentation (default: false)", default: false }
|
|
242
|
-
},
|
|
243
|
-
required: ["path"]
|
|
244
|
-
}
|
|
245
|
-
},
|
|
246
|
-
{
|
|
247
|
-
name: "read_note_lines",
|
|
248
|
-
description: "Read a specific line range from a note. Use after get_note_outline to read only the section you need instead of the full file.",
|
|
249
|
-
inputSchema: {
|
|
250
|
-
type: "object",
|
|
251
|
-
properties: {
|
|
252
|
-
path: { type: "string", description: "Path to the note relative to vault root" },
|
|
253
|
-
startLine: { type: "number", description: "First line to read (1-indexed, inclusive)" },
|
|
254
|
-
endLine: { type: "number", description: "Last line to read (1-indexed, inclusive)" },
|
|
255
|
-
prettyPrint: { type: "boolean", description: "Format JSON response with indentation (default: false)", default: false }
|
|
256
|
-
},
|
|
257
|
-
required: ["path", "startLine", "endLine"]
|
|
258
|
-
}
|
|
238
|
+
},
|
|
239
|
+
required: ["document"]
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
name: "get_note_outline",
|
|
244
|
+
description: "Get the heading structure of a note without reading its full content. Returns headings with level, text, and line number. Use this first to navigate large notes efficiently, then call read_note_lines to read only the section you need.",
|
|
245
|
+
inputSchema: {
|
|
246
|
+
type: "object",
|
|
247
|
+
properties: {
|
|
248
|
+
path: { type: "string", description: "Path to the note relative to vault root" },
|
|
249
|
+
prettyPrint: { type: "boolean", description: "Format JSON response with indentation (default: false)", default: false }
|
|
250
|
+
},
|
|
251
|
+
required: ["path"]
|
|
259
252
|
}
|
|
260
|
-
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: "read_note_lines",
|
|
256
|
+
description: "Read a specific line range from a note. Use after get_note_outline to read only the section you need instead of the full file.",
|
|
257
|
+
inputSchema: {
|
|
258
|
+
type: "object",
|
|
259
|
+
properties: {
|
|
260
|
+
path: { type: "string", description: "Path to the note relative to vault root" },
|
|
261
|
+
startLine: { type: "number", description: "First line to read (1-indexed, inclusive)" },
|
|
262
|
+
endLine: { type: "number", description: "Last line to read (1-indexed, inclusive)" },
|
|
263
|
+
prettyPrint: { type: "boolean", description: "Format JSON response with indentation (default: false)", default: false }
|
|
264
|
+
},
|
|
265
|
+
required: ["path", "startLine", "endLine"]
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
];
|
|
269
|
+
return {
|
|
270
|
+
tools: readOnly
|
|
271
|
+
? tools.filter((tool) => !MUTATING_TOOLS.has(tool.name))
|
|
272
|
+
: tools,
|
|
261
273
|
};
|
|
262
274
|
});
|
|
263
275
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
264
276
|
const { name: toolName, arguments: args } = request.params;
|
|
265
277
|
const trimmedArgs = trimPaths(args);
|
|
278
|
+
if (readOnly && MUTATING_TOOLS.has(toolName)) {
|
|
279
|
+
return {
|
|
280
|
+
content: [{
|
|
281
|
+
type: "text",
|
|
282
|
+
text: `Error: ${toolName} is disabled because MCPVault is running in read-only mode. Restart without --read-only to enable vault mutations.`,
|
|
283
|
+
}],
|
|
284
|
+
isError: true,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
266
287
|
try {
|
|
267
288
|
switch (toolName) {
|
|
268
289
|
case "read_note": {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pathfilter.d.ts","sourceRoot":"","sources":["../../src/pathfilter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,qBAAa,UAAU;IAOrB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAMxC;IAEH,OAAO,CAAC,eAAe,CAAW;IAClC,OAAO,CAAC,iBAAiB,CAAW;IAEpC,YAAY,MAAM,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,EAuB7C;IAED,OAAO,CAAC,eAAe;IAoBvB;;;;;;OAMG;IACH,OAAO,CAAC,oBAAoB;IAQ5B,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAuB/B;IAED,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAUzC;IAED,OAAO,CAAC,aAAa;
|
|
1
|
+
{"version":3,"file":"pathfilter.d.ts","sourceRoot":"","sources":["../../src/pathfilter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,qBAAa,UAAU;IAOrB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAMxC;IAEH,OAAO,CAAC,eAAe,CAAW;IAClC,OAAO,CAAC,iBAAiB,CAAW;IAEpC,YAAY,MAAM,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,EAuB7C;IAED,OAAO,CAAC,eAAe;IAoBvB;;;;;;OAMG;IACH,OAAO,CAAC,oBAAoB;IAQ5B,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAuB/B;IAED,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAUzC;IAED,OAAO,CAAC,aAAa;IA+BrB,OAAO,CAAC,MAAM;IA0Bd,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAErC;CACF"}
|
package/dist/src/pathfilter.js
CHANGED
|
@@ -99,10 +99,12 @@ export class PathFilter {
|
|
|
99
99
|
// (".Git/config", ".git./config", ".git /config") cannot bypass the deny-list
|
|
100
100
|
// on case-insensitive / Windows filesystems.
|
|
101
101
|
const canonicalPath = this.canonicalizeForMatch(normalizedPath);
|
|
102
|
-
// Deny restricted directories/files at any depth
|
|
103
|
-
//
|
|
102
|
+
// Deny hidden paths and restricted directories/files at any depth. Dotfiles
|
|
103
|
+
// are outside the public vault-note surface even when their suffix looks
|
|
104
|
+
// like an allowed note extension (for example, `.secrets.md`).
|
|
104
105
|
for (const segment of canonicalPath.split('/')) {
|
|
105
|
-
if (
|
|
106
|
+
if (segment.startsWith('.') ||
|
|
107
|
+
PathFilter.RESTRICTED_SEGMENTS.has(segment.toLowerCase())) {
|
|
106
108
|
return true;
|
|
107
109
|
}
|
|
108
110
|
}
|
package/package.json
CHANGED