obsidian-mcp-server 3.5.5 → 3.6.1
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/AGENTS.md +23 -18
- package/CLAUDE.md +23 -18
- package/Dockerfile +90 -47
- package/README.md +121 -151
- package/changelog/3.6.x/3.6.0.md +31 -0
- package/changelog/3.6.x/3.6.1.md +29 -0
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/mcp-server/tools/definitions/_shared/schemas.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/_shared/schemas.js +1 -1
- package/dist/mcp-server/tools/definitions/_shared/schemas.js.map +1 -1
- package/dist/mcp-server/tools/definitions/_shared/suggest-paths.d.ts +2 -1
- package/dist/mcp-server/tools/definitions/_shared/suggest-paths.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/_shared/suggest-paths.js +2 -2
- package/dist/mcp-server/tools/definitions/_shared/suggest-paths.js.map +1 -1
- package/dist/mcp-server/tools/definitions/index.d.ts +36 -0
- package/dist/mcp-server/tools/definitions/index.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.d.ts +12 -0
- package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.js +15 -2
- package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.d.ts +2 -1
- package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.js +61 -15
- package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-get-note.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-get-note.tool.js +1 -2
- package/dist/mcp-server/tools/definitions/obsidian-get-note.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-list-commands.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-list-commands.tool.js +1 -2
- package/dist/mcp-server/tools/definitions/obsidian-list-commands.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-list-notes.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-list-notes.tool.js +1 -2
- package/dist/mcp-server/tools/definitions/obsidian-list-notes.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.js +1 -2
- package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-manage-frontmatter.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-manage-frontmatter.tool.js +1 -2
- package/dist/mcp-server/tools/definitions/obsidian-manage-frontmatter.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-manage-tags.tool.d.ts +3 -2
- package/dist/mcp-server/tools/definitions/obsidian-manage-tags.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-manage-tags.tool.js +6 -5
- package/dist/mcp-server/tools/definitions/obsidian-manage-tags.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.d.ts +15 -2
- package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.js +18 -4
- package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.js +2 -3
- package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.js +1 -4
- package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.d.ts +12 -0
- package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.js +23 -10
- package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.js.map +1 -1
- package/dist/services/obsidian/frontmatter-ops.d.ts +4 -3
- package/dist/services/obsidian/frontmatter-ops.d.ts.map +1 -1
- package/dist/services/obsidian/frontmatter-ops.js +379 -114
- package/dist/services/obsidian/frontmatter-ops.js.map +1 -1
- package/dist/services/obsidian/markdown-blocks.d.ts +39 -0
- package/dist/services/obsidian/markdown-blocks.d.ts.map +1 -0
- package/dist/services/obsidian/markdown-blocks.js +611 -0
- package/dist/services/obsidian/markdown-blocks.js.map +1 -0
- package/dist/services/obsidian/obsidian-service.d.ts +9 -7
- package/dist/services/obsidian/obsidian-service.d.ts.map +1 -1
- package/dist/services/obsidian/obsidian-service.js +321 -129
- package/dist/services/obsidian/obsidian-service.js.map +1 -1
- package/dist/services/obsidian/patch-instruction.d.ts +141 -0
- package/dist/services/obsidian/patch-instruction.d.ts.map +1 -0
- package/dist/services/obsidian/patch-instruction.js +217 -0
- package/dist/services/obsidian/patch-instruction.js.map +1 -0
- package/dist/services/obsidian/section-extractor.d.ts +97 -24
- package/dist/services/obsidian/section-extractor.d.ts.map +1 -1
- package/dist/services/obsidian/section-extractor.js +295 -58
- package/dist/services/obsidian/section-extractor.js.map +1 -1
- package/dist/services/obsidian/types.d.ts +12 -5
- package/dist/services/obsidian/types.d.ts.map +1 -1
- package/manifest.json +1 -1
- package/package.json +14 -10
- package/server.json +10 -24
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
<div align="center">
|
|
9
9
|
|
|
10
|
-
[](./CHANGELOG.md) [](./LICENSE) [](https://github.com/users/cyanheads/packages/container/package/obsidian-mcp-server) [](https://modelcontextprotocol.io/) [](https://www.npmjs.com/package/obsidian-mcp-server) [](https://www.typescriptlang.org/) [](https://bun.sh/)
|
|
11
11
|
|
|
12
12
|
</div>
|
|
13
13
|
|
|
@@ -23,204 +23,161 @@
|
|
|
23
23
|
|
|
24
24
|
## Overview
|
|
25
25
|
|
|
26
|
-
Read,
|
|
26
|
+
Obsidian vault notes over the Local REST API plugin. Read, search, and write notes, edit single headings, blocks, and frontmatter fields in place, and manage tags, with folder-scoped read/write permissions built in. Runs as a stdio process or a local Streamable HTTP server.
|
|
27
27
|
|
|
28
28
|
### Tools
|
|
29
29
|
|
|
30
30
|
| Tool | Description |
|
|
31
31
|
|:---|:---|
|
|
32
|
-
| `obsidian_get_note` | Read a note as raw content, full structured form
|
|
33
|
-
| `obsidian_list_notes` | List notes and
|
|
34
|
-
| `obsidian_list_tags` | List vault tags with usage counts,
|
|
35
|
-
| `
|
|
36
|
-
| `
|
|
37
|
-
| `
|
|
38
|
-
| `
|
|
39
|
-
| `
|
|
40
|
-
| `
|
|
41
|
-
| `
|
|
42
|
-
| `
|
|
43
|
-
| `
|
|
44
|
-
| `
|
|
45
|
-
| `obsidian_execute_command` |
|
|
32
|
+
| `obsidian_get_note` | Read a note as raw content, full structured form, document map, or a single section |
|
|
33
|
+
| `obsidian_list_notes` | List notes and folders under a vault path, recursively, with extension and name filters |
|
|
34
|
+
| `obsidian_list_tags` | List vault tags with usage counts, most-used first |
|
|
35
|
+
| `obsidian_search_notes` | Search by text, JSONLogic, or BM25-ranked Omnisearch when that plugin is reachable |
|
|
36
|
+
| `obsidian_write_note` | Create a note, replace one section, or overwrite a whole file with `overwrite: true` |
|
|
37
|
+
| `obsidian_append_to_note` | Append to a note (creating it if missing) or to one heading, block, or frontmatter field |
|
|
38
|
+
| `obsidian_patch_note` | Append, prepend, or replace against one heading, block reference, or frontmatter field |
|
|
39
|
+
| `obsidian_replace_in_note` | Literal or regex search-replace inside one note, body-only by default |
|
|
40
|
+
| `obsidian_manage_frontmatter` | Get, set, or delete one frontmatter key |
|
|
41
|
+
| `obsidian_manage_tags` | Add, remove, or list a note's tags in frontmatter, inline, or both |
|
|
42
|
+
| `obsidian_delete_note` | Permanently delete a note after the user confirms |
|
|
43
|
+
| `obsidian_open_in_ui` | Open a file in the Obsidian app, optionally in a new pane |
|
|
44
|
+
| `obsidian_list_commands` | List command-palette commands (opt-in via `OBSIDIAN_ENABLE_COMMANDS`) |
|
|
45
|
+
| `obsidian_execute_command` | Run a command-palette command by ID (opt-in via `OBSIDIAN_ENABLE_COMMANDS`) |
|
|
46
46
|
|
|
47
47
|
### Resources
|
|
48
48
|
|
|
49
49
|
| Resource | Description |
|
|
50
50
|
|:---|:---|
|
|
51
|
-
| `obsidian://vault/{+path}` | A note
|
|
52
|
-
| `obsidian://tags` |
|
|
53
|
-
| `obsidian://status` |
|
|
51
|
+
| `obsidian://vault/{+path}` | A note's content, frontmatter, tags, and file metadata |
|
|
52
|
+
| `obsidian://tags` | Every vault tag with its usage count, as an uncapped snapshot |
|
|
53
|
+
| `obsidian://status` | Plugin reachability, auth status, versions, and registered API extensions |
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
Note and tag data are also reachable through tools (`obsidian_get_note`, `obsidian_list_tags`); `obsidian://status` has no tool equivalent.
|
|
56
56
|
|
|
57
57
|
## Capability reference
|
|
58
58
|
|
|
59
59
|
### `obsidian_get_note` <sub>tool</sub>
|
|
60
60
|
|
|
61
|
-
- `format
|
|
62
|
-
-
|
|
63
|
-
- Heading sections use `Parent::Child` syntax, and every `#`-style heading path the document map lists reads back as itself (setext headings, underlined with `===` or `---`, are not recognized); a bare leaf name matching several headings, or a full path that repeats in the note, returns the first match and lists every colliding path in `candidates`
|
|
64
|
-
- Forgiving `path` resolution: a case-mismatched path retries against the canonical filename, an ambiguous case match fails with `Conflict`, and a `NotFound` carries `Did you mean: …?` suggestions when near-matches exist
|
|
65
|
-
- Typed errors include `note_missing`, `path_forbidden`, `no_active_file`, `periodic_unsupported` / `periodic_disabled`, and `path_traversal`
|
|
61
|
+
- `target` is a vault `path`, the `active` file, or a `periodic` note (`daily` through `yearly`, optional `date`); `format` is `content`, `full`, `document-map`, or `section`, and `full` takes `includeLinks: true` for vault-internal outgoing links
|
|
62
|
+
- `result.format` discriminates the payload; a `section` read that matches several headings returns the first and lists every full path in `candidates`
|
|
66
63
|
|
|
67
64
|
---
|
|
68
65
|
|
|
69
66
|
### `obsidian_list_notes` <sub>tool</sub>
|
|
70
67
|
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
- Hard cap of 1000 entries per call — `excluded.reason: "entry_cap"` signals a truncated walk; narrow `path` or the filters to see the rest
|
|
74
|
-
- Per-directory `truncated: true` marks entries cut off by the depth limit or by path policy
|
|
68
|
+
- Walks from `path` (default vault root) to `depth` 1–20 (default 2), filtered by `extension` and `nameRegex` (≤256 chars); a folder that fails `nameRegex` is not walked
|
|
69
|
+
- Returns `entries[]` (`file` / `directory`), `totals`, and `appliedFilters`; the walk stops at 1,000 entries with `excluded.reason: "entry_cap"`, and a folder the depth limit or path policy kept out carries `truncated: true`
|
|
75
70
|
|
|
76
71
|
---
|
|
77
72
|
|
|
78
73
|
### `obsidian_list_tags` <sub>tool</sub>
|
|
79
74
|
|
|
80
|
-
-
|
|
81
|
-
-
|
|
82
|
-
- Reports `truncated` / `shown` / `cap` when the limit withheld results
|
|
83
|
-
- Not narrowed by `OBSIDIAN_READ_PATHS` — tag names (never note contents) can surface from outside the read scope
|
|
84
|
-
|
|
85
|
-
---
|
|
86
|
-
|
|
87
|
-
### `obsidian_list_commands` <sub>tool</sub>
|
|
88
|
-
|
|
89
|
-
- Lists Obsidian command-palette IDs and display names; optional `nameRegex` filters on display name
|
|
90
|
-
- **Opt-in via `OBSIDIAN_ENABLE_COMMANDS=true`** — absent from `tools/list` when unset
|
|
91
|
-
- Discovery partner for `obsidian_execute_command`
|
|
75
|
+
- `nameRegex` (≤256 chars) and `minCount` narrow the set, then tags are ranked by count and capped at `limit` (default 200, max 10000); hierarchical parents count (`work/tasks` adds to `work`)
|
|
76
|
+
- When the cap withholds tags, the response carries `truncated`, `shown`, and `cap`
|
|
92
77
|
|
|
93
78
|
---
|
|
94
79
|
|
|
95
80
|
### `obsidian_search_notes` <sub>tool</sub>
|
|
96
81
|
|
|
97
|
-
- `mode: "text"
|
|
98
|
-
- `
|
|
99
|
-
-
|
|
100
|
-
- No dedicated backlinks tool — express "what links here" via `jsonlogic`: `{"regexp": ["\\[\\[Target Note(\\||#|\\]\\])", {"var": "content"}]}`
|
|
82
|
+
- `mode: "text"` requires every whitespace-split token of `query` as a case-insensitive substring of the filename or body (quotes are literal), shaped by `contextLength` (default 100), `pathPrefix`, and `maxMatchesPerHit` (default 10); `mode: "jsonlogic"` evaluates a `logic` tree over `path`, `content`, `frontmatter.<key>`, `tags`, and `stat`, with `glob` / `regexp` taking `[PATTERN, VALUE]`
|
|
83
|
+
- `result.mode` discriminates the payload; every mode reports `totalCount` and pages via `nextCursor`, and a text hit clipped to `maxMatchesPerHit` carries `truncated` and `totalMatches`
|
|
84
|
+
- `mode: "omnisearch"` (BM25 ranking, quoted phrases, `-exclusion`, `path:` / `ext:` filters) is offered only when the Omnisearch plugin answered at startup; its 50-hit upstream cap sets `truncated: true`
|
|
101
85
|
|
|
102
86
|
---
|
|
103
87
|
|
|
104
88
|
### `obsidian_write_note` <sub>tool</sub>
|
|
105
89
|
|
|
106
|
-
-
|
|
107
|
-
- With `section
|
|
108
|
-
- Output reports `created`, plus `previousSizeInBytes` / `currentSizeInBytes` on every call to spot an accidental clobber or a mistyped path
|
|
90
|
+
- `target` and `content`, with optional `section` and `contentType` (`markdown` / `json`); a whole-file write to an existing note fails with `file_exists` unless `overwrite: true`
|
|
91
|
+
- With `section`, replaces only that heading, block, or frontmatter field and keeps the heading line; output reports `created`, `sectionTargeted`, and the resolved `sectionTarget`
|
|
109
92
|
|
|
110
93
|
---
|
|
111
94
|
|
|
112
95
|
### `obsidian_append_to_note` <sub>tool</sub>
|
|
113
96
|
|
|
114
|
-
- Without `section
|
|
115
|
-
-
|
|
116
|
-
- Block-reference targets concatenate with no separator — include a leading newline in `content` for one
|
|
117
|
-
- `previousSizeInBytes` / `currentSizeInBytes` bracket every call for drift detection
|
|
97
|
+
- Without `section`, appends to the file or creates it (`created: true`); with `section`, appends to that heading, block, or frontmatter field of an existing note, and `createTargetIfMissing: true` creates the section
|
|
98
|
+
- A section append whose content is already at the target fails with `content_preexists`; block targets add no separator, so start `content` with a newline if you want one
|
|
118
99
|
|
|
119
100
|
---
|
|
120
101
|
|
|
121
102
|
### `obsidian_patch_note` <sub>tool</sub>
|
|
122
103
|
|
|
123
|
-
- `operation: "append" | "prepend" | "replace"` against one
|
|
124
|
-
-
|
|
125
|
-
- `patchOptions`: `createTargetIfMissing`, `applyIfContentPreexists` (idempotency guard — otherwise `content_preexists`), `trimTargetWhitespace`
|
|
104
|
+
- `operation: "append" | "prepend" | "replace"` against one `section` of an existing note; `patchOptions` takes `createTargetIfMissing`, `applyIfContentPreexists`, and `trimTargetWhitespace` (plugin v4.x only)
|
|
105
|
+
- Echoes the resolved `section` and `operation`; a repeat of content already at the target fails with `content_preexists` unless `applyIfContentPreexists: true`
|
|
126
106
|
|
|
127
107
|
---
|
|
128
108
|
|
|
129
109
|
### `obsidian_replace_in_note` <sub>tool</sub>
|
|
130
110
|
|
|
131
|
-
-
|
|
132
|
-
-
|
|
133
|
-
-
|
|
134
|
-
- `perReplacement[]` reports `bodyCount` / `frontmatterCount` per entry; `totalReplacements` sums them
|
|
111
|
+
- `replacements[]` run in order, each over the previous one's output; each takes `useRegex` (≤1024 chars), `caseSensitive` (default `true`), `wholeWord`, `flexibleWhitespace` (literal mode only), and `replaceAll` (default `true`)
|
|
112
|
+
- Returns `totalReplacements` and `perReplacement[]` with `bodyCount` / `frontmatterCount`
|
|
113
|
+
- `scope: "body"` (default) leaves frontmatter byte-identical; `"frontmatter"` and `"both"` re-parse the YAML afterward and write nothing if it breaks (`frontmatter_invalid`)
|
|
135
114
|
|
|
136
115
|
---
|
|
137
116
|
|
|
138
117
|
### `obsidian_manage_frontmatter` <sub>tool</sub>
|
|
139
118
|
|
|
140
|
-
- `operation: "get" | "set" | "delete"` on
|
|
141
|
-
- `get`
|
|
142
|
-
- `set` / `delete` return the full `frontmatter` after the change plus `previousSizeInBytes` / `currentSizeInBytes`
|
|
119
|
+
- `operation: "get" | "set" | "delete"` on one `key`; `set` requires a JSON-typed `value`
|
|
120
|
+
- `get` returns `exists` and `value` (`null` when absent); `set` and `delete` return the full `frontmatter` after the change, and a `delete` against unparseable YAML fails with `frontmatter_invalid` without writing
|
|
143
121
|
|
|
144
122
|
---
|
|
145
123
|
|
|
146
124
|
### `obsidian_manage_tags` <sub>tool</sub>
|
|
147
125
|
|
|
148
|
-
- `operation: "add" | "remove" | "list"`; `location: "frontmatter"` (default,
|
|
149
|
-
-
|
|
150
|
-
- Inline
|
|
151
|
-
- `add` / `remove` report `applied` vs. `skipped` tags plus the full `tags` set after the change; `list` ignores the input `tags` array
|
|
126
|
+
- `operation: "add" | "remove" | "list"` with `tags`; `location: "frontmatter"` (default, the `tags:` array), `"inline"` (body `#tag`; `add` appends at end of file), or `"both"`
|
|
127
|
+
- `add` / `remove` report `applied`, `skipped`, and the resulting `tags`; `list` returns `frontmatter`, `inline`, and `all`
|
|
128
|
+
- Inline detection follows Obsidian's tag grammar: code, wikilinks, images, link destinations, HTML, and math are skipped, and `%% … %%` comments are read
|
|
152
129
|
|
|
153
130
|
---
|
|
154
131
|
|
|
155
132
|
### `obsidian_delete_note` <sub>tool</sub>
|
|
156
133
|
|
|
157
|
-
-
|
|
158
|
-
-
|
|
159
|
-
-
|
|
134
|
+
- Takes a `target`; the first call answers with a confirmation request naming the path and byte size, and the note is deleted only after the user accepts
|
|
135
|
+
- Declining fails with `cancelled`, and a client without elicitation support cannot delete; there is no API-level undo, only Obsidian's local trash
|
|
136
|
+
- An answer counts only against the single-use consent record stored when the prompt was shown, bound to the caller, the path, and the note's content then. A pre-supplied or replayed answer, or one given after the note changed, gets a fresh prompt instead
|
|
137
|
+
- Consent records live in the server's storage provider (`STORAGE_PROVIDER_TYPE`, default `in-memory`, process-local). That works for stdio or a single HTTP instance; several instances behind one endpoint need a shared provider (`filesystem`, `supabase`, or `cloudflare-d1`, never `cloudflare-kv`)
|
|
160
138
|
|
|
161
139
|
---
|
|
162
140
|
|
|
163
141
|
### `obsidian_open_in_ui` <sub>tool</sub>
|
|
164
142
|
|
|
165
|
-
- `failIfMissing` (default `true`)
|
|
166
|
-
- `
|
|
167
|
-
|
|
168
|
-
|
|
143
|
+
- `path`, `failIfMissing` (default `true`), and `newLeaf` (open in a split pane); with `failIfMissing: false` a missing file is created, which needs write access
|
|
144
|
+
- `createdIfMissing` reports which branch ran
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
### `obsidian_list_commands` <sub>tool</sub>
|
|
149
|
+
|
|
150
|
+
- Optional `nameRegex` (≤256 chars) matched against each command's display name
|
|
151
|
+
- Returns `commands[]` of `id` and `name`, where `id` feeds `obsidian_execute_command`
|
|
152
|
+
- Listed only when `OBSIDIAN_ENABLE_COMMANDS=true` and `OBSIDIAN_READ_ONLY` is off
|
|
169
153
|
|
|
170
154
|
---
|
|
171
155
|
|
|
172
156
|
### `obsidian_execute_command` <sub>tool</sub>
|
|
173
157
|
|
|
174
|
-
-
|
|
175
|
-
-
|
|
176
|
-
- Behavior is command-dependent — some are destructive (delete file, close vault), some open UI
|
|
158
|
+
- `commandId` from `obsidian_list_commands`; returns `executed: true`, or fails with `command_unknown` for an unregistered ID
|
|
159
|
+
- Runs with the authority of a keyboard shortcut, so some commands are destructive (delete file, close vault); gated like `obsidian_list_commands`
|
|
177
160
|
|
|
178
161
|
---
|
|
179
162
|
|
|
180
163
|
### `obsidian://vault/{+path}` <sub>resource</sub>
|
|
181
164
|
|
|
182
|
-
-
|
|
183
|
-
-
|
|
184
|
-
- Returns the same shape as `obsidian_get_note` with `format: "full"` — content, frontmatter, tags, stat
|
|
185
|
-
- Gated by `OBSIDIAN_READ_PATHS` / `OBSIDIAN_WRITE_PATHS` like the tool equivalent
|
|
165
|
+
- `{+path}` captures everything after `/vault/`, slashes included; literal and percent-encoded paths resolve to the same note
|
|
166
|
+
- Returns `path`, `content`, `frontmatter`, `tags`, and `stat`, the same shape as `obsidian_get_note` with `format: "full"`; failures are `path_forbidden`, `note_missing`, or `path_is_directory`
|
|
186
167
|
|
|
187
168
|
---
|
|
188
169
|
|
|
189
170
|
### `obsidian://tags` <sub>resource</sub>
|
|
190
171
|
|
|
191
|
-
-
|
|
192
|
-
-
|
|
172
|
+
- Every tag with its `count`, uncapped and in upstream order, hierarchical parents included
|
|
173
|
+
- No ranking or filters; `obsidian_list_tags` gives the count-ranked, capped view
|
|
193
174
|
|
|
194
175
|
---
|
|
195
176
|
|
|
196
177
|
### `obsidian://status` <sub>resource</sub>
|
|
197
178
|
|
|
198
|
-
-
|
|
199
|
-
-
|
|
200
|
-
- Still reports reachability when the API key is misconfigured; only `authenticated` reflects the key's validity
|
|
201
|
-
|
|
202
|
-
## Path policy (folder-scoped permissions)
|
|
203
|
-
|
|
204
|
-
Three optional env vars gate which vault paths each tool can target. **Default unset = full vault** for both reads and writes — backwards compatible.
|
|
205
|
-
|
|
206
|
-
| Goal | Config |
|
|
207
|
-
|:---|:---|
|
|
208
|
-
| Default (current behavior) | all unset |
|
|
209
|
-
| Read everywhere, write only in `projects/` and `scratch/` | `OBSIDIAN_WRITE_PATHS=projects/,scratch/` |
|
|
210
|
-
| Read only `public/`, write only `public/inbox/` | `OBSIDIAN_READ_PATHS=public/`, `OBSIDIAN_WRITE_PATHS=public/inbox/` |
|
|
211
|
-
| Read-only deployment — no writes anywhere | `OBSIDIAN_READ_ONLY=true` |
|
|
212
|
-
|
|
213
|
-
**Matching is prefix-based with implicit recursion**, case-insensitive, with trailing slashes normalized. `projects/` matches `projects/a.md`, `projects/sub/b.md`, etc.
|
|
214
|
-
|
|
215
|
-
**Write paths are implicitly readable** — you can't sanely edit what you can't see. So a read passes when the target matches `READ_PATHS` *or* `WRITE_PATHS`.
|
|
216
|
-
|
|
217
|
-
**`OBSIDIAN_READ_ONLY=true` short-circuits before the path checks** — every write tool and the command-palette pair are wrapped with `disabledTool()` at startup (absent from `tools/list`), and any write that still reaches the service is denied at runtime regardless of `WRITE_PATHS`.
|
|
218
|
-
|
|
219
|
-
Denies are typed `path_forbidden` (JSON-RPC code `Forbidden`) with the active scope echoed back in `data.recovery.hint` and `data.activeScope`, so the LLM can self-correct without inspecting server logs. Search results from `obsidian_search_notes` are filtered against `READ_PATHS` silently — surfacing a "we hid N hits" indicator would defeat the gate.
|
|
220
|
-
|
|
221
|
-
**Tag listing is vault-wide.** `obsidian_list_tags` and the `obsidian://tags` resource aggregate tag names across the whole vault and are *not* narrowed by `OBSIDIAN_READ_PATHS` — they take no path to gate, so tag names (never note contents) from outside the read scope can surface.
|
|
222
|
-
|
|
223
|
-
The startup banner logs the active scope so operators can verify their config at boot.
|
|
179
|
+
- `status`, `service`, `authenticated`, `versions`, `manifest`, and `apiExtensions[]`; still answers with a wrong API key, reporting `authenticated: false`
|
|
180
|
+
- On plugin v5.0.2 and later, check `apiExtensions` for `local-rest-api-periodic-notes` before using `periodic` targets
|
|
224
181
|
|
|
225
182
|
## Features
|
|
226
183
|
|
|
@@ -228,22 +185,23 @@ Built on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core): s
|
|
|
228
185
|
|
|
229
186
|
Obsidian-specific:
|
|
230
187
|
|
|
231
|
-
-
|
|
232
|
-
-
|
|
233
|
-
-
|
|
234
|
-
-
|
|
235
|
-
-
|
|
188
|
+
- Typed client for the [Obsidian Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api) plugin; section writes speak markdown-patch 2.0 to plugin v5.0 and later and 1.x to v4.x, chosen from the reported plugin version
|
|
189
|
+
- Heading targets take a full `Parent::Child` path or a bare leaf name; writes reject an ambiguous one with `ambiguous_section` and its `candidates`, unless exactly one match is a top-level heading. On plugin v5.0 and later, content added to a heading is set off by a blank line (a list item continues an adjacent list), and a heading inside it must sit below the section's level (`heading_outside_section`)
|
|
190
|
+
- Folder-scoped read/write permissions, a read-only switch, and an opt-in command-palette pair (see [Path policy](#path-policy)); server-level `instructions` on `initialize` report the active policy
|
|
191
|
+
- `obsidian_get_note` and `obsidian_open_in_ui` retry a case-mismatched path against the real filename and add `Did you mean` suggestions to a miss; writes and deletes match the exact path
|
|
192
|
+
- Regex inputs are capped (`nameRegex` at 256 chars, `useRegex` at 1024) and rejected with `regex_unsafe` when they nest quantifiers
|
|
193
|
+
- Backlinks have no dedicated tool; `obsidian_search_notes` in `jsonlogic` mode finds them with `{"regexp": ["\\[\\[Target Note(\\||#|\\]\\])", {"var": "content"}]}`
|
|
236
194
|
|
|
237
195
|
Agent-friendly output:
|
|
238
196
|
|
|
239
|
-
- Recovery-guided errors
|
|
240
|
-
- Size
|
|
241
|
-
- Ambiguity surfaced
|
|
242
|
-
- Discriminated output contracts
|
|
197
|
+
- Recovery-guided errors: every declared failure carries a `reason`, a JSON-RPC code, and a `recovery.hint` written for that case
|
|
198
|
+
- Size deltas: every mutating tool returns `previousSizeInBytes` / `currentSizeInBytes`, so a caller can spot an accidental clobber without a follow-up read
|
|
199
|
+
- Ambiguity surfaced as data: shared heading names return `candidates`, and tag operations report `applied` vs. `skipped`
|
|
200
|
+
- Discriminated output contracts: `format` on `obsidian_get_note`, `mode` on `obsidian_search_notes`, `operation` on `obsidian_manage_frontmatter` and `obsidian_manage_tags`
|
|
243
201
|
|
|
244
202
|
## Getting started
|
|
245
203
|
|
|
246
|
-
Add the following to your MCP client configuration file. The Obsidian Local REST API plugin must be installed and enabled in your vault
|
|
204
|
+
Add the following to your MCP client configuration file. The Obsidian Local REST API plugin must be installed and enabled in your vault; see [Prerequisites](#prerequisites).
|
|
247
205
|
|
|
248
206
|
```json
|
|
249
207
|
{
|
|
@@ -301,9 +259,9 @@ Or with Docker:
|
|
|
301
259
|
}
|
|
302
260
|
```
|
|
303
261
|
|
|
304
|
-
|
|
262
|
+
Inside a container, the default `OBSIDIAN_BASE_URL` (`http://127.0.0.1:27123`) is the container's own loopback. Add `-e OBSIDIAN_BASE_URL=http://host.docker.internal:27123` (Docker Desktop) or run with `--network host` (Linux) to reach the plugin on your host.
|
|
305
263
|
|
|
306
|
-
For Streamable HTTP, set the transport and start the server
|
|
264
|
+
For Streamable HTTP, set the transport and start the server:
|
|
307
265
|
|
|
308
266
|
```sh
|
|
309
267
|
MCP_TRANSPORT_TYPE=http OBSIDIAN_API_KEY=... bun run start:http
|
|
@@ -313,10 +271,11 @@ MCP_TRANSPORT_TYPE=http OBSIDIAN_API_KEY=... bun run start:http
|
|
|
313
271
|
### Prerequisites
|
|
314
272
|
|
|
315
273
|
- [Bun v1.4.0](https://bun.sh/) or higher (or Node.js v24+).
|
|
316
|
-
- The [Obsidian Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api) plugin,
|
|
317
|
-
-
|
|
318
|
-
-
|
|
319
|
-
-
|
|
274
|
+
- The [Obsidian Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api) plugin, v4.0.0 or later, enabled in your vault. Generate an API key under **Settings → Community Plugins → Local REST API** and set it as `OBSIDIAN_API_KEY`.
|
|
275
|
+
- The server defaults to `http://127.0.0.1:27123`, so enable **"Non-encrypted (HTTP) Server"** in the plugin settings, or set `OBSIDIAN_BASE_URL=https://127.0.0.1:27124` for the always-on HTTPS port (its self-signed cert is accepted while `OBSIDIAN_VERIFY_SSL=false`, the default).
|
|
276
|
+
- Periodic-note targets work natively on plugin v5.0.1 and earlier. From v5.0.2 they need the [periodic-notes API extension](https://github.com/coddingtonbear/obsidian-local-rest-api-periodic-notes); without it they fail with `periodic_unsupported`.
|
|
277
|
+
- Plugin v6.0 drops markdown-patch 1.x, which two table-row writes (`contentType: "json"`) still use: rows under a heading, and rows through a block ID on its own line below the table. On v6.0, target the table by an ID on its last row.
|
|
278
|
+
- An MCP client that supports elicitation, to use `obsidian_delete_note`. Every other tool works without it.
|
|
320
279
|
|
|
321
280
|
### Installation
|
|
322
281
|
|
|
@@ -349,30 +308,41 @@ MCP_TRANSPORT_TYPE=http OBSIDIAN_API_KEY=... bun run start:http
|
|
|
349
308
|
|
|
350
309
|
| Variable | Description | Default |
|
|
351
310
|
|:---------|:------------|:--------|
|
|
352
|
-
| `OBSIDIAN_API_KEY` | **Required.** Bearer token for the
|
|
353
|
-
| `OBSIDIAN_BASE_URL` |
|
|
354
|
-
| `OBSIDIAN_VERIFY_SSL` | Verify the TLS certificate.
|
|
311
|
+
| `OBSIDIAN_API_KEY` | **Required.** Bearer token for the Local REST API plugin. | — |
|
|
312
|
+
| `OBSIDIAN_BASE_URL` | Local REST API base URL; `https://127.0.0.1:27124` is the always-on HTTPS port. A trailing slash is stripped. When nothing answers, calls fail with `obsidian_unreachable`. | `http://127.0.0.1:27123` |
|
|
313
|
+
| `OBSIDIAN_VERIFY_SSL` | Verify the plugin's TLS certificate. Off by default for its self-signed cert; the relaxation applies only to an `https:` `OBSIDIAN_BASE_URL`. With `true`, an untrusted cert fails calls with `certificate_rejected`. | `false` |
|
|
355
314
|
| `OBSIDIAN_REQUEST_TIMEOUT_MS` | Per-request timeout in milliseconds. | `30000` |
|
|
356
|
-
| `OBSIDIAN_ENABLE_COMMANDS` |
|
|
357
|
-
| `OBSIDIAN_READ_PATHS` | Comma-separated
|
|
358
|
-
| `OBSIDIAN_WRITE_PATHS` | Comma-separated
|
|
359
|
-
| `OBSIDIAN_READ_ONLY` |
|
|
360
|
-
| `OBSIDIAN_OMNISEARCH_URL` |
|
|
315
|
+
| `OBSIDIAN_ENABLE_COMMANDS` | Enable `obsidian_list_commands` and `obsidian_execute_command`. Commands are opaque and can be destructive. | `false` |
|
|
316
|
+
| `OBSIDIAN_READ_PATHS` | Comma-separated folder allowlist for reads. See [Path policy](#path-policy). | unset (full vault) |
|
|
317
|
+
| `OBSIDIAN_WRITE_PATHS` | Comma-separated folder allowlist for writes. See [Path policy](#path-policy). | unset (full vault) |
|
|
318
|
+
| `OBSIDIAN_READ_ONLY` | Deny every write and disable the command-palette pair. | `false` |
|
|
319
|
+
| `OBSIDIAN_OMNISEARCH_URL` | [Omnisearch](https://github.com/scambier/obsidian-omnisearch) HTTP server URL. Unset derives from the `OBSIDIAN_BASE_URL` host on port `51361`. Probed once at startup; the `omnisearch` search mode appears only if it answers. | derived |
|
|
361
320
|
| `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http`. | `stdio` |
|
|
362
|
-
| `
|
|
363
|
-
| `
|
|
364
|
-
| `
|
|
365
|
-
| `MCP_SESSION_MODE` | Session handling for the HTTP transport: `stateless`, `stateful`, or `auto`. Defaults to `stateful` here — `obsidian_delete_note` confirms via an elicitation round, and under `stateless` a 2025-era client's round is refused (`client_capability_missing`). | `stateful` |
|
|
366
|
-
| `MCP_PUBLIC_URL` | Public origin override for TLS-terminating reverse-proxy deployments (landing page, Server Card, RFC 9728 metadata). | unset |
|
|
367
|
-
| `MCP_AUTH_MODE` | Auth mode: `none`, `jwt`, or `oauth`. | `none` |
|
|
368
|
-
| `MCP_AUTH_SECRET_KEY` | **Required when `MCP_AUTH_MODE=jwt`.** ≥32-char shared secret used to verify incoming JWTs. | — |
|
|
369
|
-
| `MCP_AUTH_DISABLE_SCOPE_CHECKS` | When `true`, bypasses per-tool scope enforcement after the auth-context presence check. Token signature, audience, issuer, and expiry validation remain intact. Use only when a custom claim can't be injected and combine with `OBSIDIAN_READ_PATHS` / `OBSIDIAN_WRITE_PATHS` / `OBSIDIAN_READ_ONLY` for access control. A `WARNING` is logged at startup whenever the bypass is active. | `false` |
|
|
321
|
+
| `MCP_HTTP_PORT` | HTTP server port. | `3010` |
|
|
322
|
+
| `MCP_SESSION_MODE` | HTTP session mode: `stateful` or `auto`. The server requires a stateful session because the `obsidian_delete_note` confirmation needs one on 2025-era clients, so `stateless` fails startup over HTTP. No effect on stdio. | `stateful` |
|
|
323
|
+
| `MCP_AUTH_MODE` | Authentication: `none`, `jwt`, or `oauth`. | `none` |
|
|
370
324
|
| `MCP_LOG_LEVEL` | Log level (RFC 5424). | `info` |
|
|
371
325
|
| `LOGS_DIR` | Directory for log files (Node.js only). | `<project-root>/logs` |
|
|
372
|
-
| `OTEL_ENABLED` | Enable [OpenTelemetry
|
|
326
|
+
| `OTEL_ENABLED` | Enable [OpenTelemetry](https://github.com/cyanheads/mcp-ts-core/tree/main/docs/telemetry). | `false` |
|
|
373
327
|
|
|
374
328
|
See [`.env.example`](./.env.example) for the full list of optional overrides.
|
|
375
329
|
|
|
330
|
+
### Path policy
|
|
331
|
+
|
|
332
|
+
Three optional env vars limit which vault paths the tools can touch. Unset, reads and writes cover the full vault.
|
|
333
|
+
|
|
334
|
+
| Goal | Config |
|
|
335
|
+
|:---|:---|
|
|
336
|
+
| Read everywhere, write only in `projects/` and `scratch/` | `OBSIDIAN_WRITE_PATHS=projects/,scratch/` |
|
|
337
|
+
| Read only `public/`, write only `public/inbox/` | `OBSIDIAN_READ_PATHS=public/`, `OBSIDIAN_WRITE_PATHS=public/inbox/` |
|
|
338
|
+
| No writes anywhere | `OBSIDIAN_READ_ONLY=true` |
|
|
339
|
+
|
|
340
|
+
- Matching is by prefix, recursive, and case-insensitive; trailing slashes are normalized. Write paths are also readable.
|
|
341
|
+
- `OBSIDIAN_READ_ONLY=true` removes every write tool and the command-palette pair from `tools/list`, including `obsidian_manage_frontmatter` and `obsidian_manage_tags` (so their `get` / `list` go too). `obsidian_open_in_ui` still opens existing files but won't create one.
|
|
342
|
+
- A denial fails with `path_forbidden`, echoing the active scope in `data.activeScope` and the recovery hint. Search hits outside the read scope are dropped silently, and `obsidian_list_notes` shows an out-of-scope folder without walking it.
|
|
343
|
+
- Tag listings (`obsidian_list_tags`, `obsidian://tags`) are vault-wide, so tag names (never note contents) from outside the read scope can appear.
|
|
344
|
+
- The startup log prints the active scope.
|
|
345
|
+
|
|
376
346
|
## Running the server
|
|
377
347
|
|
|
378
348
|
### Local development
|
|
@@ -404,22 +374,22 @@ docker build -t obsidian-mcp-server .
|
|
|
404
374
|
docker run --rm -e OBSIDIAN_API_KEY=your-key -p 3010:3010 obsidian-mcp-server
|
|
405
375
|
```
|
|
406
376
|
|
|
407
|
-
The
|
|
377
|
+
The image defaults to HTTP transport on `0.0.0.0`, stateful sessions, and logs in `/var/log/obsidian-mcp-server`. Point `OBSIDIAN_BASE_URL` at `http://host.docker.internal:27123` (Docker Desktop) or use `--network host` (Linux) to reach the plugin. OpenTelemetry peer dependencies are installed by default; build with `--build-arg OTEL_ENABLED=false` to omit them.
|
|
408
378
|
|
|
409
|
-
|
|
379
|
+
If the port is reachable from other machines, set `MCP_AUTH_MODE=jwt` (with `MCP_AUTH_SECRET_KEY`) or `oauth`. With the default `none`, every caller acts on your vault with your `OBSIDIAN_API_KEY`.
|
|
410
380
|
|
|
411
381
|
## Project structure
|
|
412
382
|
|
|
413
383
|
| Directory | Purpose |
|
|
414
384
|
|:----------|:--------|
|
|
415
|
-
| `src/index.ts` | `createApp()` entry point
|
|
385
|
+
| `src/index.ts` | `createApp()` entry point: registers tools and resources, probes Omnisearch, applies the read-only and command gates. |
|
|
416
386
|
| `src/config` | Server-specific environment variable parsing (`OBSIDIAN_*`) with Zod. |
|
|
417
|
-
| `src/services/obsidian` | Local REST API client,
|
|
418
|
-
| `src/mcp-server/tools` | Tool definitions (`*.tool.ts`) and shared
|
|
387
|
+
| `src/services/obsidian` | Local REST API client, path policy, markdown-patch format handling, frontmatter/section/tag parsing, domain types. |
|
|
388
|
+
| `src/mcp-server/tools` | Tool definitions (`*.tool.ts`) and shared schemas and helpers. |
|
|
419
389
|
| `src/mcp-server/resources` | Resource definitions (`*.resource.ts`). |
|
|
420
|
-
| `src/mcp-server/prompts` | Prompt definitions (
|
|
421
|
-
| `tests/` | Vitest tests
|
|
422
|
-
| `docs/` |
|
|
390
|
+
| `src/mcp-server/prompts` | Prompt definitions (none registered). |
|
|
391
|
+
| `tests/` | Vitest tests for tools, resources, services, and config. |
|
|
392
|
+
| `docs/` | Local REST API OpenAPI spec and the generated `tree.md`. |
|
|
423
393
|
| `changelog/` | Per-version release notes; `CHANGELOG.md` is the regenerated rollup. |
|
|
424
394
|
|
|
425
395
|
## Development guide
|
|
@@ -427,13 +397,13 @@ The image binds to `0.0.0.0` inside the container (required for Docker port mapp
|
|
|
427
397
|
See [`CLAUDE.md`](./CLAUDE.md) for development guidelines and architectural rules. The short version:
|
|
428
398
|
|
|
429
399
|
- Handlers throw, framework catches — no `try/catch` in tool logic
|
|
430
|
-
- Use `ctx.log` for request-scoped logging
|
|
400
|
+
- Use `ctx.log` for request-scoped logging; route every Local REST API call through `getObsidianService()`
|
|
431
401
|
- Register new tools and resources via the barrels in `src/mcp-server/*/definitions/index.ts`
|
|
432
402
|
- Wrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields
|
|
433
403
|
|
|
434
404
|
## Contributing
|
|
435
405
|
|
|
436
|
-
Bugs, feature requests, and documentation gaps belong in an issue
|
|
406
|
+
Bugs, feature requests, and documentation gaps belong in an issue; see [CONTRIBUTING.md](.github/CONTRIBUTING.md) for what makes one actionable and [CODE_OF_CONDUCT.md](.github/CODE_OF_CONDUCT.md) for how we work together. Security reports go through [SECURITY.md](.github/SECURITY.md), never a public issue.
|
|
437
407
|
|
|
438
408
|
Run checks and tests before submitting:
|
|
439
409
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "obsidian_patch_note, obsidian_append_to_note, obsidian_write_note, and the document map now speak markdown-patch 2.0 on Local REST API v5.x, required before plugin v6.0 removes the 1.x format they used to pin."
|
|
3
|
+
breaking: false
|
|
4
|
+
security: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 3.6.0 — 2026-09-23
|
|
8
|
+
|
|
9
|
+
## Added
|
|
10
|
+
|
|
11
|
+
- **Section-targeted writes and the document map now speak markdown-patch 2.0**, negotiated once per service from the plugin's `GET /` capability report: a JSON instruction body on Local REST API v5.0 and later, 1.x request headers on v4.x. The supported plugin range widens from v4.0–v5.x to v4.0 and later ([#102](https://github.com/cyanheads/obsidian-mcp-server/issues/102)).
|
|
12
|
+
|
|
13
|
+
## Changed
|
|
14
|
+
|
|
15
|
+
- **On plugin v5.x, content appended or prepended to a heading is separated from the section's existing content by a blank line**, except a list item continuing a list at the section's own edge, which stays tight instead of gaining paragraph spacing on every append ([#145](https://github.com/cyanheads/obsidian-mcp-server/issues/145)).
|
|
16
|
+
- **On plugin v5.x, a heading at or above the target section's own level in written content is rejected** with the new `heading_outside_section` reason instead of silently closing the section early.
|
|
17
|
+
- **A patch refusal that isn't a missing target now reports `patch_rejected`** instead of the misleading `section_target_missing`.
|
|
18
|
+
- **Two table-row writes still go out as markdown-patch 1.x on v5.x** — rows written under a heading, and rows addressed through a standalone `^id` line — since 2.0 cannot express either shape.
|
|
19
|
+
- **A failing `GET /` now fails the section write or document-map read with its classified error** instead of guessing a markdown-patch format.
|
|
20
|
+
- **`patchOptions.trimTargetWhitespace` is honored on plugin v4.x only**; v5.0 and later place the blank lines around inserted content themselves and ignore it.
|
|
21
|
+
|
|
22
|
+
## Fixed
|
|
23
|
+
|
|
24
|
+
- **`obsidian_manage_tags` reads inline tags against a block-structure scan** instead of a flat regex split, so indented code, glued tags (`#a#b`), underscore boundaries (`_#t_`), table cells, narrowed link/fence/HTML-tag grammars, and HTML blocks beyond `<div>`/`<p>` all agree with Obsidian's own metadata cache ([#139](https://github.com/cyanheads/obsidian-mcp-server/issues/139), [#140](https://github.com/cyanheads/obsidian-mcp-server/issues/140), [#144](https://github.com/cyanheads/obsidian-mcp-server/issues/144)).
|
|
25
|
+
- **`obsidian_get_note` `format: "section"` finds headings the way the document map does** — lexed with `marked`, so setext headings count and a `#` line inside a list item or HTML block does not — so a section read now covers exactly the span a write edits. The plugin v4.x repeated-heading check reads the same scan, and `obsidian_write_note` strips a leading setext heading that names the section as it does an ATX one ([#141](https://github.com/cyanheads/obsidian-mcp-server/issues/141)).
|
|
26
|
+
- **On plugin v5.x, a heading write to a CRLF note lands at the section's edge**; the 1.x format placed it mid-line, offset by the note's line endings ([#102](https://github.com/cyanheads/obsidian-mcp-server/issues/102)).
|
|
27
|
+
- **`obsidian_get_note` `format: "document-map"` lists headings in note order when a heading name is integer-like** (`## 2025` above `## 2024`), where the map listed those names first; the `candidates` of an `ambiguous_section` error follow the same order ([#102](https://github.com/cyanheads/obsidian-mcp-server/issues/102)).
|
|
28
|
+
|
|
29
|
+
## Dependencies
|
|
30
|
+
|
|
31
|
+
- `marked` added at `17.0.6` (pinned exact — the heading scan's tokenizer overrides read `marked`'s rule internals, and the pin matches the major the plugin's own markdown-patch lexes with)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "obsidian_delete_note now deletes only on a round that redeems a server-stored consent record, HTTP startup requires a stateful session, and the server moves to mcp-ts-core 0.13.12, whose tool errors carry a request ID and whose argument hints lead with the field path."
|
|
3
|
+
breaking: true
|
|
4
|
+
security: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 3.6.1 — 2026-10-05
|
|
8
|
+
|
|
9
|
+
## Changed
|
|
10
|
+
|
|
11
|
+
- **HTTP startup requires a stateful session.** `createApp` declares `sessionMode: { default: 'stateful', require: 'stateful' }`: over HTTP, a resolved `stateless` mode (`MCP_SESSION_MODE=stateless`) now fails startup with a `ConfigurationError` instead of serving a delete confirmation no 2025-era client can answer. stdio starts regardless of the variable. Before, stateful was only the default and an explicit `stateless` still started.
|
|
12
|
+
- **`obsidian_delete_note` reads the note once (GET) to build its confirmation prompt**, replacing the size probe (HEAD).
|
|
13
|
+
- **`obsidian_append_to_note` description** now says a line break is inserted before content appended to a file that does not end in one.
|
|
14
|
+
- **A table-patch rejection now reads "the target is not a table"** instead of "the target block is not a table".
|
|
15
|
+
- **Docker:** dependencies install in a `deps` stage pinned to `$BUILDPLATFORM` (OTel peers via `scripts/install-otel.ts`, musl bindings pruned by `scripts/prune-musl-packages.ts`), base images move to Bun 1.4.2, and the image no longer sets `MCP_SESSION_MODE` (the session mode comes from `src/`) or `MCP_FORCE_CONSOLE_LOGGING` (nothing read it).
|
|
16
|
+
- **`server.json`:** npm entries drop the `run start:*` package arguments and set `runtimeHint: "npx"`; the `streamable-http` entry fixes `MCP_TRANSPORT_TYPE` to `http`.
|
|
17
|
+
- **mcp-ts-core 0.13.12:** tool error results carry `data.requestId` and their text closes with `request <id>` (0.13.10); a declared `recovery` hint reaches the client without the handler forwarding it, so the server's 29 `ctx.recoveryFor` forwards are removed with no change to the hints clients receive (0.13.10); argument-rejection hints lead with the field path, e.g. `format: Invalid option: …` (0.13.9); server stack traces (0.13.7) and request context (0.13.8) no longer appear in error `data`.
|
|
18
|
+
- README rewritten; package keywords add `obsidian-md` and `pkm`; `dist/*.mcpb` is excluded from the npm package; `.gitignore`, `.mcpbignore`, `.dockerignore`, and `.env.example` updated; framework skills, scripts, and the CodeQL workflow re-synced.
|
|
19
|
+
|
|
20
|
+
## Security
|
|
21
|
+
|
|
22
|
+
- **`obsidian_delete_note` deletes only on a round that redeems the consent record the server stored when it asked** (operation, caller, resolved path, SHA-256 of the note content). A replayed or pre-supplied confirmation, a made-up `requestState`, or a record for another note, caller, or since-changed content gets a fresh prompt instead of a deletion. Before, an accepted `confirm` answer alone was enough, and a client can send an answer on a call nothing prompted for: mcp-ts-core 0.13.10 limits that to clients that declared elicitation, and the record closes it for them. Records live in `ctx.state` (default in-memory, process-local): a multi-instance HTTP deployment needs a shared storage provider.
|
|
23
|
+
|
|
24
|
+
## Dependencies
|
|
25
|
+
|
|
26
|
+
- `@cyanheads/mcp-ts-core` ^0.13.6 → ^0.13.12
|
|
27
|
+
- `undici` ^8.10.2 → ^8.11.2
|
|
28
|
+
- dev: `@biomejs/biome` ^2.5.14 → ^2.5.15, `@socketsecurity/bun-security-scanner` ^1.1.2 → ^1.1.3, `@types/node` ^26.6.2 → ^26.6.4, `ignore` ^7.0.9 → ^7.0.11, `tsc-alias` ^1.9.5 → ^1.9.7, `vitest` ^5.0.1 → ^5.0.3
|
|
29
|
+
- `marked` stays pinned at `17.0.6` (the Local REST API's markdown-patch engine is on marked 17; marked 18 changes block-token spans)
|
package/dist/index.js
CHANGED
|
@@ -79,12 +79,13 @@ const { services } = await createApp({
|
|
|
79
79
|
instructions: buildInstructions(),
|
|
80
80
|
/**
|
|
81
81
|
* `obsidian_delete_note` confirms through `ctx.requestInput`, which a
|
|
82
|
-
* 2025-era HTTP client can only answer on a stateful session
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* `
|
|
82
|
+
* 2025-era HTTP client can only answer on a stateful session — under
|
|
83
|
+
* stateless HTTP the tool would be unusable for them. Required, not just
|
|
84
|
+
* defaulted: over HTTP, a resolved `stateless` mode (an explicit
|
|
85
|
+
* `MCP_SESSION_MODE=stateless`) fails startup with a `ConfigurationError`.
|
|
86
|
+
* stdio is never refused; `MCP_SESSION_MODE` has no effect there.
|
|
86
87
|
*/
|
|
87
|
-
sessionMode: 'stateful',
|
|
88
|
+
sessionMode: { default: 'stateful', require: 'stateful' },
|
|
88
89
|
teardown: () => obsidian.close(),
|
|
89
90
|
});
|
|
90
91
|
/**
|