mcp-obsidian-cli 1.0.1 → 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 +7 -2
- package/prompts/obsidian-bases.md +380 -0
- package/prompts/obsidian-canvas.md +312 -0
- package/prompts/obsidian-cli.md +115 -0
- package/prompts/obsidian-markdown.md +224 -0
- package/server.js +131 -135
- package/.claude/settings.local.json +0 -15
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,15 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-obsidian-cli",
|
|
3
|
-
"version": "1.0
|
|
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",
|
|
7
7
|
"bin": {
|
|
8
8
|
"mcp-obsidian-cli": "server.js"
|
|
9
9
|
},
|
|
10
|
+
"files": [
|
|
11
|
+
"server.js",
|
|
12
|
+
"lib/",
|
|
13
|
+
"prompts/"
|
|
14
|
+
],
|
|
10
15
|
"scripts": {
|
|
11
16
|
"start": "node server.js",
|
|
12
|
-
"test": "node --test test
|
|
17
|
+
"test": "node --test test/*.test.js"
|
|
13
18
|
},
|
|
14
19
|
"engines": {
|
|
15
20
|
"node": ">=18"
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
# Obsidian Bases Reference
|
|
2
|
+
|
|
3
|
+
Bases are database-like views of vault notes, defined as `.base` YAML files. They let you query, filter, and display notes as tables, card galleries, lists, or maps — without writing custom code.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
- **File format:** `.base` files containing valid YAML
|
|
8
|
+
- **How they work:** Obsidian reads all vault notes, applies your filters, computes any formulas, and renders results in the configured views
|
|
9
|
+
- **When to use:** Tracking tasks, reading lists, project dashboards, meeting logs, any situation where you want a live view across multiple notes
|
|
10
|
+
|
|
11
|
+
## YAML Schema
|
|
12
|
+
|
|
13
|
+
```yaml
|
|
14
|
+
# Global filters apply to ALL views
|
|
15
|
+
filters:
|
|
16
|
+
and: []
|
|
17
|
+
or: []
|
|
18
|
+
not: []
|
|
19
|
+
|
|
20
|
+
# Computed properties available across all views
|
|
21
|
+
formulas:
|
|
22
|
+
formula_name: 'expression'
|
|
23
|
+
|
|
24
|
+
# Display name overrides for properties
|
|
25
|
+
properties:
|
|
26
|
+
property_name:
|
|
27
|
+
displayName: "Display Name"
|
|
28
|
+
formula.formula_name:
|
|
29
|
+
displayName: "Formula Display Name"
|
|
30
|
+
file.ext:
|
|
31
|
+
displayName: "Extension"
|
|
32
|
+
|
|
33
|
+
# Custom summary formulas
|
|
34
|
+
summaries:
|
|
35
|
+
custom_name: 'values.mean().round(3)'
|
|
36
|
+
|
|
37
|
+
# One or more views
|
|
38
|
+
views:
|
|
39
|
+
- type: table | cards | list | map
|
|
40
|
+
name: "View Name"
|
|
41
|
+
limit: 10
|
|
42
|
+
groupBy:
|
|
43
|
+
property: property_name
|
|
44
|
+
direction: ASC | DESC
|
|
45
|
+
filters:
|
|
46
|
+
and: []
|
|
47
|
+
order:
|
|
48
|
+
- file.name
|
|
49
|
+
- property_name
|
|
50
|
+
- formula.formula_name
|
|
51
|
+
summaries:
|
|
52
|
+
property_name: Average
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Filters
|
|
56
|
+
|
|
57
|
+
Filters narrow which notes appear. Apply globally (to all views) or per-view.
|
|
58
|
+
|
|
59
|
+
### Filter Structure
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
# Single filter expression
|
|
63
|
+
filters: 'status == "done"'
|
|
64
|
+
|
|
65
|
+
# AND - all conditions must be true
|
|
66
|
+
filters:
|
|
67
|
+
and:
|
|
68
|
+
- 'status == "done"'
|
|
69
|
+
- 'priority > 3'
|
|
70
|
+
|
|
71
|
+
# OR - any condition can be true
|
|
72
|
+
filters:
|
|
73
|
+
or:
|
|
74
|
+
- 'file.hasTag("book")'
|
|
75
|
+
- 'file.hasTag("article")'
|
|
76
|
+
|
|
77
|
+
# NOT - exclude matching items
|
|
78
|
+
filters:
|
|
79
|
+
not:
|
|
80
|
+
- 'file.hasTag("archived")'
|
|
81
|
+
|
|
82
|
+
# Nested filters
|
|
83
|
+
filters:
|
|
84
|
+
or:
|
|
85
|
+
- file.hasTag("tag")
|
|
86
|
+
- and:
|
|
87
|
+
- file.hasTag("book")
|
|
88
|
+
- file.hasLink("Textbook")
|
|
89
|
+
- not:
|
|
90
|
+
- file.hasTag("book")
|
|
91
|
+
- file.inFolder("Required Reading")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Filter Operators
|
|
95
|
+
|
|
96
|
+
| Operator | Description |
|
|
97
|
+
|----------|-------------|
|
|
98
|
+
| `==` | equals |
|
|
99
|
+
| `!=` | not equal |
|
|
100
|
+
| `>` | greater than |
|
|
101
|
+
| `<` | less than |
|
|
102
|
+
| `>=` | greater than or equal |
|
|
103
|
+
| `<=` | less than or equal |
|
|
104
|
+
| `&&` | logical and |
|
|
105
|
+
| `\|\|` | logical or |
|
|
106
|
+
| `!` | logical not |
|
|
107
|
+
|
|
108
|
+
Useful filter functions: `file.hasTag("tag")`, `file.inFolder("path/")`, `file.hasLink("Note Name")`.
|
|
109
|
+
|
|
110
|
+
## Properties
|
|
111
|
+
|
|
112
|
+
### Three Property Types
|
|
113
|
+
|
|
114
|
+
1. **Note properties** — from note frontmatter: `status`, `priority`, `due`
|
|
115
|
+
2. **File properties** — computed file metadata: `file.name`, `file.path`, `file.mtime`, etc.
|
|
116
|
+
3. **Formula properties** — your computed values: `formula.days_until_due`
|
|
117
|
+
|
|
118
|
+
### File Properties Reference
|
|
119
|
+
|
|
120
|
+
| Property | Type | Description |
|
|
121
|
+
|----------|------|-------------|
|
|
122
|
+
| `file.name` | String | File name with extension |
|
|
123
|
+
| `file.basename` | String | File name without extension |
|
|
124
|
+
| `file.path` | String | Full path from vault root |
|
|
125
|
+
| `file.folder` | String | Parent folder path |
|
|
126
|
+
| `file.ext` | String | File extension |
|
|
127
|
+
| `file.size` | Number | File size in bytes |
|
|
128
|
+
| `file.ctime` | Date | Created time |
|
|
129
|
+
| `file.mtime` | Date | Modified time |
|
|
130
|
+
| `file.tags` | List | All tags in file |
|
|
131
|
+
| `file.links` | List | Internal links in file |
|
|
132
|
+
| `file.backlinks` | List | Files linking to this file |
|
|
133
|
+
|
|
134
|
+
## Formula Syntax
|
|
135
|
+
|
|
136
|
+
Formulas compute values from properties. Define them in the `formulas` section, reference them as `formula.name` in views.
|
|
137
|
+
|
|
138
|
+
```yaml
|
|
139
|
+
formulas:
|
|
140
|
+
# Simple arithmetic
|
|
141
|
+
total: "price * quantity"
|
|
142
|
+
|
|
143
|
+
# Conditional logic
|
|
144
|
+
status_icon: 'if(done, "Done", "Pending")'
|
|
145
|
+
|
|
146
|
+
# String formatting
|
|
147
|
+
formatted_price: 'if(price, price.toFixed(2) + " USD", "")'
|
|
148
|
+
|
|
149
|
+
# Date formatting
|
|
150
|
+
created_label: 'file.ctime.format("YYYY-MM-DD")'
|
|
151
|
+
|
|
152
|
+
# Days since creation
|
|
153
|
+
days_old: '(now() - file.ctime).days'
|
|
154
|
+
|
|
155
|
+
# Days until due date (with null guard)
|
|
156
|
+
days_until_due: 'if(due_date, (date(due_date) - today()).days, "")'
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Key Functions
|
|
160
|
+
|
|
161
|
+
| Function | Description |
|
|
162
|
+
|----------|-------------|
|
|
163
|
+
| `date(string)` | Parse string to date (`YYYY-MM-DD`) |
|
|
164
|
+
| `now()` | Current date and time |
|
|
165
|
+
| `today()` | Current date (time = 00:00:00) |
|
|
166
|
+
| `if(condition, trueVal, falseVal?)` | Conditional expression |
|
|
167
|
+
| `duration(string)` | Parse duration string |
|
|
168
|
+
| `file(path)` | Get file object |
|
|
169
|
+
| `link(path, display?)` | Create a link |
|
|
170
|
+
|
|
171
|
+
### Duration Type
|
|
172
|
+
|
|
173
|
+
Subtracting two dates returns a **Duration** — not a number. Access a numeric field before applying math:
|
|
174
|
+
|
|
175
|
+
```yaml
|
|
176
|
+
# CORRECT
|
|
177
|
+
"(now() - file.ctime).days" # Returns days as number
|
|
178
|
+
"(date(due) - today()).days.round(0)" # Rounded days
|
|
179
|
+
|
|
180
|
+
# WRONG — Duration doesn't support direct round()
|
|
181
|
+
# "(date(due) - today()).round(0)"
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Date Arithmetic
|
|
185
|
+
|
|
186
|
+
```yaml
|
|
187
|
+
"now() + \"1 day\"" # Tomorrow
|
|
188
|
+
"today() + \"7d\"" # A week from today
|
|
189
|
+
"now() - file.ctime" # Returns Duration type
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## View Types
|
|
193
|
+
|
|
194
|
+
### Table View
|
|
195
|
+
|
|
196
|
+
Displays notes as rows with sortable columns.
|
|
197
|
+
|
|
198
|
+
```yaml
|
|
199
|
+
views:
|
|
200
|
+
- type: table
|
|
201
|
+
name: "Task List"
|
|
202
|
+
order:
|
|
203
|
+
- file.name
|
|
204
|
+
- status
|
|
205
|
+
- due_date
|
|
206
|
+
summaries:
|
|
207
|
+
priority: Average
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Cards View
|
|
211
|
+
|
|
212
|
+
Displays notes as a visual card gallery.
|
|
213
|
+
|
|
214
|
+
```yaml
|
|
215
|
+
views:
|
|
216
|
+
- type: cards
|
|
217
|
+
name: "Gallery"
|
|
218
|
+
order:
|
|
219
|
+
- file.name
|
|
220
|
+
- cover_image
|
|
221
|
+
- description
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### List View
|
|
225
|
+
|
|
226
|
+
Displays a minimal list of notes.
|
|
227
|
+
|
|
228
|
+
```yaml
|
|
229
|
+
views:
|
|
230
|
+
- type: list
|
|
231
|
+
name: "Quick List"
|
|
232
|
+
order:
|
|
233
|
+
- file.name
|
|
234
|
+
- status
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Map View
|
|
238
|
+
|
|
239
|
+
Requires latitude/longitude properties and the Maps community plugin.
|
|
240
|
+
|
|
241
|
+
```yaml
|
|
242
|
+
views:
|
|
243
|
+
- type: map
|
|
244
|
+
name: "Locations"
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Summaries
|
|
248
|
+
|
|
249
|
+
Summarize numeric or date columns in table footers.
|
|
250
|
+
|
|
251
|
+
| Summary | Input | Description |
|
|
252
|
+
|---------|-------|-------------|
|
|
253
|
+
| `Average` | Number | Mean value |
|
|
254
|
+
| `Sum` | Number | Total |
|
|
255
|
+
| `Min` | Number | Smallest value |
|
|
256
|
+
| `Max` | Number | Largest value |
|
|
257
|
+
| `Range` | Number | Max minus Min |
|
|
258
|
+
| `Median` | Number | Median value |
|
|
259
|
+
| `Stddev` | Number | Standard deviation |
|
|
260
|
+
| `Earliest` | Date | Earliest date |
|
|
261
|
+
| `Latest` | Date | Latest date |
|
|
262
|
+
| `Checked` | Boolean | Count of true |
|
|
263
|
+
| `Unchecked` | Boolean | Count of false |
|
|
264
|
+
| `Empty` | Any | Count of empty |
|
|
265
|
+
| `Filled` | Any | Count of non-empty |
|
|
266
|
+
| `Unique` | Any | Count of distinct values |
|
|
267
|
+
|
|
268
|
+
## Complete Examples
|
|
269
|
+
|
|
270
|
+
### Task Tracker
|
|
271
|
+
|
|
272
|
+
```yaml
|
|
273
|
+
filters:
|
|
274
|
+
and:
|
|
275
|
+
- file.hasTag("task")
|
|
276
|
+
|
|
277
|
+
formulas:
|
|
278
|
+
days_until_due: 'if(due, (date(due) - today()).days, "")'
|
|
279
|
+
is_overdue: 'if(due, date(due) < today() && status != "done", false)'
|
|
280
|
+
priority_label: 'if(priority == 1, "High", if(priority == 2, "Medium", "Low"))'
|
|
281
|
+
|
|
282
|
+
properties:
|
|
283
|
+
formula.days_until_due:
|
|
284
|
+
displayName: "Days Until Due"
|
|
285
|
+
formula.priority_label:
|
|
286
|
+
displayName: Priority
|
|
287
|
+
|
|
288
|
+
views:
|
|
289
|
+
- type: table
|
|
290
|
+
name: "Active Tasks"
|
|
291
|
+
filters:
|
|
292
|
+
and:
|
|
293
|
+
- 'status != "done"'
|
|
294
|
+
order:
|
|
295
|
+
- file.name
|
|
296
|
+
- status
|
|
297
|
+
- formula.priority_label
|
|
298
|
+
- due
|
|
299
|
+
- formula.days_until_due
|
|
300
|
+
groupBy:
|
|
301
|
+
property: status
|
|
302
|
+
direction: ASC
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Reading List
|
|
306
|
+
|
|
307
|
+
```yaml
|
|
308
|
+
filters:
|
|
309
|
+
or:
|
|
310
|
+
- file.hasTag("book")
|
|
311
|
+
- file.hasTag("article")
|
|
312
|
+
|
|
313
|
+
formulas:
|
|
314
|
+
status_icon: 'if(status == "reading", "Reading", if(status == "done", "Done", "To Read"))'
|
|
315
|
+
year_read: 'if(finished_date, date(finished_date).year, "")'
|
|
316
|
+
|
|
317
|
+
views:
|
|
318
|
+
- type: cards
|
|
319
|
+
name: "Library"
|
|
320
|
+
order:
|
|
321
|
+
- cover
|
|
322
|
+
- file.name
|
|
323
|
+
- author
|
|
324
|
+
- formula.status_icon
|
|
325
|
+
filters:
|
|
326
|
+
not:
|
|
327
|
+
- 'status == "dropped"'
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Daily Notes Index
|
|
331
|
+
|
|
332
|
+
```yaml
|
|
333
|
+
filters:
|
|
334
|
+
and:
|
|
335
|
+
- file.inFolder("Daily Notes")
|
|
336
|
+
|
|
337
|
+
formulas:
|
|
338
|
+
word_estimate: '(file.size / 5).round(0)'
|
|
339
|
+
day_of_week: 'date(file.basename).format("dddd")'
|
|
340
|
+
|
|
341
|
+
properties:
|
|
342
|
+
formula.day_of_week:
|
|
343
|
+
displayName: "Day"
|
|
344
|
+
formula.word_estimate:
|
|
345
|
+
displayName: "~Words"
|
|
346
|
+
|
|
347
|
+
views:
|
|
348
|
+
- type: table
|
|
349
|
+
name: "Recent Notes"
|
|
350
|
+
limit: 30
|
|
351
|
+
order:
|
|
352
|
+
- file.name
|
|
353
|
+
- formula.day_of_week
|
|
354
|
+
- formula.word_estimate
|
|
355
|
+
- file.mtime
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## YAML Quoting Rules
|
|
359
|
+
|
|
360
|
+
- Use single quotes for formulas containing double quotes: `'if(done, "Yes", "No")'`
|
|
361
|
+
- Use double quotes for simple string values: `"My View Name"`
|
|
362
|
+
- Strings containing `:`, `{`, `}`, `[`, `]`, `#` must be quoted
|
|
363
|
+
|
|
364
|
+
## Embedding Bases in Notes
|
|
365
|
+
|
|
366
|
+
```markdown
|
|
367
|
+
![[MyBase.base]]
|
|
368
|
+
|
|
369
|
+
![[MyBase.base#View Name]]
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
## Using This Knowledge with MCP Tools
|
|
373
|
+
|
|
374
|
+
When working with Bases through this MCP server:
|
|
375
|
+
|
|
376
|
+
- Create a new base: `obsidian_create({ name: "Tasks", path: "bases/Tasks.base", content: "<yaml content>" })`
|
|
377
|
+
- Read an existing base definition: `obsidian_read({ path: "bases/Tasks.base" })`
|
|
378
|
+
- List all base files: `obsidian_files({ ext: "base" })`
|
|
379
|
+
- Set a property on a note that a base queries: `obsidian_property_set({ name: "status", value: "done", file: "My Task Note" })`
|
|
380
|
+
- Search for notes that match base criteria: `obsidian_search({ query: "tag:#task" })`
|