mcp-obsidian-cli 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -37,7 +37,7 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
37
37
  ## Requirements
38
38
 
39
39
  - Obsidian running with the CLI plugin active
40
- - `obsidian` on your PATH
40
+ - `obsidian-cli` on your PATH (typically `/Applications/Obsidian.app/Contents/MacOS/obsidian-cli` on macOS)
41
41
  - Node.js >= 18
42
42
 
43
43
  ## How it works
@@ -67,7 +67,7 @@ The generic `obsidian` tool means the MCP server never falls behind the CLI —
67
67
  | Variable | Default | Description |
68
68
  |---|---|---|
69
69
  | `OBSIDIAN_VAULT` | _(none)_ | Target vault by name |
70
- | `OBSIDIAN_CLI_PATH` | `obsidian` | Path to CLI binary |
70
+ | `OBSIDIAN_CLI_PATH` | `obsidian-cli` | Path to CLI binary |
71
71
  | `OBSIDIAN_TIMEOUT_MS` | `15000` | Command timeout |
72
72
  | `XDG_CONFIG_HOME` | `~/.config` | Base path for config file |
73
73
 
@@ -81,7 +81,7 @@ The server can read settings from a YAML config file:
81
81
  Config file format:
82
82
  ```yaml
83
83
  vault: "my-vault"
84
- cliPath: "obsidian"
84
+ cliPath: "obsidian-cli"
85
85
  timeoutMs: 15000
86
86
  ```
87
87
 
package/lib/helpers.js ADDED
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Pure helper functions extracted from server.js for testability.
3
+ */
4
+
5
+ import { readFileSync, existsSync } from "node:fs";
6
+ import { basename } from "node:path";
7
+ import { load as yamlLoad } from "js-yaml";
8
+
9
+ /**
10
+ * Load config from YAML file with env var overrides.
11
+ * @param {string} configFile - Path to the YAML config file.
12
+ * @returns {{ vault: string, cliPath: string, timeoutMs: number }}
13
+ */
14
+ export function loadConfig(configFile) {
15
+ const defaults = { vault: "", cliPath: "obsidian-cli", timeoutMs: 15000 };
16
+ let config = { ...defaults };
17
+
18
+ if (existsSync(configFile)) {
19
+ try {
20
+ const content = readFileSync(configFile, "utf8");
21
+ const fileConfig = yamlLoad(content);
22
+ if (fileConfig) {
23
+ if (fileConfig.vault) config.vault = fileConfig.vault;
24
+ if (fileConfig.cliPath) config.cliPath = fileConfig.cliPath;
25
+ if (fileConfig.timeoutMs) config.timeoutMs = fileConfig.timeoutMs;
26
+ }
27
+ } catch (err) {
28
+ console.error("Warning: failed to load config file:", err.message);
29
+ }
30
+ }
31
+
32
+ if (process.env.OBSIDIAN_VAULT) config.vault = process.env.OBSIDIAN_VAULT;
33
+ if (process.env.OBSIDIAN_CLI_PATH) config.cliPath = process.env.OBSIDIAN_CLI_PATH;
34
+ if (process.env.OBSIDIAN_TIMEOUT_MS) config.timeoutMs = parseInt(process.env.OBSIDIAN_TIMEOUT_MS, 10);
35
+
36
+ return config;
37
+ }
38
+
39
+ /**
40
+ * Minimal arg parser: splits on whitespace but respects key="value with spaces".
41
+ */
42
+ export function parseArgs(str) {
43
+ const args = [];
44
+ const re = /(?:[^\s"]+|"[^"]*")+/g;
45
+ let m;
46
+ while ((m = re.exec(str)) !== null) {
47
+ args.push(m[0].replace(/"([^"]*)"/g, "$1"));
48
+ }
49
+ return args;
50
+ }
51
+
52
+ /**
53
+ * Build CLI argv with vault= prepended when configured. Caller-supplied
54
+ * `vault=` (must be first token) wins — the configured vault is skipped so
55
+ * per-call overrides through the generic `obsidian` tool remain reachable.
56
+ */
57
+ export function buildCliArgs(input, vault) {
58
+ const args = Array.isArray(input) ? [...input] : parseArgs(input);
59
+ if (vault && !args[0]?.startsWith("vault=")) {
60
+ args.unshift(`vault=${vault}`);
61
+ }
62
+ return args;
63
+ }
64
+
65
+ /**
66
+ * Load the set of vault names Obsidian knows about by parsing the desktop
67
+ * app's registry file. Returns an empty Set if the file is missing or
68
+ * unreadable (non-macOS, fresh install, etc.) — callers should treat that
69
+ * case as "no validation possible, trust the configured vault".
70
+ *
71
+ * @param {string} obsidianJsonPath - Path to Obsidian's vaults registry.
72
+ * @returns {Set<string>} basenames of known vault paths.
73
+ */
74
+ export function loadKnownVaults(obsidianJsonPath) {
75
+ if (!existsSync(obsidianJsonPath)) return new Set();
76
+ try {
77
+ const content = readFileSync(obsidianJsonPath, "utf8");
78
+ const data = JSON.parse(content);
79
+ const vaults = data?.vaults || {};
80
+ const names = new Set();
81
+ for (const v of Object.values(vaults)) {
82
+ if (v?.path) names.add(basename(v.path));
83
+ }
84
+ return names;
85
+ } catch {
86
+ return new Set();
87
+ }
88
+ }
89
+
90
+ /**
91
+ * If the first token of args is `vault=NAME`, return NAME. Otherwise null.
92
+ * Mirrors the CLI's requirement that `vault=` be the first positional token
93
+ * when overriding the focused vault.
94
+ *
95
+ * @param {string[]} args - Parsed CLI args.
96
+ * @returns {string|null}
97
+ */
98
+ export function extractLeadingVault(args) {
99
+ const first = args[0];
100
+ if (typeof first === "string" && first.startsWith("vault=")) {
101
+ return first.slice("vault=".length);
102
+ }
103
+ return null;
104
+ }
105
+
106
+ /** ENOENT error string for a missing CLI binary. */
107
+ export function cliNotFoundMessage(cli) {
108
+ return `Obsidian CLI not found at: ${cli}. Set OBSIDIAN_CLI_PATH or ensure '${cli}' is on PATH.`;
109
+ }
110
+
111
+ /** Standard MCP text result. */
112
+ export function text(content) {
113
+ return { content: [{ type: "text", text: content }] };
114
+ }
115
+
116
+ /** Standard MCP error result. */
117
+ export function errorResult(content, code = "EXECUTION_ERROR") {
118
+ return {
119
+ content: [{ type: "text", text: content }],
120
+ isError: true,
121
+ };
122
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-obsidian-cli",
3
- "version": "1.1.0",
3
+ "version": "1.3.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/run.test.js"
17
+ "test": "node --test test/*.test.js"
17
18
  },
18
19
  "engines": {
19
20
  "node": ">=18"
package/server.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * common operations (read, search, daily, tasks, properties, etc.).
8
8
  *
9
9
  * Environment variables:
10
- * OBSIDIAN_CLI_PATH - Path to the obsidian CLI binary (default: "obsidian")
10
+ * OBSIDIAN_CLI_PATH - Path to the obsidian CLI binary (default: "obsidian-cli")
11
11
  * OBSIDIAN_VAULT - Vault name to use (default: "")
12
12
  * OBSIDIAN_TIMEOUT_MS - Command timeout in ms (default: 15000)
13
13
  *
@@ -22,12 +22,21 @@ 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, mkdirSync, existsSync } from "node:fs";
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 {
31
+ loadConfig,
32
+ text,
33
+ errorResult,
34
+ buildCliArgs,
35
+ cliNotFoundMessage,
36
+ loadKnownVaults,
37
+ extractLeadingVault,
38
+ parseArgs,
39
+ } from "./lib/helpers.js";
31
40
 
32
41
  const execFileAsync = promisify(execFile);
33
42
  const execAsync = promisify(exec);
@@ -46,42 +55,19 @@ const configBase = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
46
55
  const CONFIG_DIR = join(configBase, "mcp-obsidian-cli");
47
56
  const CONFIG_FILE = join(CONFIG_DIR, "config.yaml");
48
57
 
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
58
 
74
59
  const KNOWN_CLI_PATHS = [
75
- "/Applications/Obsidian.app/Contents/MacOS/obsidian",
76
- join(homedir(), "Applications/Obsidian.app/Contents/MacOS/obsidian"),
60
+ "/Applications/Obsidian.app/Contents/MacOS/obsidian-cli",
61
+ join(homedir(), "Applications/Obsidian.app/Contents/MacOS/obsidian-cli"),
77
62
  ];
78
63
 
79
64
  async function resolveCliPath(configured) {
80
- if (configured !== "obsidian") return configured;
65
+ if (configured !== "obsidian-cli") return configured;
81
66
 
82
67
  try {
83
- await execAsync("which obsidian", { timeout: 2000 });
84
- return configured;
68
+ const { stdout } = await execFileAsync("which", ["obsidian-cli"], { timeout: 2000 });
69
+ const resolved = stdout.trim();
70
+ if (resolved) return resolved;
85
71
  } catch { /* not on PATH */ }
86
72
 
87
73
  for (const p of KNOWN_CLI_PATHS) {
@@ -89,47 +75,77 @@ async function resolveCliPath(configured) {
89
75
  }
90
76
 
91
77
  try {
92
- const { stdout } = await execAsync(
93
- "ps aux | grep -i obsidian | grep -v grep | grep -v Helper",
78
+ const { stdout } = await execFileAsync(
79
+ "pgrep",
80
+ ["-lf", "/Applications/Obsidian.app/Contents/MacOS/Obsidian$"],
94
81
  { timeout: 2000 }
95
82
  );
96
- const match = stdout.match(/(\S*\/Contents\/MacOS\/obsidian)/i);
97
- if (match && existsSync(match[1])) return match[1];
83
+ const match = stdout.match(/(\S*\/Contents\/MacOS\/)Obsidian/i);
84
+ if (match) {
85
+ const cliPath = `${match[1]}obsidian-cli`;
86
+ if (existsSync(cliPath)) return cliPath;
87
+ }
98
88
  } catch { /* no running process */ }
99
89
 
100
90
  return configured;
101
91
  }
102
92
 
103
- const config = loadConfig();
93
+ const config = loadConfig(CONFIG_FILE);
104
94
  const CLI = await resolveCliPath(config.cliPath);
105
- const VAULT = config.vault;
106
95
  const TIMEOUT_MS = config.timeoutMs;
107
96
 
97
+ const OBSIDIAN_REGISTRY = join(
98
+ homedir(),
99
+ "Library/Application Support/obsidian/obsidian.json"
100
+ );
101
+ const KNOWN_VAULTS = loadKnownVaults(OBSIDIAN_REGISTRY);
102
+
103
+ // Runtime vault selection. Held in process memory only — not persisted.
104
+ // Initialized from config/env when that value names a known vault; otherwise
105
+ // null, which triggers the prompt-on-first-use flow. Caller-supplied
106
+ // `vault=NAME` in a generic `obsidian` call overrides + caches.
107
+ let runtimeVault = null;
108
+ if (config.vault) {
109
+ if (KNOWN_VAULTS.size === 0 || KNOWN_VAULTS.has(config.vault)) {
110
+ runtimeVault = config.vault;
111
+ } else {
112
+ console.error(
113
+ `Warning: configured OBSIDIAN_VAULT='${config.vault}' is not in Obsidian's known vaults ` +
114
+ `(${[...KNOWN_VAULTS].join(", ") || "<none detected>"}). ` +
115
+ `Server will ask which vault to use on first tool call.`
116
+ );
117
+ }
118
+ }
119
+
120
+ function vaultPromptResponse() {
121
+ const list = [...KNOWN_VAULTS].sort().map((v) => ` - ${v}`).join("\n");
122
+ return text(
123
+ `No vault selected. Available vaults:\n${list}\n\n` +
124
+ `Ask the user which vault to use, then either:\n` +
125
+ ` - retry through the generic \`obsidian\` tool with \`vault=NAME\` as the first token (e.g. \`vault=tyee read file="My Note"\`), or\n` +
126
+ ` - retry any convenience tool — the server will cache the vault from the first \`vault=\` override and reuse it for subsequent calls.\n\n` +
127
+ `If the user named a vault in conversation (e.g. "save this in tyee"), prepend \`vault=tyee\` automatically.`
128
+ );
129
+ }
130
+
131
+ const OBSIDIAN_PROCESS_PATTERN = "/Applications/Obsidian.app/Contents/MacOS/Obsidian$";
132
+ const RUNNING_CACHE_TTL_MS = 5000;
133
+ let runningCache = { value: null, at: 0 };
134
+
108
135
  async function checkObsidianRunning() {
136
+ const now = Date.now();
137
+ if (runningCache.value !== null && now - runningCache.at < RUNNING_CACHE_TTL_MS) {
138
+ return runningCache.value;
139
+ }
140
+ let running = false;
109
141
  try {
110
- const { stdout: psOut } = await execAsync("ps aux | grep -i obsidian | grep -v grep | grep -v Helper", { timeout: 2000 });
111
- const obsidianRunning = psOut.includes("/Applications/Obsidian.app");
112
- if (!obsidianRunning) {
113
- return { running: false, version: null };
114
- }
115
- const { stdout } = await execFileAsync(CLI, ["version"], { timeout: 2000 });
116
- const hasStartupMsg = stdout.includes("Loaded updated app package") ||
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 };
142
+ await execFileAsync("pgrep", ["-f", OBSIDIAN_PROCESS_PATTERN], { timeout: 2000 });
143
+ running = true;
144
+ } catch {
145
+ running = false;
132
146
  }
147
+ runningCache = { value: running, at: now };
148
+ return running;
133
149
  }
134
150
 
135
151
  // ---------------------------------------------------------------------------
@@ -140,9 +156,8 @@ async function checkObsidianRunning() {
140
156
  * Run the Obsidian CLI with the given argument string.
141
157
  * Returns { stdout, stderr } or throws on non-zero exit / timeout.
142
158
  */
143
- async function run(argString) {
144
- const args = parseArgs(argString);
145
- if (VAULT) args.push(`vault=${VAULT}`);
159
+ async function run(input) {
160
+ const args = buildCliArgs(input, runtimeVault);
146
161
 
147
162
  try {
148
163
  const { stdout, stderr } = await execFileAsync(CLI, args, {
@@ -158,7 +173,7 @@ async function run(argString) {
158
173
  stderr: '',
159
174
  error: {
160
175
  type: 'CLI_NOT_FOUND',
161
- message: `Obsidian CLI not found at: ${CLI}. Set OBSIDIAN_CLI_PATH or ensure 'obsidian' is on PATH.`
176
+ message: cliNotFoundMessage(CLI),
162
177
  }
163
178
  };
164
179
  }
@@ -177,35 +192,33 @@ async function run(argString) {
177
192
  }
178
193
  }
179
194
 
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
195
 
193
- /** Standard MCP text result. */
194
- function text(content) {
195
- return { content: [{ type: "text", text: content }] };
196
- }
196
+ /** Run CLI, return MCP result. Accepts a command string or an args array. */
197
+ async function runTool(input) {
198
+ if (!(await checkObsidianRunning())) {
199
+ return errorResult(
200
+ "Obsidian.app is not running. Open Obsidian and retry — no Claude Desktop restart needed.",
201
+ "OBSIDIAN_NOT_RUNNING"
202
+ );
203
+ }
197
204
 
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
+ // Cache caller-supplied vault override so subsequent convenience-tool calls
206
+ // route to the same vault without the caller having to repeat it.
207
+ const parsed = Array.isArray(input) ? input : parseArgs(input);
208
+ const callerVault = extractLeadingVault(parsed);
209
+ if (callerVault) {
210
+ if (KNOWN_VAULTS.size > 0 && !KNOWN_VAULTS.has(callerVault)) {
211
+ return errorResult(
212
+ `Unknown vault '${callerVault}'. Known vaults: ${[...KNOWN_VAULTS].sort().join(", ")}.`,
213
+ "VAULT_NOT_FOUND"
214
+ );
215
+ }
216
+ runtimeVault = callerVault;
217
+ } else if (!runtimeVault && KNOWN_VAULTS.size > 0) {
218
+ return vaultPromptResponse();
219
+ }
205
220
 
206
- /** Run CLI, return MCP result. */
207
- async function runTool(argString) {
208
- const { stdout, stderr, error } = await run(argString);
221
+ const { stdout, stderr, error } = await run(input);
209
222
  if (error) {
210
223
  return errorResult(error.message, error.type);
211
224
  }
@@ -221,7 +234,7 @@ async function runTool(argString) {
221
234
 
222
235
  const server = new McpServer({
223
236
  name: "obsidian-mcp",
224
- version: "1.1.0",
237
+ version: "1.3.0",
225
238
  capabilities: { tools: {} },
226
239
  });
227
240
 
@@ -231,6 +244,16 @@ server.tool(
231
244
  "obsidian",
232
245
  `Run any Obsidian CLI command. Pass the full command string exactly as you
233
246
  would on the terminal (minus the leading 'obsidian' binary name).
247
+
248
+ IMPORTANT: when multiple vaults are loaded, the CLI's vault= argument must
249
+ be the FIRST token. The server auto-prepends vault=<runtimeVault> once a
250
+ vault has been selected; if you include vault= manually in this command
251
+ string, put it first or the CLI silently routes to the focused vault.
252
+
253
+ VAULT ROUTING: if the user names a vault in conversation (e.g. "save this
254
+ in tyee", "scarp note"), prepend \`vault=NAME \` as the first token. The
255
+ server caches that selection in memory for subsequent calls.
256
+
234
257
  Examples:
235
258
  "daily:read"
236
259
  "search:context query=\"meeting notes\" limit=5"
@@ -238,6 +261,7 @@ Examples:
238
261
  "tags counts sort=count"
239
262
  "tasks daily"
240
263
  "property:read name=status path=\"1p/my-project/my-project.md\""
264
+ "vault=tyee read file=\"My Note\"" # explicit vault override, first
241
265
  "help search"`,
242
266
  { command: z.string().describe("CLI command and arguments") },
243
267
  async ({ command }) => runTool(command),
@@ -263,7 +287,7 @@ server.tool(
263
287
  "obsidian_daily_append",
264
288
  "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
289
  { content: z.string().describe("Content to append") },
266
- async ({ content }) => runTool(`daily:append content="${content.replace(/"/g, '\\"')}"`),
290
+ async ({ content }) => runTool(["daily:append", `content=${content}`]),
267
291
  );
268
292
 
269
293
  server.tool(
@@ -275,8 +299,10 @@ server.tool(
275
299
  },
276
300
  async ({ file, path }) => {
277
301
  if (!file && !path) return text("Error: provide file= or path=");
278
- const arg = file ? `file="${file}"` : `path="${path}"`;
279
- return runTool(`read ${arg}`);
302
+ const args = ["read"];
303
+ if (file) args.push(`file=${file}`);
304
+ if (path) args.push(`path=${path}`);
305
+ return runTool(args);
280
306
  },
281
307
  );
282
308
 
@@ -289,10 +315,10 @@ server.tool(
289
315
  limit: z.number().optional().describe("Max files to return"),
290
316
  },
291
317
  async ({ query, path, limit }) => {
292
- let cmd = `search:context query="${query.replace(/"/g, '\\"')}"`;
293
- if (path) cmd += ` path="${path}"`;
294
- if (limit) cmd += ` limit=${limit}`;
295
- return runTool(cmd);
318
+ const args = ["search:context", `query=${query}`];
319
+ if (path) args.push(`path=${path}`);
320
+ if (limit) args.push(`limit=${limit}`);
321
+ return runTool(args);
296
322
  },
297
323
  );
298
324
 
@@ -303,9 +329,9 @@ server.tool(
303
329
  sort: z.enum(["name", "count"]).optional().describe("Sort order"),
304
330
  },
305
331
  async ({ sort }) => {
306
- let cmd = "tags counts";
307
- if (sort) cmd += ` sort=${sort}`;
308
- return runTool(cmd);
332
+ const args = ["tags", "counts"];
333
+ if (sort) args.push(`sort=${sort}`);
334
+ return runTool(args);
309
335
  },
310
336
  );
311
337
 
@@ -319,12 +345,12 @@ server.tool(
319
345
  path: z.string().optional().describe("Filter by file path"),
320
346
  },
321
347
  async ({ daily, todo, done, path }) => {
322
- let cmd = "tasks";
323
- if (daily) cmd += " daily";
324
- if (todo) cmd += " todo";
325
- if (done) cmd += " done";
326
- if (path) cmd += ` path="${path}"`;
327
- return runTool(cmd);
348
+ const args = ["tasks"];
349
+ if (daily) args.push("daily");
350
+ if (todo) args.push("todo");
351
+ if (done) args.push("done");
352
+ if (path) args.push(`path=${path}`);
353
+ return runTool(args);
328
354
  },
329
355
  );
330
356
 
@@ -338,15 +364,16 @@ server.tool(
338
364
  },
339
365
  async ({ file, path, name }) => {
340
366
  if (name && (file || path)) {
341
- // Read a specific property from a specific file
342
- const target = file ? `file="${file}"` : `path="${path}"`;
343
- return runTool(`property:read name="${name}" ${target}`);
367
+ const args = ["property:read", `name=${name}`];
368
+ if (file) args.push(`file=${file}`);
369
+ if (path) args.push(`path=${path}`);
370
+ return runTool(args);
344
371
  }
345
- let cmd = "properties";
346
- if (file) cmd += ` file="${file}"`;
347
- if (path) cmd += ` path="${path}"`;
348
- cmd += " counts";
349
- return runTool(cmd);
372
+ const args = ["properties"];
373
+ if (file) args.push(`file=${file}`);
374
+ if (path) args.push(`path=${path}`);
375
+ args.push("counts");
376
+ return runTool(args);
350
377
  },
351
378
  );
352
379
 
@@ -360,12 +387,12 @@ server.tool(
360
387
  template: z.string().optional().describe("Template to use"),
361
388
  },
362
389
  async ({ name, path, content, template }) => {
363
- let cmd = "create";
364
- if (name) cmd += ` name="${name}"`;
365
- if (path) cmd += ` path="${path}"`;
366
- if (template) cmd += ` template="${template}"`;
367
- if (content) cmd += ` content="${content.replace(/"/g, '\\"')}"`;
368
- return runTool(cmd);
390
+ const args = ["create"];
391
+ if (name) args.push(`name=${name}`);
392
+ if (path) args.push(`path=${path}`);
393
+ if (template) args.push(`template=${template}`);
394
+ if (content) args.push(`content=${content}`);
395
+ return runTool(args);
369
396
  },
370
397
  );
371
398
 
@@ -379,9 +406,11 @@ server.tool(
379
406
  path: z.string().optional().describe("File path"),
380
407
  },
381
408
  async ({ name, value, file, path: filePath }) => {
382
- const target = file ? `file="${file}"` : filePath ? `path="${filePath}"` : "";
383
- if (!target) return text("Error: provide file= or path=");
384
- return runTool(`property:set name="${name}" value="${value.replace(/"/g, '\\"')}" ${target}`);
409
+ if (!file && !filePath) return text("Error: provide file= or path=");
410
+ const args = ["property:set", `name=${name}`, `value=${value}`];
411
+ if (file) args.push(`file=${file}`);
412
+ if (filePath) args.push(`path=${filePath}`);
413
+ return runTool(args);
385
414
  },
386
415
  );
387
416
 
@@ -393,8 +422,11 @@ server.tool(
393
422
  path: z.string().optional().describe("File path"),
394
423
  },
395
424
  async ({ file, path }) => {
396
- const target = file ? `file="${file}"` : path ? `path="${path}"` : "";
397
- return runTool(`backlinks ${target} counts`);
425
+ const args = ["backlinks"];
426
+ if (file) args.push(`file=${file}`);
427
+ if (path) args.push(`path=${path}`);
428
+ args.push("counts");
429
+ return runTool(args);
398
430
  },
399
431
  );
400
432
 
@@ -406,10 +438,10 @@ server.tool(
406
438
  ext: z.string().optional().describe("Filter by extension"),
407
439
  },
408
440
  async ({ folder, ext }) => {
409
- let cmd = "files";
410
- if (folder) cmd += ` folder="${folder}"`;
411
- if (ext) cmd += ` ext=${ext}`;
412
- return runTool(cmd);
441
+ const args = ["files"];
442
+ if (folder) args.push(`folder=${folder}`);
443
+ if (ext) args.push(`ext=${ext}`);
444
+ return runTool(args);
413
445
  },
414
446
  );
415
447
 
@@ -464,13 +496,14 @@ for (const [name, content] of Object.entries(promptContent)) {
464
496
  // ---- Start ---------------------------------------------------------------
465
497
 
466
498
  async function main() {
467
- const { running, version } = await checkObsidianRunning();
499
+ const running = await checkObsidianRunning();
468
500
  if (!running) {
469
- console.error("Error: Obsidian is not running. Please open Obsidian and try again.");
470
- process.exit(1);
501
+ console.error(
502
+ "Warning: Obsidian.app not detected. Server will accept connections; tool calls will fail until Obsidian is opened."
503
+ );
471
504
  }
472
505
  const transport = new StdioServerTransport();
473
- console.error(`obsidian-mcp server running on stdio (Obsidian ${version})`);
506
+ console.error("obsidian-mcp server running on stdio");
474
507
  await server.connect(transport);
475
508
  }
476
509