mcp-obsidian-cli 1.1.0 → 1.2.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/lib/helpers.js +62 -0
- package/package.json +3 -2
- package/server.js +65 -121
package/lib/helpers.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helper functions extracted from server.js for testability.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
6
|
+
import { load as yamlLoad } from "js-yaml";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Load config from YAML file with env var overrides.
|
|
10
|
+
* @param {string} configFile - Path to the YAML config file.
|
|
11
|
+
* @returns {{ vault: string, cliPath: string, timeoutMs: number }}
|
|
12
|
+
*/
|
|
13
|
+
export function loadConfig(configFile) {
|
|
14
|
+
const defaults = { vault: "", cliPath: "obsidian", timeoutMs: 15000 };
|
|
15
|
+
let config = { ...defaults };
|
|
16
|
+
|
|
17
|
+
if (existsSync(configFile)) {
|
|
18
|
+
try {
|
|
19
|
+
const content = readFileSync(configFile, "utf8");
|
|
20
|
+
const fileConfig = yamlLoad(content);
|
|
21
|
+
if (fileConfig) {
|
|
22
|
+
if (fileConfig.vault) config.vault = fileConfig.vault;
|
|
23
|
+
if (fileConfig.cliPath) config.cliPath = fileConfig.cliPath;
|
|
24
|
+
if (fileConfig.timeoutMs) config.timeoutMs = fileConfig.timeoutMs;
|
|
25
|
+
}
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.error("Warning: failed to load config file:", err.message);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (process.env.OBSIDIAN_VAULT) config.vault = process.env.OBSIDIAN_VAULT;
|
|
32
|
+
if (process.env.OBSIDIAN_CLI_PATH) config.cliPath = process.env.OBSIDIAN_CLI_PATH;
|
|
33
|
+
if (process.env.OBSIDIAN_TIMEOUT_MS) config.timeoutMs = parseInt(process.env.OBSIDIAN_TIMEOUT_MS, 10);
|
|
34
|
+
|
|
35
|
+
return config;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Minimal arg parser: splits on whitespace but respects key="value with spaces".
|
|
40
|
+
*/
|
|
41
|
+
export function parseArgs(str) {
|
|
42
|
+
const args = [];
|
|
43
|
+
const re = /(?:[^\s"]+|"[^"]*")+/g;
|
|
44
|
+
let m;
|
|
45
|
+
while ((m = re.exec(str)) !== null) {
|
|
46
|
+
args.push(m[0].replace(/"([^"]*)"/g, "$1"));
|
|
47
|
+
}
|
|
48
|
+
return args;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Standard MCP text result. */
|
|
52
|
+
export function text(content) {
|
|
53
|
+
return { content: [{ type: "text", text: content }] };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Standard MCP error result. */
|
|
57
|
+
export function errorResult(content, code = "EXECUTION_ERROR") {
|
|
58
|
+
return {
|
|
59
|
+
content: [{ type: "text", text: content }],
|
|
60
|
+
isError: true,
|
|
61
|
+
};
|
|
62
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-obsidian-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MCP server wrapping the Obsidian CLI — full native API access over Model Context Protocol",
|
|
6
6
|
"main": "server.js",
|
|
@@ -9,11 +9,12 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"server.js",
|
|
12
|
+
"lib/",
|
|
12
13
|
"prompts/"
|
|
13
14
|
],
|
|
14
15
|
"scripts": {
|
|
15
16
|
"start": "node server.js",
|
|
16
|
-
"test": "node --test test
|
|
17
|
+
"test": "node --test test/*.test.js"
|
|
17
18
|
},
|
|
18
19
|
"engines": {
|
|
19
20
|
"node": ">=18"
|
package/server.js
CHANGED
|
@@ -22,12 +22,12 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
22
22
|
import { z } from "zod";
|
|
23
23
|
import { execFile } from "node:child_process";
|
|
24
24
|
import { promisify } from "node:util";
|
|
25
|
-
import { readFileSync,
|
|
26
|
-
import { load as yamlLoad } from "js-yaml";
|
|
25
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
27
26
|
import { homedir } from "node:os";
|
|
28
27
|
import { join, dirname } from "node:path";
|
|
29
28
|
import { fileURLToPath } from "node:url";
|
|
30
29
|
import { exec } from "node:child_process";
|
|
30
|
+
import { loadConfig, parseArgs, text, errorResult } from "./lib/helpers.js";
|
|
31
31
|
|
|
32
32
|
const execFileAsync = promisify(execFile);
|
|
33
33
|
const execAsync = promisify(exec);
|
|
@@ -46,30 +46,6 @@ const configBase = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
|
46
46
|
const CONFIG_DIR = join(configBase, "mcp-obsidian-cli");
|
|
47
47
|
const CONFIG_FILE = join(CONFIG_DIR, "config.yaml");
|
|
48
48
|
|
|
49
|
-
function loadConfig() {
|
|
50
|
-
const defaults = { vault: "", cliPath: "obsidian", timeoutMs: 15000 };
|
|
51
|
-
let config = { ...defaults };
|
|
52
|
-
|
|
53
|
-
if (existsSync(CONFIG_FILE)) {
|
|
54
|
-
try {
|
|
55
|
-
const content = readFileSync(CONFIG_FILE, "utf8");
|
|
56
|
-
const fileConfig = yamlLoad(content);
|
|
57
|
-
if (fileConfig) {
|
|
58
|
-
if (fileConfig.vault) config.vault = fileConfig.vault;
|
|
59
|
-
if (fileConfig.cliPath) config.cliPath = fileConfig.cliPath;
|
|
60
|
-
if (fileConfig.timeoutMs) config.timeoutMs = fileConfig.timeoutMs;
|
|
61
|
-
}
|
|
62
|
-
} catch (err) {
|
|
63
|
-
console.error("Warning: failed to load config file:", err.message);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (process.env.OBSIDIAN_VAULT) config.vault = process.env.OBSIDIAN_VAULT;
|
|
68
|
-
if (process.env.OBSIDIAN_CLI_PATH) config.cliPath = process.env.OBSIDIAN_CLI_PATH;
|
|
69
|
-
if (process.env.OBSIDIAN_TIMEOUT_MS) config.timeoutMs = parseInt(process.env.OBSIDIAN_TIMEOUT_MS, 10);
|
|
70
|
-
|
|
71
|
-
return config;
|
|
72
|
-
}
|
|
73
49
|
|
|
74
50
|
const KNOWN_CLI_PATHS = [
|
|
75
51
|
"/Applications/Obsidian.app/Contents/MacOS/obsidian",
|
|
@@ -100,35 +76,20 @@ async function resolveCliPath(configured) {
|
|
|
100
76
|
return configured;
|
|
101
77
|
}
|
|
102
78
|
|
|
103
|
-
const config = loadConfig();
|
|
79
|
+
const config = loadConfig(CONFIG_FILE);
|
|
104
80
|
const CLI = await resolveCliPath(config.cliPath);
|
|
105
81
|
const VAULT = config.vault;
|
|
106
82
|
const TIMEOUT_MS = config.timeoutMs;
|
|
107
83
|
|
|
108
84
|
async function checkObsidianRunning() {
|
|
109
85
|
try {
|
|
110
|
-
const { stdout
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
stdout.includes("Checking for update") ||
|
|
118
|
-
stdout.includes("App is up to date") ||
|
|
119
|
-
stdout.includes("Latest version is");
|
|
120
|
-
if (hasStartupMsg) {
|
|
121
|
-
return { running: false, version: null };
|
|
122
|
-
}
|
|
123
|
-
if (stdout.includes("(installer")) {
|
|
124
|
-
const match = stdout.match(/(\d+\.\d+\.\d+)/);
|
|
125
|
-
if (match) {
|
|
126
|
-
return { running: true, version: match[1] };
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return { running: false, version: null };
|
|
130
|
-
} catch (err) {
|
|
131
|
-
return { running: false, version: null };
|
|
86
|
+
const { stdout } = await execAsync(
|
|
87
|
+
"ps aux | grep -i obsidian | grep -v grep | grep -v Helper",
|
|
88
|
+
{ timeout: 2000 }
|
|
89
|
+
);
|
|
90
|
+
return stdout.includes("/Applications/Obsidian.app");
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
132
93
|
}
|
|
133
94
|
}
|
|
134
95
|
|
|
@@ -140,8 +101,8 @@ async function checkObsidianRunning() {
|
|
|
140
101
|
* Run the Obsidian CLI with the given argument string.
|
|
141
102
|
* Returns { stdout, stderr } or throws on non-zero exit / timeout.
|
|
142
103
|
*/
|
|
143
|
-
async function run(
|
|
144
|
-
const args = parseArgs(
|
|
104
|
+
async function run(input) {
|
|
105
|
+
const args = Array.isArray(input) ? [...input] : parseArgs(input);
|
|
145
106
|
if (VAULT) args.push(`vault=${VAULT}`);
|
|
146
107
|
|
|
147
108
|
try {
|
|
@@ -177,35 +138,10 @@ async function run(argString) {
|
|
|
177
138
|
}
|
|
178
139
|
}
|
|
179
140
|
|
|
180
|
-
/**
|
|
181
|
-
* Minimal arg parser: splits on whitespace but respects key="value with spaces".
|
|
182
|
-
*/
|
|
183
|
-
function parseArgs(str) {
|
|
184
|
-
const args = [];
|
|
185
|
-
const re = /(?:[^\s"]+|"[^"]*")+/g;
|
|
186
|
-
let m;
|
|
187
|
-
while ((m = re.exec(str)) !== null) {
|
|
188
|
-
args.push(m[0]);
|
|
189
|
-
}
|
|
190
|
-
return args;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/** Standard MCP text result. */
|
|
194
|
-
function text(content) {
|
|
195
|
-
return { content: [{ type: "text", text: content }] };
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/** Standard MCP error result. */
|
|
199
|
-
function errorResult(content, code = "EXECUTION_ERROR") {
|
|
200
|
-
return {
|
|
201
|
-
content: [{ type: "text", text: content }],
|
|
202
|
-
isError: true,
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
141
|
|
|
206
|
-
/** Run CLI, return MCP result. */
|
|
207
|
-
async function runTool(
|
|
208
|
-
const { stdout, stderr, error } = await run(
|
|
142
|
+
/** Run CLI, return MCP result. Accepts a command string or an args array. */
|
|
143
|
+
async function runTool(input) {
|
|
144
|
+
const { stdout, stderr, error } = await run(input);
|
|
209
145
|
if (error) {
|
|
210
146
|
return errorResult(error.message, error.type);
|
|
211
147
|
}
|
|
@@ -221,7 +157,7 @@ async function runTool(argString) {
|
|
|
221
157
|
|
|
222
158
|
const server = new McpServer({
|
|
223
159
|
name: "obsidian-mcp",
|
|
224
|
-
version: "1.
|
|
160
|
+
version: "1.2.0",
|
|
225
161
|
capabilities: { tools: {} },
|
|
226
162
|
});
|
|
227
163
|
|
|
@@ -263,7 +199,7 @@ server.tool(
|
|
|
263
199
|
"obsidian_daily_append",
|
|
264
200
|
"Append content to today's daily note.\n\nParameters:\n content (required) — markdown text to append at the end of today's daily note\n\nExamples:\n obsidian_daily_append({ content: \"- Meeting with team at 3pm\" })\n obsidian_daily_append({ content: \"> [!tip] Remember\\n> Review PR before EOD\" })",
|
|
265
201
|
{ content: z.string().describe("Content to append") },
|
|
266
|
-
async ({ content }) => runTool(
|
|
202
|
+
async ({ content }) => runTool(["daily:append", `content=${content}`]),
|
|
267
203
|
);
|
|
268
204
|
|
|
269
205
|
server.tool(
|
|
@@ -275,8 +211,10 @@ server.tool(
|
|
|
275
211
|
},
|
|
276
212
|
async ({ file, path }) => {
|
|
277
213
|
if (!file && !path) return text("Error: provide file= or path=");
|
|
278
|
-
const
|
|
279
|
-
|
|
214
|
+
const args = ["read"];
|
|
215
|
+
if (file) args.push(`file=${file}`);
|
|
216
|
+
if (path) args.push(`path=${path}`);
|
|
217
|
+
return runTool(args);
|
|
280
218
|
},
|
|
281
219
|
);
|
|
282
220
|
|
|
@@ -289,10 +227,10 @@ server.tool(
|
|
|
289
227
|
limit: z.number().optional().describe("Max files to return"),
|
|
290
228
|
},
|
|
291
229
|
async ({ query, path, limit }) => {
|
|
292
|
-
|
|
293
|
-
if (path)
|
|
294
|
-
if (limit)
|
|
295
|
-
return runTool(
|
|
230
|
+
const args = ["search:context", `query=${query}`];
|
|
231
|
+
if (path) args.push(`path=${path}`);
|
|
232
|
+
if (limit) args.push(`limit=${limit}`);
|
|
233
|
+
return runTool(args);
|
|
296
234
|
},
|
|
297
235
|
);
|
|
298
236
|
|
|
@@ -303,9 +241,9 @@ server.tool(
|
|
|
303
241
|
sort: z.enum(["name", "count"]).optional().describe("Sort order"),
|
|
304
242
|
},
|
|
305
243
|
async ({ sort }) => {
|
|
306
|
-
|
|
307
|
-
if (sort)
|
|
308
|
-
return runTool(
|
|
244
|
+
const args = ["tags", "counts"];
|
|
245
|
+
if (sort) args.push(`sort=${sort}`);
|
|
246
|
+
return runTool(args);
|
|
309
247
|
},
|
|
310
248
|
);
|
|
311
249
|
|
|
@@ -319,12 +257,12 @@ server.tool(
|
|
|
319
257
|
path: z.string().optional().describe("Filter by file path"),
|
|
320
258
|
},
|
|
321
259
|
async ({ daily, todo, done, path }) => {
|
|
322
|
-
|
|
323
|
-
if (daily)
|
|
324
|
-
if (todo)
|
|
325
|
-
if (done)
|
|
326
|
-
if (path)
|
|
327
|
-
return runTool(
|
|
260
|
+
const args = ["tasks"];
|
|
261
|
+
if (daily) args.push("daily");
|
|
262
|
+
if (todo) args.push("todo");
|
|
263
|
+
if (done) args.push("done");
|
|
264
|
+
if (path) args.push(`path=${path}`);
|
|
265
|
+
return runTool(args);
|
|
328
266
|
},
|
|
329
267
|
);
|
|
330
268
|
|
|
@@ -338,15 +276,16 @@ server.tool(
|
|
|
338
276
|
},
|
|
339
277
|
async ({ file, path, name }) => {
|
|
340
278
|
if (name && (file || path)) {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
279
|
+
const args = ["property:read", `name=${name}`];
|
|
280
|
+
if (file) args.push(`file=${file}`);
|
|
281
|
+
if (path) args.push(`path=${path}`);
|
|
282
|
+
return runTool(args);
|
|
344
283
|
}
|
|
345
|
-
|
|
346
|
-
if (file)
|
|
347
|
-
if (path)
|
|
348
|
-
|
|
349
|
-
return runTool(
|
|
284
|
+
const args = ["properties"];
|
|
285
|
+
if (file) args.push(`file=${file}`);
|
|
286
|
+
if (path) args.push(`path=${path}`);
|
|
287
|
+
args.push("counts");
|
|
288
|
+
return runTool(args);
|
|
350
289
|
},
|
|
351
290
|
);
|
|
352
291
|
|
|
@@ -360,12 +299,12 @@ server.tool(
|
|
|
360
299
|
template: z.string().optional().describe("Template to use"),
|
|
361
300
|
},
|
|
362
301
|
async ({ name, path, content, template }) => {
|
|
363
|
-
|
|
364
|
-
if (name)
|
|
365
|
-
if (path)
|
|
366
|
-
if (template)
|
|
367
|
-
if (content)
|
|
368
|
-
return runTool(
|
|
302
|
+
const args = ["create"];
|
|
303
|
+
if (name) args.push(`name=${name}`);
|
|
304
|
+
if (path) args.push(`path=${path}`);
|
|
305
|
+
if (template) args.push(`template=${template}`);
|
|
306
|
+
if (content) args.push(`content=${content}`);
|
|
307
|
+
return runTool(args);
|
|
369
308
|
},
|
|
370
309
|
);
|
|
371
310
|
|
|
@@ -379,9 +318,11 @@ server.tool(
|
|
|
379
318
|
path: z.string().optional().describe("File path"),
|
|
380
319
|
},
|
|
381
320
|
async ({ name, value, file, path: filePath }) => {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
321
|
+
if (!file && !filePath) return text("Error: provide file= or path=");
|
|
322
|
+
const args = ["property:set", `name=${name}`, `value=${value}`];
|
|
323
|
+
if (file) args.push(`file=${file}`);
|
|
324
|
+
if (filePath) args.push(`path=${filePath}`);
|
|
325
|
+
return runTool(args);
|
|
385
326
|
},
|
|
386
327
|
);
|
|
387
328
|
|
|
@@ -393,8 +334,11 @@ server.tool(
|
|
|
393
334
|
path: z.string().optional().describe("File path"),
|
|
394
335
|
},
|
|
395
336
|
async ({ file, path }) => {
|
|
396
|
-
const
|
|
397
|
-
|
|
337
|
+
const args = ["backlinks"];
|
|
338
|
+
if (file) args.push(`file=${file}`);
|
|
339
|
+
if (path) args.push(`path=${path}`);
|
|
340
|
+
args.push("counts");
|
|
341
|
+
return runTool(args);
|
|
398
342
|
},
|
|
399
343
|
);
|
|
400
344
|
|
|
@@ -406,10 +350,10 @@ server.tool(
|
|
|
406
350
|
ext: z.string().optional().describe("Filter by extension"),
|
|
407
351
|
},
|
|
408
352
|
async ({ folder, ext }) => {
|
|
409
|
-
|
|
410
|
-
if (folder)
|
|
411
|
-
if (ext)
|
|
412
|
-
return runTool(
|
|
353
|
+
const args = ["files"];
|
|
354
|
+
if (folder) args.push(`folder=${folder}`);
|
|
355
|
+
if (ext) args.push(`ext=${ext}`);
|
|
356
|
+
return runTool(args);
|
|
413
357
|
},
|
|
414
358
|
);
|
|
415
359
|
|
|
@@ -464,13 +408,13 @@ for (const [name, content] of Object.entries(promptContent)) {
|
|
|
464
408
|
// ---- Start ---------------------------------------------------------------
|
|
465
409
|
|
|
466
410
|
async function main() {
|
|
467
|
-
const
|
|
411
|
+
const running = await checkObsidianRunning();
|
|
468
412
|
if (!running) {
|
|
469
413
|
console.error("Error: Obsidian is not running. Please open Obsidian and try again.");
|
|
470
414
|
process.exit(1);
|
|
471
415
|
}
|
|
472
416
|
const transport = new StdioServerTransport();
|
|
473
|
-
console.error(
|
|
417
|
+
console.error("obsidian-mcp server running on stdio");
|
|
474
418
|
await server.connect(transport);
|
|
475
419
|
}
|
|
476
420
|
|