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 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.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",
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/run.test.js"
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" })`