obsidian-mcp-server 3.8.0 → 3.9.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.
Files changed (57) hide show
  1. package/AGENTS.md +12 -10
  2. package/CLAUDE.md +12 -10
  3. package/README.md +12 -8
  4. package/changelog/3.9.x/3.9.0.md +43 -0
  5. package/changelog/template.md +10 -3
  6. package/dist/config/server-config.d.ts.map +1 -1
  7. package/dist/config/server-config.js +6 -4
  8. package/dist/config/server-config.js.map +1 -1
  9. package/dist/index.d.ts +3 -3
  10. package/dist/index.js +22 -15
  11. package/dist/index.js.map +1 -1
  12. package/dist/instructions.d.ts +4 -3
  13. package/dist/instructions.d.ts.map +1 -1
  14. package/dist/instructions.js +6 -9
  15. package/dist/instructions.js.map +1 -1
  16. package/dist/mcp-server/tools/definitions/_shared/suggest-paths.d.ts +7 -5
  17. package/dist/mcp-server/tools/definitions/_shared/suggest-paths.d.ts.map +1 -1
  18. package/dist/mcp-server/tools/definitions/_shared/suggest-paths.js +11 -8
  19. package/dist/mcp-server/tools/definitions/_shared/suggest-paths.js.map +1 -1
  20. package/dist/mcp-server/tools/definitions/index.d.ts +237 -217
  21. package/dist/mcp-server/tools/definitions/index.d.ts.map +1 -1
  22. package/dist/mcp-server/tools/definitions/index.js +6 -4
  23. package/dist/mcp-server/tools/definitions/index.js.map +1 -1
  24. package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.d.ts +6 -0
  25. package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.d.ts.map +1 -1
  26. package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.js +8 -1
  27. package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.js.map +1 -1
  28. package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.js +1 -1
  29. package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.js.map +1 -1
  30. package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.js +1 -1
  31. package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.js.map +1 -1
  32. package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.js +1 -1
  33. package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.js.map +1 -1
  34. package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.d.ts +6 -0
  35. package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.d.ts.map +1 -1
  36. package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.js +8 -1
  37. package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.js.map +1 -1
  38. package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.d.ts +143 -27
  39. package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.d.ts.map +1 -1
  40. package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.js +87 -79
  41. package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.js.map +1 -1
  42. package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.d.ts +6 -0
  43. package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.d.ts.map +1 -1
  44. package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.js +10 -3
  45. package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.js.map +1 -1
  46. package/dist/services/obsidian/obsidian-service.d.ts +53 -15
  47. package/dist/services/obsidian/obsidian-service.d.ts.map +1 -1
  48. package/dist/services/obsidian/obsidian-service.js +461 -155
  49. package/dist/services/obsidian/obsidian-service.js.map +1 -1
  50. package/dist/services/obsidian/section-extractor.d.ts +62 -6
  51. package/dist/services/obsidian/section-extractor.d.ts.map +1 -1
  52. package/dist/services/obsidian/section-extractor.js +303 -62
  53. package/dist/services/obsidian/section-extractor.js.map +1 -1
  54. package/manifest.json +3 -3
  55. package/package.json +8 -8
  56. package/server.json +7 -7
  57. package/skills/obsidian-guide/SKILL.md +61 -0
package/server.json CHANGED
@@ -6,14 +6,14 @@
6
6
  "url": "https://github.com/cyanheads/obsidian-mcp-server",
7
7
  "source": "github"
8
8
  },
9
- "version": "3.8.0",
9
+ "version": "3.9.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "registryBaseUrl": "https://registry.npmjs.org",
14
14
  "identifier": "obsidian-mcp-server",
15
15
  "runtimeHint": "npx",
16
- "version": "3.8.0",
16
+ "version": "3.9.0",
17
17
  "environmentVariables": [
18
18
  {
19
19
  "name": "OBSIDIAN_API_KEY",
@@ -37,7 +37,7 @@
37
37
  },
38
38
  {
39
39
  "name": "OBSIDIAN_REQUEST_TIMEOUT_MS",
40
- "description": "Per-request timeout in milliseconds.",
40
+ "description": "Milliseconds one request attempt (headers and body) may take before it fails with obsidian_timeout. GET, PUT, and DELETE retry a timed-out attempt; all the requests of one tool call share a 45 s budget, or this value when it is longer.",
41
41
  "format": "string",
42
42
  "isRequired": false,
43
43
  "default": "30000"
@@ -83,7 +83,7 @@
83
83
  },
84
84
  {
85
85
  "name": "OBSIDIAN_OMNISEARCH_URL",
86
- "description": "Override URL for the Omnisearch plugin HTTP server. Unset derives from OBSIDIAN_BASE_URL host with port 51361. Required only if the Omnisearch plugin is not on the default port.",
86
+ "description": "Override URL for the Omnisearch plugin HTTP server. Unset derives from OBSIDIAN_BASE_URL host with port 51361. Required only if the Omnisearch plugin is not on the default port. Under OBSIDIAN_READ_PATHS or OBSIDIAN_DENY_PATHS the URL is never called and the omnisearch search mode is withheld.",
87
87
  "format": "string",
88
88
  "isRequired": false
89
89
  },
@@ -104,7 +104,7 @@
104
104
  "registryBaseUrl": "https://registry.npmjs.org",
105
105
  "identifier": "obsidian-mcp-server",
106
106
  "runtimeHint": "npx",
107
- "version": "3.8.0",
107
+ "version": "3.9.0",
108
108
  "environmentVariables": [
109
109
  {
110
110
  "name": "MCP_TRANSPORT_TYPE",
@@ -134,7 +134,7 @@
134
134
  },
135
135
  {
136
136
  "name": "OBSIDIAN_REQUEST_TIMEOUT_MS",
137
- "description": "Per-request timeout in milliseconds.",
137
+ "description": "Milliseconds one request attempt (headers and body) may take before it fails with obsidian_timeout. GET, PUT, and DELETE retry a timed-out attempt; all the requests of one tool call share a 45 s budget, or this value when it is longer.",
138
138
  "format": "string",
139
139
  "isRequired": false,
140
140
  "default": "30000"
@@ -180,7 +180,7 @@
180
180
  },
181
181
  {
182
182
  "name": "OBSIDIAN_OMNISEARCH_URL",
183
- "description": "Override URL for the Omnisearch plugin HTTP server. Unset derives from OBSIDIAN_BASE_URL host with port 51361. Required only if the Omnisearch plugin is not on the default port.",
183
+ "description": "Override URL for the Omnisearch plugin HTTP server. Unset derives from OBSIDIAN_BASE_URL host with port 51361. Required only if the Omnisearch plugin is not on the default port. Under OBSIDIAN_READ_PATHS or OBSIDIAN_DENY_PATHS the URL is never called and the omnisearch search mode is withheld.",
184
184
  "format": "string",
185
185
  "isRequired": false
186
186
  },
@@ -0,0 +1,61 @@
1
+ ---
2
+ name: obsidian-guide
3
+ description: "Gates and multi-call workflows for the obsidian_* tools: pin active and periodic targets to a path, round-trip heading sections without editing the wrong one or dropping its sub-sections, page a search to the end before editing its hits, match tags exactly, and repeat writes after a timeout or a dropped connection. Use before editing several notes, restructuring headings, writing to a periodic note, retagging, moving a note, or retrying a failed obsidian_* write."
4
+ license: Apache-2.0
5
+ metadata:
6
+ author: cyanheads
7
+ version: "1.0"
8
+ ---
9
+
10
+ ## Scope
11
+
12
+ One Obsidian vault, reached through the Local REST API plugin while Obsidian runs. Each note tool reads or edits one note per call. No tool moves or renames a note, and none rewrites links in other notes. The multi-note jobs are the workflows below.
13
+
14
+ ## Identifiers
15
+
16
+ - **Note path**: `hits[].filename` from obsidian_search_notes, `entries[].path` from obsidian_list_notes, and the `path` every note tool returns. Writes and deletes match it exactly, letter case included.
17
+ - **Heading locator**: the full `Parent::Child` paths in obsidian_get_note's `document-map` `headings`, and the `sectionTarget` a section read or write returns.
18
+ - **Tag**: no leading `#`. A note's own `tags` (obsidian_get_note `full`, the JSONLogic `tags` field) hold its tags as written, letter case included. Parent tags show up only in tag counts.
19
+
20
+ ## Gates
21
+
22
+ 1. **Pin `active` and `periodic` targets after the first call.** Each call resolves them again. `active` is whichever file has focus in Obsidian at that moment, and a `periodic` target with no `date` is the current period. For the rest of the task, address the note by the `path` the first call returned.
23
+ 2. **A missing periodic note can't be created through a `periodic` target.** Every tool reads the target before acting, so a write to a period with no note also fails `periodic_not_found`. Ask the user to create it in Obsidian. Or, if they agree, write it by path: read an earlier period's note (pass `date`) to learn the folder and filename pattern. A note written this way holds only what you send, without the user's periodic-note template. On Local REST API 5.0.2 and later without the periodic-notes extension, every `periodic` target fails `periodic_unsupported`, whether or not the note exists; find the note with obsidian_search_notes or obsidian_list_notes and address it by path.
24
+ 3. **Write a heading back with the `sectionTarget` the read returned, never the bare name you sent.** A bare name resolves differently on each side. A read takes the first heading with that text in document order. A write takes the top-level heading with that text when there is one. Reading `Notes` and then writing `Notes` can touch two different sections.
25
+ 4. **A heading section is its whole subtree.** obsidian_get_note `format: "section"` returns the heading line, its body, and every sub-section under it. A replace rewrites all of that, so send the sub-sections back or target the child heading instead. An append lands below the last sub-section. A prepend lands right under the heading line. Round-trip a read through obsidian_write_note with `section`, which drops the leading heading line. obsidian_patch_note `replace` keeps a leading `#` heading line, and plugin v5.0 and later refuse that with `heading_outside_section`.
26
+ 5. **Page a search to the end before editing its hits.** Without OBSIDIAN_READ_PATHS or OBSIDIAN_DENY_PATHS, every page re-runs the search. The cursor is an offset into the new result, so edits that remove matches shift later pages and skip notes. Under a read scope, later pages come from the first page's hits for at most 5 minutes past the last page read. Collect every `filename` first, then edit.
27
+ 6. **A text hit doesn't promise a replace match.** Text mode needs each token somewhere in the filename or body, in any case. obsidian_replace_in_note needs the exact string, case-sensitive by default, and searches the body only by default. Read each note's `perReplacement[].count`: `0` is a success that wrote nothing. `useRegex` patterns run without the `m` flag, so `^` and `$` match the ends of the scoped text, not of a line. To anchor a line, use `(?<=^|\n)` and `(?=\n|$)`.
28
+ 7. **Tags match exactly.** `{"in": ["work", {"var": "tags"}]}` misses notes tagged only `Work`, or only `work/tasks`, which obsidian_list_tags still counts under `work`. To catch nested tags, use `{"some": [{"var": "tags"}, {"regexp": ["^work(/|$)", {"var": ""}]}]}`. obsidian_manage_tags `remove` is exact the same way, and its default `location: "frontmatter"` leaves inline `#tag`s in the body. Pass `location: "both"` and check the returned `tags`.
29
+ 8. **A JSONLogic hit is a note whose tree returns something truthy**, and that value is the hit's `result`. A note whose tree returns `false`, `null`, `0`, `""`, `[]`, or `{}` is left out. Return the field you need and one search reads it from every match: `{"if": [<test>, {"var": "tags"}, null]}` gives each match's tags. An empty value still drops the note.
30
+ 9. **After `obsidian_timeout` or `obsidian_unreachable` on a write, assume it may have landed.** One write call makes several requests under one shared budget (45 s at the default timeout): a read before the write, the write, and a read after. The error can arrive after the write applied, and the server never re-sends a whole-file append or a section write. Repeating is safe for obsidian_write_note, obsidian_patch_note `replace`, obsidian_manage_frontmatter, and obsidian_manage_tags. On a retry, `file_exists` (a create), `note_missing` (a delete), and `content_preexists` (a section append or prepend sent without `applyIfContentPreexists`) each mean the first attempt landed. Don't force a `content_preexists` through with `applyIfContentPreexists`. Read the note before repeating a whole-file obsidian_append_to_note, or an obsidian_replace_in_note whose `replace` still contains its `search`.
31
+ 10. **Confirm before deleting.** obsidian_delete_note deletes on the first call unless its description says it asks. Get the user's go-ahead on the exact path first.
32
+
33
+ ## Workflows
34
+
35
+ **Capture into a periodic note.** Call obsidian_append_to_note with the `periodic` target. Add `section` to land under one of the note's headings, and `createTargetIfMissing: true` if that heading may not exist yet. On `periodic_not_found` or `periodic_unsupported`, follow gate 2. Follow-up edits use the returned `path` (gate 1).
36
+
37
+ **Restructure headings.**
38
+
39
+ 1. Call obsidian_get_note `document-map` for every full heading path.
40
+ 2. Read each section you'll change by its full path, edit it, and write it back through obsidian_write_note `section` (gates 3–4).
41
+ 3. Rename or remove a heading line with obsidian_replace_in_note. No section write changes a heading line.
42
+ 4. To reorder or move whole sections, rewrite the note: obsidian_get_note `content`, edit, then obsidian_write_note `overwrite: true`. Compare `previousSizeInBytes` with `currentSizeInBytes`.
43
+ 5. A renamed heading breaks `[[Note#Old heading]]` links in other notes. Fix them as in *Move or rename a note*.
44
+
45
+ **Find, then edit across notes.**
46
+
47
+ 1. Search and page to the end (gate 5). To find notes by filename anywhere in the vault, test `path` with `regexp` in jsonlogic mode, e.g. `{"regexp": ["(^|/)Meeting[^/]*\\.md$", {"var": "path"}]}`. Don't walk obsidian_list_notes for this: its `nameRegex` never enters a folder whose own name fails the pattern.
48
+ 2. When `instructions` gives a narrower Writable scope than Readable, drop hits outside the Writable scope before writing.
49
+ 3. Edit each hit and read its counts (gate 6).
50
+
51
+ **Retag notes.**
52
+
53
+ 1. Find the notes with a tree that returns their tags (gates 7–8).
54
+ 2. For each note, call obsidian_manage_tags `add` with the new tag and `remove` with the old one, both with `location: "both"`.
55
+ 3. Confirm with obsidian_list_tags `nameRegex`. A parent tag's count includes its nested tags.
56
+
57
+ **Move or rename a note.** No tool does this, and nothing updates links that point at the old name.
58
+
59
+ 1. Read the note with obsidian_get_note `content` and write it with obsidian_write_note at the new path. That write fails with `file_exists` rather than overwrite a note already there.
60
+ 2. Find the notes that wikilink the old name with the backlink `regexp` in obsidian_search_notes' `logic` description. Rewrite each link with obsidian_replace_in_note (gate 6).
61
+ 3. Delete the old note once the user agrees (gate 10).