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 +3 -3
- package/lib/helpers.js +122 -0
- package/package.json +3 -2
- package/server.js +167 -134
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.
|
|
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
|
|
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,
|
|
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
|
|
84
|
-
|
|
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
|
|
93
|
-
"
|
|
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\/
|
|
97
|
-
if (match
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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(
|
|
144
|
-
const args =
|
|
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:
|
|
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
|
-
/**
|
|
194
|
-
function
|
|
195
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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
|
|
279
|
-
|
|
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
|
-
|
|
293
|
-
if (path)
|
|
294
|
-
if (limit)
|
|
295
|
-
return runTool(
|
|
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
|
-
|
|
307
|
-
if (sort)
|
|
308
|
-
return runTool(
|
|
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
|
-
|
|
323
|
-
if (daily)
|
|
324
|
-
if (todo)
|
|
325
|
-
if (done)
|
|
326
|
-
if (path)
|
|
327
|
-
return runTool(
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
346
|
-
if (file)
|
|
347
|
-
if (path)
|
|
348
|
-
|
|
349
|
-
return runTool(
|
|
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
|
-
|
|
364
|
-
if (name)
|
|
365
|
-
if (path)
|
|
366
|
-
if (template)
|
|
367
|
-
if (content)
|
|
368
|
-
return runTool(
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
|
397
|
-
|
|
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
|
-
|
|
410
|
-
if (folder)
|
|
411
|
-
if (ext)
|
|
412
|
-
return runTool(
|
|
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
|
|
499
|
+
const running = await checkObsidianRunning();
|
|
468
500
|
if (!running) {
|
|
469
|
-
console.error(
|
|
470
|
-
|
|
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(
|
|
506
|
+
console.error("obsidian-mcp server running on stdio");
|
|
474
507
|
await server.connect(transport);
|
|
475
508
|
}
|
|
476
509
|
|