@bitbonsai/mcpvault 0.14.1 → 0.16.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 +24 -1
- package/dist/server.js +13 -9
- 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 +3 -1
- package/dist/src/createServer.d.ts.map +1 -1
- package/dist/src/createServer.js +262 -242
- package/dist/src/wikilink/wikiLinkTool.d.ts +1 -1
- package/dist/src/wikilink/wikiLinkTool.d.ts.map +1 -1
- package/package.json +8 -3
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
|
|
|
@@ -991,6 +1012,8 @@ This MCP server implements several security measures to protect your Obsidian va
|
|
|
991
1012
|
4. Ensure all tests pass: `npm test`
|
|
992
1013
|
5. Submit a pull request
|
|
993
1014
|
|
|
1015
|
+
Maintainers: production publishing is driven by GitHub Releases. See [RELEASING.md](RELEASING.md).
|
|
1016
|
+
|
|
994
1017
|
## License
|
|
995
1018
|
|
|
996
1019
|
MIT
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
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,23 +33,26 @@ 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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
// Serve both the legacy handshake-based protocol and MCP 2026-07-28 from the
|
|
54
|
+
// same process. The opening exchange selects the era for this connection.
|
|
55
|
+
const serverHandle = serveStdio(() => createServer(vaultPath, { version: VERSION, readOnly }), { onerror: (error) => console.error(error) });
|
|
52
56
|
// Exit when the client disconnects (stdin EOF) or the process is asked to
|
|
53
57
|
// terminate. Hosts that don't send an MCP shutdown request otherwise leave
|
|
54
58
|
// this process running forever, orphaned once stdin closes (#159).
|
|
@@ -58,7 +62,7 @@ async function shutdown() {
|
|
|
58
62
|
return;
|
|
59
63
|
isShuttingDown = true;
|
|
60
64
|
try {
|
|
61
|
-
await
|
|
65
|
+
await serverHandle.close();
|
|
62
66
|
}
|
|
63
67
|
catch {
|
|
64
68
|
// Best-effort: exit regardless of transport close errors.
|
|
@@ -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
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Server } from "@modelcontextprotocol/
|
|
1
|
+
import { Server } from "@modelcontextprotocol/server";
|
|
2
2
|
import { FrontmatterHandler } from "./frontmatter.js";
|
|
3
3
|
import { PathFilter } from "./pathfilter.js";
|
|
4
4
|
export interface CreateServerOptions {
|
|
@@ -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,
|
|
1
|
+
{"version":3,"file":"createServer.d.ts","sourceRoot":"","sources":["../../src/createServer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAa,MAAM,8BAA8B,CAAC;AAEjE,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
|
@@ -1,268 +1,288 @@
|
|
|
1
|
-
import { Server } from "@modelcontextprotocol/
|
|
2
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
1
|
+
import { Server } from "@modelcontextprotocol/server";
|
|
3
2
|
import { FileSystemService } from "./filesystem.js";
|
|
4
3
|
import { FrontmatterHandler, parseFrontmatter } from "./frontmatter.js";
|
|
5
4
|
import { PathFilter } from "./pathfilter.js";
|
|
6
5
|
import { SearchService } from "./search.js";
|
|
7
6
|
import { handleWikiLinkTool } from "./wikilink/index.js";
|
|
8
7
|
import { resolve } from "path";
|
|
8
|
+
const MUTATING_TOOLS = new Set([
|
|
9
|
+
"write_note",
|
|
10
|
+
"patch_note",
|
|
11
|
+
"delete_note",
|
|
12
|
+
"move_note",
|
|
13
|
+
"move_file",
|
|
14
|
+
"update_frontmatter",
|
|
15
|
+
"manage_tags",
|
|
16
|
+
]);
|
|
9
17
|
export function createServer(vaultPath, options = {}) {
|
|
10
|
-
const { name = "mcpvault", version = "0.0.0", pathFilter = new PathFilter(), frontmatterHandler = new FrontmatterHandler(), } = options;
|
|
18
|
+
const { name = "mcpvault", version = "0.0.0", pathFilter = new PathFilter(), frontmatterHandler = new FrontmatterHandler(), readOnly = false, } = options;
|
|
11
19
|
const resolvedVaultPath = resolve(vaultPath);
|
|
12
20
|
const fileSystem = new FileSystemService(resolvedVaultPath, pathFilter, frontmatterHandler);
|
|
13
21
|
const searchService = new SearchService(resolvedVaultPath, pathFilter);
|
|
14
22
|
const server = new Server({ name, version }, {
|
|
15
23
|
capabilities: { tools: {} },
|
|
16
24
|
});
|
|
17
|
-
server.setRequestHandler(
|
|
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"]
|
|
25
|
+
server.setRequestHandler("tools/list", async () => {
|
|
26
|
+
const tools = [
|
|
27
|
+
{
|
|
28
|
+
name: "read_note",
|
|
29
|
+
description: "Read a note from the Obsidian vault",
|
|
30
|
+
inputSchema: {
|
|
31
|
+
type: "object",
|
|
32
|
+
properties: {
|
|
33
|
+
path: { type: "string", description: "Path to the note relative to vault root" },
|
|
34
|
+
prettyPrint: { type: "boolean", description: "Format JSON response with indentation (default: false)", default: false }
|
|
35
|
+
},
|
|
36
|
+
required: ["path"]
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "write_note",
|
|
41
|
+
description: "Write a note to the Obsidian vault",
|
|
42
|
+
inputSchema: {
|
|
43
|
+
type: "object",
|
|
44
|
+
properties: {
|
|
45
|
+
path: { type: "string", description: "Path to the note relative to vault root" },
|
|
46
|
+
content: { type: "string", description: "Content of the note" },
|
|
47
|
+
frontmatter: { type: "object", description: "Frontmatter object (optional)" },
|
|
48
|
+
mode: { type: "string", enum: ["overwrite", "append", "prepend"], description: "Write mode: 'overwrite' (default), 'append', or 'prepend'", default: "overwrite" }
|
|
49
|
+
},
|
|
50
|
+
required: ["path", "content"]
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: "patch_note",
|
|
55
|
+
description: "Efficiently update part of a note by replacing a specific string. This is more efficient than rewriting the entire note for small changes.",
|
|
56
|
+
inputSchema: {
|
|
57
|
+
type: "object",
|
|
58
|
+
properties: {
|
|
59
|
+
path: { type: "string", description: "Path to the note relative to vault root" },
|
|
60
|
+
oldString: { type: "string", description: "The exact string to replace. Must match exactly including whitespace and line breaks." },
|
|
61
|
+
newString: { type: "string", description: "The new string to insert in place of oldString" },
|
|
62
|
+
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 }
|
|
63
|
+
},
|
|
64
|
+
required: ["path", "oldString", "newString"]
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "list_directory",
|
|
69
|
+
description: "List files and directories in the vault (includes non-note filenames, while read/write tools remain note-only)",
|
|
70
|
+
inputSchema: {
|
|
71
|
+
type: "object",
|
|
72
|
+
properties: {
|
|
73
|
+
path: { type: "string", description: "Path relative to vault root (default: '/')", default: "/" },
|
|
74
|
+
prettyPrint: { type: "boolean", description: "Format JSON response with indentation (default: false)", default: false }
|
|
155
75
|
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
},
|
|
166
|
-
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "delete_note",
|
|
80
|
+
description: "Delete a note from the Obsidian vault (requires confirmation). Supports permanent delete, vault trash, or system trash.",
|
|
81
|
+
inputSchema: {
|
|
82
|
+
type: "object",
|
|
83
|
+
properties: {
|
|
84
|
+
path: { type: "string", description: "Path to the note relative to vault root" },
|
|
85
|
+
confirmPath: { type: "string", description: "Confirmation: must exactly match the path parameter to proceed with deletion" },
|
|
86
|
+
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" }
|
|
87
|
+
},
|
|
88
|
+
required: ["path", "confirmPath"]
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: "search_notes",
|
|
93
|
+
description: "Search for notes in the vault by content or frontmatter",
|
|
94
|
+
inputSchema: {
|
|
95
|
+
type: "object",
|
|
96
|
+
properties: {
|
|
97
|
+
query: { type: "string", description: "Search query text" },
|
|
98
|
+
limit: { type: "number", description: "Maximum number of results (default: 5, max: 20)", default: 5 },
|
|
99
|
+
searchContent: { type: "boolean", description: "Search in note content (default: true)", default: true },
|
|
100
|
+
searchFrontmatter: { type: "boolean", description: "Search in frontmatter (default: false)", default: false },
|
|
101
|
+
caseSensitive: { type: "boolean", description: "Case sensitive search (default: false)", default: false },
|
|
102
|
+
pathPrefix: { type: "string", description: "Restrict the search to a vault subtree, e.g. \"Projects/2026\" (directory prefix)" },
|
|
103
|
+
excludePaths: { type: "array", items: { type: "string" }, description: "Skip files under these subtrees, e.g. [\"Archive\", \"meta\"] (directory prefixes)" },
|
|
104
|
+
prettyPrint: { type: "boolean", description: "Format JSON response with indentation (default: false)", default: false }
|
|
105
|
+
},
|
|
106
|
+
required: ["query"]
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "move_note",
|
|
111
|
+
description: "Move or rename a note in the vault",
|
|
112
|
+
inputSchema: {
|
|
113
|
+
type: "object",
|
|
114
|
+
properties: {
|
|
115
|
+
oldPath: { type: "string", description: "Current path of the note" },
|
|
116
|
+
newPath: { type: "string", description: "New path for the note" },
|
|
117
|
+
overwrite: { type: "boolean", description: "Allow overwriting existing file (default: false)", default: false }
|
|
118
|
+
},
|
|
119
|
+
required: ["oldPath", "newPath"]
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: "move_file",
|
|
124
|
+
description: "Move or rename any file in the vault (binary-safe, file-only, requires confirmation)",
|
|
125
|
+
inputSchema: {
|
|
126
|
+
type: "object",
|
|
127
|
+
properties: {
|
|
128
|
+
oldPath: { type: "string", description: "Current path of the file" },
|
|
129
|
+
newPath: { type: "string", description: "New path for the file" },
|
|
130
|
+
confirmOldPath: { type: "string", description: "Confirmation: must exactly match oldPath" },
|
|
131
|
+
confirmNewPath: { type: "string", description: "Confirmation: must exactly match newPath" },
|
|
132
|
+
overwrite: { type: "boolean", description: "Allow overwriting existing file (default: false)", default: false }
|
|
133
|
+
},
|
|
134
|
+
required: ["oldPath", "newPath", "confirmOldPath", "confirmNewPath"]
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: "read_multiple_notes",
|
|
139
|
+
description: "Read multiple notes in a batch (max 10 files)",
|
|
140
|
+
inputSchema: {
|
|
141
|
+
type: "object",
|
|
142
|
+
properties: {
|
|
143
|
+
paths: { type: "array", items: { type: "string" }, description: "Array of note paths to read", maxItems: 10 },
|
|
144
|
+
includeContent: { type: "boolean", description: "Include note content (default: true)", default: true },
|
|
145
|
+
includeFrontmatter: { type: "boolean", description: "Include frontmatter (default: true)", default: true },
|
|
146
|
+
prettyPrint: { type: "boolean", description: "Format JSON response with indentation (default: false)", default: false }
|
|
147
|
+
},
|
|
148
|
+
required: ["paths"]
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: "update_frontmatter",
|
|
153
|
+
description: "Update frontmatter of a note without changing content",
|
|
154
|
+
inputSchema: {
|
|
155
|
+
type: "object",
|
|
156
|
+
properties: {
|
|
157
|
+
path: { type: "string", description: "Path to the note" },
|
|
158
|
+
frontmatter: { type: "object", description: "Frontmatter object to update" },
|
|
159
|
+
merge: { type: "boolean", description: "Merge with existing frontmatter (default: true)", default: true }
|
|
160
|
+
},
|
|
161
|
+
required: ["path", "frontmatter"]
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: "get_notes_info",
|
|
166
|
+
description: "Get metadata for notes without reading full content",
|
|
167
|
+
inputSchema: {
|
|
168
|
+
type: "object",
|
|
169
|
+
properties: {
|
|
170
|
+
paths: { type: "array", items: { type: "string" }, description: "Array of note paths to get info for" },
|
|
171
|
+
prettyPrint: { type: "boolean", description: "Format JSON response with indentation (default: false)", default: false }
|
|
172
|
+
},
|
|
173
|
+
required: ["paths"]
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: "get_frontmatter",
|
|
178
|
+
description: "Extract frontmatter from a note without reading the content",
|
|
179
|
+
inputSchema: {
|
|
180
|
+
type: "object",
|
|
181
|
+
properties: {
|
|
182
|
+
path: { type: "string", description: "Path to the note relative to vault root" },
|
|
183
|
+
prettyPrint: { type: "boolean", description: "Format JSON response with indentation (default: false)", default: false }
|
|
184
|
+
},
|
|
185
|
+
required: ["path"]
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: "manage_tags",
|
|
190
|
+
description: "Add, remove, or list tags in a note",
|
|
191
|
+
inputSchema: {
|
|
192
|
+
type: "object",
|
|
193
|
+
properties: {
|
|
194
|
+
path: { type: "string", description: "Path to the note relative to vault root" },
|
|
195
|
+
operation: { type: "string", enum: ["add", "remove", "list"], description: "Operation to perform: 'add', 'remove', or 'list'" },
|
|
196
|
+
tags: { type: "array", items: { type: "string" }, description: "Array of tags (required for 'add' and 'remove' operations)" }
|
|
197
|
+
},
|
|
198
|
+
required: ["path", "operation"]
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
name: "get_vault_stats",
|
|
203
|
+
description: "Get vault statistics including total notes, folders, size, and recently modified files. Useful for understanding vault scope before batch operations.",
|
|
204
|
+
inputSchema: {
|
|
205
|
+
type: "object",
|
|
206
|
+
properties: {
|
|
207
|
+
recentCount: { type: "number", description: "Number of recently modified files to return (default: 5, max: 20)", default: 5 },
|
|
208
|
+
prettyPrint: { type: "boolean", description: "Format JSON response with indentation (default: false)", default: false }
|
|
167
209
|
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
},
|
|
178
|
-
required: ["path"]
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: "list_all_tags",
|
|
214
|
+
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.",
|
|
215
|
+
inputSchema: {
|
|
216
|
+
type: "object",
|
|
217
|
+
properties: {
|
|
218
|
+
prettyPrint: { type: "boolean", description: "Format JSON response with indentation (default: false)", default: false }
|
|
179
219
|
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: "wiki_link",
|
|
224
|
+
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.",
|
|
225
|
+
inputSchema: {
|
|
226
|
+
type: "object",
|
|
227
|
+
properties: {
|
|
228
|
+
document: {
|
|
229
|
+
type: "string",
|
|
230
|
+
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
231
|
},
|
|
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 }
|
|
232
|
+
prettyPrint: {
|
|
233
|
+
type: "boolean",
|
|
234
|
+
description: "Format JSON response with indentation (default: false)",
|
|
235
|
+
default: false
|
|
202
236
|
}
|
|
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
|
-
}
|
|
237
|
+
},
|
|
238
|
+
required: ["document"]
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
name: "get_note_outline",
|
|
243
|
+
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.",
|
|
244
|
+
inputSchema: {
|
|
245
|
+
type: "object",
|
|
246
|
+
properties: {
|
|
247
|
+
path: { type: "string", description: "Path to the note relative to vault root" },
|
|
248
|
+
prettyPrint: { type: "boolean", description: "Format JSON response with indentation (default: false)", default: false }
|
|
249
|
+
},
|
|
250
|
+
required: ["path"]
|
|
259
251
|
}
|
|
260
|
-
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
name: "read_note_lines",
|
|
255
|
+
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.",
|
|
256
|
+
inputSchema: {
|
|
257
|
+
type: "object",
|
|
258
|
+
properties: {
|
|
259
|
+
path: { type: "string", description: "Path to the note relative to vault root" },
|
|
260
|
+
startLine: { type: "number", description: "First line to read (1-indexed, inclusive)" },
|
|
261
|
+
endLine: { type: "number", description: "Last line to read (1-indexed, inclusive)" },
|
|
262
|
+
prettyPrint: { type: "boolean", description: "Format JSON response with indentation (default: false)", default: false }
|
|
263
|
+
},
|
|
264
|
+
required: ["path", "startLine", "endLine"]
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
];
|
|
268
|
+
return {
|
|
269
|
+
tools: readOnly
|
|
270
|
+
? tools.filter((tool) => !MUTATING_TOOLS.has(tool.name))
|
|
271
|
+
: tools,
|
|
261
272
|
};
|
|
262
273
|
});
|
|
263
|
-
server.setRequestHandler(
|
|
274
|
+
server.setRequestHandler("tools/call", async (request) => {
|
|
264
275
|
const { name: toolName, arguments: args } = request.params;
|
|
265
276
|
const trimmedArgs = trimPaths(args);
|
|
277
|
+
if (readOnly && MUTATING_TOOLS.has(toolName)) {
|
|
278
|
+
return {
|
|
279
|
+
content: [{
|
|
280
|
+
type: "text",
|
|
281
|
+
text: `Error: ${toolName} is disabled because MCPVault is running in read-only mode. Restart without --read-only to enable vault mutations.`,
|
|
282
|
+
}],
|
|
283
|
+
isError: true,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
266
286
|
try {
|
|
267
287
|
switch (toolName) {
|
|
268
288
|
case "read_note": {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wikiLinkTool.d.ts","sourceRoot":"","sources":["../../../src/wikilink/wikiLinkTool.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"wikiLinkTool.d.ts","sourceRoot":"","sources":["../../../src/wikilink/wikiLinkTool.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAG1D,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,iBAAiB,EAC7B,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,cAAc,CAAC,CAiDzB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitbonsai/mcpvault",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Universal AI bridge for Obsidian vaults - connect any MCP-compatible assistant",
|
|
5
5
|
"homepage": "https://mcpvault.org",
|
|
6
6
|
"author": "bitbonsai",
|
|
@@ -33,15 +33,20 @@
|
|
|
33
33
|
"prepack": "npm install",
|
|
34
34
|
"publish:dry": "npm publish --dry-run",
|
|
35
35
|
"publish:beta": "npm publish --tag beta",
|
|
36
|
-
"publish:latest": "npm publish"
|
|
36
|
+
"publish:latest": "npm publish",
|
|
37
|
+
"ship": "bun scripts/ship.ts",
|
|
38
|
+
"ship:setup": "bun scripts/ship.ts --setup",
|
|
39
|
+
"ship:update": "bun scripts/ship.ts --update"
|
|
37
40
|
},
|
|
38
41
|
"dependencies": {
|
|
39
|
-
"@modelcontextprotocol/
|
|
42
|
+
"@modelcontextprotocol/server": "^2.0.0",
|
|
40
43
|
"gray-matter": "^4.0.3",
|
|
41
44
|
"trash": "^10.1.1",
|
|
42
45
|
"yaml": "^2.9.0"
|
|
43
46
|
},
|
|
44
47
|
"devDependencies": {
|
|
48
|
+
"@clack/prompts": "^0.7.0",
|
|
49
|
+
"@modelcontextprotocol/client": "^2.0.0",
|
|
45
50
|
"@types/node": "^26.1.2",
|
|
46
51
|
"tsx": "^4.23.9",
|
|
47
52
|
"typescript": "^7.0.2",
|