obsidian-mcp-server 3.5.3 → 3.5.5
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 +47 -6
- package/CLAUDE.md +47 -6
- package/Dockerfile +3 -2
- package/README.md +144 -100
- package/changelog/3.5.x/3.5.3.md +4 -3
- package/changelog/3.5.x/3.5.4.md +42 -0
- package/changelog/3.5.x/3.5.5.md +20 -0
- package/changelog/template.md +7 -7
- package/dist/config/server-config.d.ts +1 -1
- package/dist/config/server-config.d.ts.map +1 -1
- package/dist/config/server-config.js +9 -2
- package/dist/config/server-config.js.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp-server/resources/definitions/obsidian-vault-note.resource.d.ts.map +1 -1
- package/dist/mcp-server/resources/definitions/obsidian-vault-note.resource.js +19 -1
- package/dist/mcp-server/resources/definitions/obsidian-vault-note.resource.js.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/index.d.ts +157 -65
- 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 +13 -2
- 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 +14 -3
- 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 +9 -0
- 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 +9 -0
- package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-execute-command.tool.d.ts +1 -0
- package/dist/mcp-server/tools/definitions/obsidian-execute-command.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-execute-command.tool.js +1 -0
- package/dist/mcp-server/tools/definitions/obsidian-execute-command.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-get-note.tool.d.ts +9 -0
- 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 +19 -26
- package/dist/mcp-server/tools/definitions/obsidian-get-note.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-list-notes.tool.d.ts +3 -0
- 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 +3 -0
- package/dist/mcp-server/tools/definitions/obsidian-list-notes.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-manage-frontmatter.tool.d.ts +13 -0
- 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 +19 -1
- 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 +17 -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 +22 -3
- package/dist/mcp-server/tools/definitions/obsidian-manage-tags.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.d.ts +4 -0
- package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.js +4 -0
- package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.d.ts +13 -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 +14 -3
- 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 +10 -2
- 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 +10 -2
- 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 +12 -5
- 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 +29 -24
- 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 -2
- 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 +20 -10
- package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.js.map +1 -1
- package/dist/services/obsidian/frontmatter-ops.d.ts +44 -11
- package/dist/services/obsidian/frontmatter-ops.d.ts.map +1 -1
- package/dist/services/obsidian/frontmatter-ops.js +370 -69
- package/dist/services/obsidian/frontmatter-ops.js.map +1 -1
- package/dist/services/obsidian/obsidian-service.d.ts +26 -5
- package/dist/services/obsidian/obsidian-service.d.ts.map +1 -1
- package/dist/services/obsidian/obsidian-service.js +469 -85
- package/dist/services/obsidian/obsidian-service.js.map +1 -1
- package/dist/services/obsidian/section-extractor.d.ts +33 -3
- package/dist/services/obsidian/section-extractor.d.ts.map +1 -1
- package/dist/services/obsidian/section-extractor.js +115 -75
- package/dist/services/obsidian/section-extractor.js.map +1 -1
- package/dist/services/obsidian/types.d.ts +11 -4
- package/dist/services/obsidian/types.d.ts.map +1 -1
- package/manifest.json +1 -1
- package/package.json +9 -9
- package/server.json +3 -3
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
|
|
|
@@ -21,12 +21,14 @@
|
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
24
|
-
##
|
|
24
|
+
## Overview
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
Read, write, search, and surgically edit Obsidian vault notes — sections, frontmatter, tags — over the Local REST API plugin, with folder-scoped read/write permissions built in. Runs as a stdio process or a local Streamable HTTP server.
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
### Tools
|
|
29
|
+
|
|
30
|
+
| Tool | Description |
|
|
31
|
+
|:---|:---|
|
|
30
32
|
| `obsidian_get_note` | Read a note as raw content, full structured form (content + frontmatter + tags + stat, with optional outgoing links), structural document map, or a single section. |
|
|
31
33
|
| `obsidian_list_notes` | List notes and subdirectories under a vault path. Recursive walk (default depth 2, max depth 20; 1000-entry cap) with optional `extension` and `nameRegex` filters. |
|
|
32
34
|
| `obsidian_list_tags` | List vault tags with usage counts, including hierarchical parents. Ordered by count descending and capped at `limit` (default 200, max 10000), with the withheld remainder disclosed. Optional `nameRegex` and `minCount` narrow the set first. |
|
|
@@ -42,124 +44,161 @@ Fourteen tools grouped by shape — readers fetch notes and metadata, writers cr
|
|
|
42
44
|
| `obsidian_open_in_ui` | Open a file in the Obsidian app UI, with `failIfMissing` and `newLeaf` toggles. |
|
|
43
45
|
| `obsidian_execute_command` | Execute an Obsidian command-palette command by ID. **Opt-in via `OBSIDIAN_ENABLE_COMMANDS=true`.** |
|
|
44
46
|
|
|
45
|
-
###
|
|
47
|
+
### Resources
|
|
46
48
|
|
|
47
|
-
|
|
49
|
+
| Resource | Description |
|
|
50
|
+
|:---|:---|
|
|
51
|
+
| `obsidian://vault/{+path}` | A note in the vault — content, frontmatter, tags, and file metadata. |
|
|
52
|
+
| `obsidian://tags` | All tags found across the vault, with usage counts (full snapshot). |
|
|
53
|
+
| `obsidian://status` | Server reachability, auth status, plugin/Obsidian version info, and registered API extensions. |
|
|
48
54
|
|
|
49
|
-
- `
|
|
50
|
-
- `format: "full"` — content, frontmatter, tags, and file metadata; pass `includeLinks: true` to also parse outgoing wiki and markdown link references from the body (vault-internal only — external URLs are filtered)
|
|
51
|
-
- `format: "document-map"` — catalog of headings, block references, and frontmatter fields
|
|
52
|
-
- `format: "section"` — single heading/block/frontmatter section value (requires `section`); heading sections include the full subtree under that heading. The response echoes the locator the read resolved to in `sectionTarget` as a full `Parent::Child` path, and when a bare leaf name matches several headings it lists every colliding path in `candidates` — the read still returns the first match
|
|
55
|
+
Vault-note and tag data are also reachable via tools — `obsidian_get_note` for `obsidian://vault/{+path}`, `obsidian_list_tags` for `obsidian://tags` (count-ranked and capped, unlike the resource's raw snapshot). `obsidian://status` has no tool equivalent. Resources exist for clients that prefer attaching a note or vault snapshot to a conversation.
|
|
53
56
|
|
|
54
|
-
|
|
57
|
+
## Capability reference
|
|
55
58
|
|
|
56
|
-
|
|
59
|
+
### `obsidian_get_note` <sub>tool</sub>
|
|
57
60
|
|
|
58
|
-
|
|
61
|
+
- `format: "content" | "full" | "document-map" | "section"` selects the projection; `full` accepts `includeLinks: true` for outgoing wiki/markdown links (vault-internal only — external URLs are filtered)
|
|
62
|
+
- Addressed by vault `path`, the `active` file, or a `periodic` note (`daily` / `weekly` / `monthly` / `quarterly` / `yearly`)
|
|
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`
|
|
59
66
|
|
|
60
|
-
|
|
67
|
+
---
|
|
61
68
|
|
|
62
|
-
|
|
63
|
-
- `jsonlogic` — JSONLogic tree evaluated against `path`, `content`, `frontmatter.<key>`, `tags`, and `stat.{ctime,mtime,size}`; custom `glob` and `regexp` operators, both taking `[PATTERN, VALUE]` — pattern first, then the field reference: `{"glob": ["Projects/*.md", {"var": "path"}]}`. The reverse order compiles the note's own field as the pattern: `glob` then matches nothing, and `regexp` fails outright on whatever the field parses as. This is also how backlinks are expressed, since there is no dedicated tool or upstream endpoint for them: `{"regexp": ["\\[\\[Target Note(\\||#|\\]\\])", {"var": "content"}]}` returns every note whose body wikilinks `Target Note`.
|
|
64
|
-
- `omnisearch` — BM25-ranked search via the community [Omnisearch](https://github.com/scambier/obsidian-omnisearch) plugin. Supports quoted phrases, `-exclusion`, `path:` / `ext:` filters, typo tolerance, and PDF + OCR coverage (via [Text Extractor](https://github.com/scambier/obsidian-text-extractor)). Only present in the mode enum when the plugin's HTTP server is reachable at startup; the upstream hard-caps results at 50 — narrow the query to surface more (the response carries `truncated: true` when the cap was likely hit).
|
|
69
|
+
### `obsidian_list_notes` <sub>tool</sub>
|
|
65
70
|
|
|
66
|
-
|
|
71
|
+
- Recursive walk from `path` (default vault root); `depth` 1–20 (default 2 = target plus immediate children)
|
|
72
|
+
- Optional `extension` and `nameRegex` (≤256 chars, no nested quantifiers) filters; a directory failing `nameRegex` is skipped without recursing into it
|
|
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
|
|
67
75
|
|
|
68
76
|
---
|
|
69
77
|
|
|
70
|
-
### `
|
|
78
|
+
### `obsidian_list_tags` <sub>tool</sub>
|
|
71
79
|
|
|
72
|
-
|
|
80
|
+
- Vault-wide tag counts, including hierarchical parents (`work/tasks` contributes to both `work` and `work/tasks`)
|
|
81
|
+
- Ordered by count descending, capped at `limit` (default 200, max 10000); optional `nameRegex` and `minCount` narrow the candidate set before ranking
|
|
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
|
|
73
84
|
|
|
74
|
-
|
|
75
|
-
- With `section` — `PATCH`-with-replace against the named heading/block/frontmatter field, leaving the rest of the file untouched. The `overwrite` flag is ignored in section mode.
|
|
85
|
+
---
|
|
76
86
|
|
|
77
|
-
|
|
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`
|
|
78
92
|
|
|
79
93
|
---
|
|
80
94
|
|
|
81
|
-
### `
|
|
95
|
+
### `obsidian_search_notes` <sub>tool</sub>
|
|
96
|
+
|
|
97
|
+
- `mode: "text" | "jsonlogic"` always; `"omnisearch"` is added to the schema only when the Omnisearch plugin's HTTP server is reachable at startup (restart to re-probe)
|
|
98
|
+
- `text` — whitespace-split tokens, all required, each matched case-insensitively as a substring (quotes are literal, so there is no phrase operator), with `contextLength`-sized context windows (default 100) and an optional `pathPrefix`; tokens within 2 × `contextLength` of each other, such as a phrase's words, share one match location; `jsonlogic` — a JSONLogic tree with `var` paths into `path` / `content` / `frontmatter.<key>` / `tags` / `stat.{ctime,mtime,size}`, plus `glob` / `regexp` operators taking `[PATTERN, VALUE]`; `omnisearch` — BM25-ranked, quoted phrases, `-exclusion`, `path:` / `ext:` filters, typo tolerance, PDF/OCR via Text Extractor, hard-capped at 50 upstream hits (`truncated: true` when likely hit)
|
|
99
|
+
- Cursor pagination — omit `cursor` for page one, pass `nextCursor` from the prior response; text-mode hits additionally clip to `maxMatchesPerHit` match locations (default 10), flagged with `truncated` / `totalMatches`
|
|
100
|
+
- No dedicated backlinks tool — express "what links here" via `jsonlogic`: `{"regexp": ["\\[\\[Target Note(\\||#|\\]\\])", {"var": "content"}]}`
|
|
82
101
|
|
|
83
|
-
|
|
102
|
+
---
|
|
84
103
|
|
|
85
|
-
|
|
86
|
-
- With `section` — `PATCH`-with-append against the named heading, block reference, or frontmatter field. The file must exist (PATCH preflight throws `note_missing` otherwise). Pass `createTargetIfMissing: true` to bring the section itself into existence inside an existing file. Block-reference targets concatenate adjacent to the block line without a separator — include a leading newline in `content` if you want one.
|
|
104
|
+
### `obsidian_write_note` <sub>tool</sub>
|
|
87
105
|
|
|
88
|
-
|
|
106
|
+
- Without `section` — full-file write; refuses to clobber an existing note unless `overwrite: true` (`file_exists` conflict otherwise, naming the surgical-edit tools as the alternative)
|
|
107
|
+
- With `section` — `PATCH`-with-replace against a heading/block/frontmatter target, leaving the rest of the file untouched (`overwrite` is ignored); a bare heading leaf shared by several headings fails with `ambiguous_section` unless one of them has no parent heading, which the write then targets, and a full heading path that repeats in the note fails the same way
|
|
108
|
+
- Output reports `created`, plus `previousSizeInBytes` / `currentSizeInBytes` on every call to spot an accidental clobber or a mistyped path
|
|
89
109
|
|
|
90
110
|
---
|
|
91
111
|
|
|
92
|
-
### `
|
|
112
|
+
### `obsidian_append_to_note` <sub>tool</sub>
|
|
93
113
|
|
|
94
|
-
|
|
114
|
+
- Without `section` — appends to an existing file, or creates it with the given content as the whole body (`created: true` flags the second case)
|
|
115
|
+
- With `section` — appends to a heading/block/frontmatter target; the file must already exist, and `createTargetIfMissing: true` brings the section itself into existence
|
|
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
|
|
95
118
|
|
|
96
|
-
|
|
97
|
-
- `operation: "prepend"` adds before the section
|
|
98
|
-
- `operation: "replace"` swaps it out
|
|
99
|
-
- Targets: heading path, block reference ID, or frontmatter field
|
|
119
|
+
---
|
|
100
120
|
|
|
101
|
-
|
|
121
|
+
### `obsidian_patch_note` <sub>tool</sub>
|
|
102
122
|
|
|
103
|
-
|
|
123
|
+
- `operation: "append" | "prepend" | "replace"` against one heading, block reference, or frontmatter field per call
|
|
124
|
+
- Heading targets accept the full `Parent::Child` path or a bare leaf name; a leaf matching several headings fails with `ambiguous_section` and lists the candidates, unless one of them has no parent heading, which the patch then targets; a full path that repeats in the note fails with `ambiguous_section` too
|
|
125
|
+
- `patchOptions`: `createTargetIfMissing`, `applyIfContentPreexists` (idempotency guard — otherwise `content_preexists`), `trimTargetWhitespace`
|
|
104
126
|
|
|
105
127
|
---
|
|
106
128
|
|
|
107
|
-
### `obsidian_replace_in_note`
|
|
129
|
+
### `obsidian_replace_in_note` <sub>tool</sub>
|
|
108
130
|
|
|
109
|
-
|
|
131
|
+
- One or more `replacements`, applied in array order, each over the previous one's output
|
|
132
|
+
- `scope: "body"` (default, frontmatter left byte-identical) | `"frontmatter"` | `"both"`; frontmatter/both re-parse the rewritten YAML afterward and write nothing if it breaks (`frontmatter_invalid`)
|
|
133
|
+
- Per-replacement options: `useRegex` (≤1024 chars, no nested quantifiers), `caseSensitive`, `wholeWord` (`\b…\b` in both modes), `flexibleWhitespace` (literal mode only), `replaceAll` (default `true`)
|
|
134
|
+
- `perReplacement[]` reports `bodyCount` / `frontmatterCount` per entry; `totalReplacements` sums them
|
|
110
135
|
|
|
111
|
-
|
|
136
|
+
---
|
|
112
137
|
|
|
113
|
-
|
|
114
|
-
- `frontmatter` — only the YAML between the `---` fences. The fences themselves are never matched.
|
|
115
|
-
- `both` — each replacement runs over the frontmatter and then the body; `perReplacement[]` reports `bodyCount` and `frontmatterCount` separately.
|
|
138
|
+
### `obsidian_manage_frontmatter` <sub>tool</sub>
|
|
116
139
|
|
|
117
|
-
|
|
140
|
+
- `operation: "get" | "set" | "delete"` on a single frontmatter `key`; `set` requires a JSON-typed `value` (string, number, boolean, array, or object)
|
|
141
|
+
- `get` needs read access; `set` / `delete` need the path inside `OBSIDIAN_WRITE_PATHS` with `OBSIDIAN_READ_ONLY=false`
|
|
142
|
+
- `set` / `delete` return the full `frontmatter` after the change plus `previousSizeInBytes` / `currentSizeInBytes`
|
|
118
143
|
|
|
119
|
-
|
|
144
|
+
---
|
|
120
145
|
|
|
121
|
-
|
|
122
|
-
- `caseSensitive` — when `false`, match case-insensitively
|
|
123
|
-
- `wholeWord` — wrap the pattern in `\b…\b`; works in both literal and regex modes
|
|
124
|
-
- `flexibleWhitespace` — substitute any run of whitespace in `search` with `\s+`. Literal mode only — has no effect when `useRegex: true` (express it directly).
|
|
125
|
-
- `replaceAll` — when `false`, only the first match is replaced. Under `scope: 'both'` that one substitution goes to the frontmatter when it matches there, and to the body otherwise.
|
|
146
|
+
### `obsidian_manage_tags` <sub>tool</sub>
|
|
126
147
|
|
|
127
|
-
|
|
148
|
+
- `operation: "add" | "remove" | "list"`; `location: "frontmatter"` (default, canonical `tags:` array) | `"inline"` (body `#tag`, `add` appends at end-of-file) | `"both"` (reconciles both)
|
|
149
|
+
- Inline detection skips fenced/inline code spans, link spans (`[[...]]`, `[text](...)`, `[text][ref]`), HTML comments, and math (`$…$`, `$$…$$`), so a heading anchor or wikilink alias is never mistaken for a tag; `%% … %%` comments are still read, as Obsidian reads them
|
|
150
|
+
- Inline tags follow Obsidian's grammar: a tag starts at line start, after whitespace, or right after markup such as `**`, `==`, `<br>`, or a `\`-escape (`**#x**` is a tag; `(#x`, `.#x`, `a *#x`, and `\#x` are not) and runs through letters and digits in any script, emoji, `_`, `-`, and `/`, with at least one character that is not an ASCII digit (`#1990s`, `#café`, `#日本語`, and `#✅done` are tags; `#1984` is not)
|
|
151
|
+
- `add` / `remove` report `applied` vs. `skipped` tags plus the full `tags` set after the change; `list` ignores the input `tags` array
|
|
128
152
|
|
|
129
153
|
---
|
|
130
154
|
|
|
131
|
-
### `
|
|
132
|
-
|
|
133
|
-
Add, remove, or list tags on a note. Operates on one of two representations, defaulting to the canonical Obsidian frontmatter location:
|
|
155
|
+
### `obsidian_delete_note` <sub>tool</sub>
|
|
134
156
|
|
|
135
|
-
-
|
|
136
|
-
-
|
|
137
|
-
-
|
|
157
|
+
- Always asks for confirmation first — the initial call returns an elicitation request naming the file's byte size, and is retried with the answer; declining fails with `cancelled` and issues no `DELETE`
|
|
158
|
+
- No API-level undo — recovery requires Obsidian's local trash
|
|
159
|
+
- Requires an MCP client that can serve an elicitation round-trip; every other tool works without one
|
|
138
160
|
|
|
139
|
-
|
|
161
|
+
---
|
|
140
162
|
|
|
141
|
-
|
|
163
|
+
### `obsidian_open_in_ui` <sub>tool</sub>
|
|
142
164
|
|
|
143
|
-
|
|
165
|
+
- `failIfMissing` (default `true`) controls open-vs-create: opening an existing file needs read access, opening a missing one (with `failIfMissing: false`) creates it and needs write access
|
|
166
|
+
- `newLeaf` opens in a split pane instead of the active one
|
|
167
|
+
- Same forgiving path resolution as `obsidian_get_note` (case fallback, `Did you mean` suggestions); `obsidian_delete_note` deliberately doesn't get it — a destructive op never silently rewrites its target
|
|
168
|
+
- Output reports `createdIfMissing` so the caller can tell which branch ran
|
|
144
169
|
|
|
145
170
|
---
|
|
146
171
|
|
|
147
|
-
### `
|
|
172
|
+
### `obsidian_execute_command` <sub>tool</sub>
|
|
148
173
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
174
|
+
- Dispatches an Obsidian command-palette command by `commandId` (discover via `obsidian_list_commands`); runs with the same authority as a keyboard invocation
|
|
175
|
+
- **Opt-in via `OBSIDIAN_ENABLE_COMMANDS=true`** — absent from `tools/list` when unset
|
|
176
|
+
- Behavior is command-dependent — some are destructive (delete file, close vault), some open UI
|
|
152
177
|
|
|
153
178
|
---
|
|
154
179
|
|
|
155
|
-
### `
|
|
180
|
+
### `obsidian://vault/{+path}` <sub>resource</sub>
|
|
181
|
+
|
|
182
|
+
- The `{+path}` segment captures everything after `/vault/`, including slashes
|
|
183
|
+
- Paths may be sent literally or percent-encoded — `Folder/Test Note.md` and `Folder/Test%20Note.md` resolve to the same note, as do non-ASCII names and a bare `%`
|
|
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
|
|
156
186
|
|
|
157
|
-
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
### `obsidian://tags` <sub>resource</sub>
|
|
158
190
|
|
|
159
|
-
|
|
191
|
+
- Full snapshot of the upstream `/tags/` payload — unsorted, uncapped, includes hierarchical parents
|
|
192
|
+
- Not a mirror of `obsidian_list_tags`: no count-descending order, no `limit` / `nameRegex` / `minCount`
|
|
160
193
|
|
|
161
194
|
---
|
|
162
195
|
|
|
196
|
+
### `obsidian://status` <sub>resource</sub>
|
|
197
|
+
|
|
198
|
+
- Reachability, plugin version, `authenticated` (whether the configured `OBSIDIAN_API_KEY` was accepted), and plugin manifest info
|
|
199
|
+
- `apiExtensions[]` lists registered plugin extensions — check for `local-rest-api-periodic-notes` before relying on `periodic` targets on plugin v5.0.2 and later
|
|
200
|
+
- Still reports reachability when the API key is misconfigured; only `authenticated` reflects the key's validity
|
|
201
|
+
|
|
163
202
|
## Path policy (folder-scoped permissions)
|
|
164
203
|
|
|
165
204
|
Three optional env vars gate which vault paths each tool can target. **Default unset = full vault** for both reads and writes — backwards compatible.
|
|
@@ -183,41 +222,24 @@ Denies are typed `path_forbidden` (JSON-RPC code `Forbidden`) with the active sc
|
|
|
183
222
|
|
|
184
223
|
The startup banner logs the active scope so operators can verify their config at boot.
|
|
185
224
|
|
|
186
|
-
---
|
|
187
|
-
|
|
188
|
-
## Resources
|
|
189
|
-
|
|
190
|
-
| Type | URI | Description |
|
|
191
|
-
|:---|:---|:---|
|
|
192
|
-
| Resource | `obsidian://vault/{+path}` | A note in the vault — content, frontmatter, tags, and file metadata. |
|
|
193
|
-
| Resource | `obsidian://tags` | All tags found across the vault, with usage counts. |
|
|
194
|
-
| Resource | `obsidian://status` | Server reachability, auth status, plugin/Obsidian version info, and the plugin manifest. |
|
|
195
|
-
|
|
196
|
-
All resource data is also reachable via tools — `obsidian_get_note` for `obsidian://vault/{+path}`, `obsidian_list_tags` for `obsidian://tags`. Resources exist for clients that prefer attaching a specific note or vault snapshot to a conversation. The tag pair is not a mirror: `obsidian://tags` keeps snapshot semantics and returns the upstream payload whole and unsorted, while `obsidian_list_tags` orders by count and caps.
|
|
197
|
-
|
|
198
225
|
## Features
|
|
199
226
|
|
|
200
|
-
Built on [`@cyanheads/mcp-ts-core`](https://
|
|
201
|
-
|
|
202
|
-
- Declarative tool and resource definitions — single file per primitive, framework handles registration and validation
|
|
203
|
-
- Unified error handling — handlers throw, framework catches, classifies, and formats. Tools advertise their failure surface via typed `errors[]` contracts.
|
|
204
|
-
- Server-level `instructions` on `initialize` — surfaces deployment-specific orientation (active path policy, read-only mode, command-palette toggle) to spec-compliant clients alongside the static tool/resource catalog
|
|
205
|
-
- Pluggable auth on the HTTP transport: `none`, `jwt`, `oauth`
|
|
206
|
-
- Structured logging with optional OpenTelemetry tracing
|
|
207
|
-
- STDIO and Streamable HTTP transports
|
|
208
|
-
|
|
209
|
-
The server itself is stateless — every tool call hits the Local REST API directly. The framework's storage backends, request-state KV, and progress streams aren't used here; Obsidian is single-vault and there's nothing to persist between calls.
|
|
227
|
+
Built on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core): stdio and Streamable HTTP transports, pluggable auth (`none` / `jwt` / `oauth`), swappable storage (`in-memory`, `filesystem`, `Supabase`, `Cloudflare KV/R2/D1`), structured logging with optional OpenTelemetry tracing.
|
|
210
228
|
|
|
211
229
|
Obsidian-specific:
|
|
212
230
|
|
|
213
231
|
- Wraps the [Obsidian Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api) plugin — typed client, deterministic error mapping
|
|
214
232
|
- Section-aware editing across headings, block references, and frontmatter fields via `PATCH`-with-target operations
|
|
215
|
-
-
|
|
216
|
-
-
|
|
217
|
-
-
|
|
218
|
-
|
|
219
|
-
-
|
|
220
|
-
|
|
233
|
+
- Search across three modes — text, JSONLogic, and (when reachable) BM25-ranked Omnisearch — cursor-paginated per the MCP 2025-11-25 spec
|
|
234
|
+
- Tag reconciliation across both representations: frontmatter `tags:` array and inline `#tag` syntax
|
|
235
|
+
- Folder-scoped read/write permissions via `OBSIDIAN_READ_PATHS` / `OBSIDIAN_WRITE_PATHS` and a global `OBSIDIAN_READ_ONLY` kill switch; opt-in command-palette pair gated by `OBSIDIAN_ENABLE_COMMANDS`. Server-level `instructions` on `initialize` report the active policy to the caller
|
|
236
|
+
|
|
237
|
+
Agent-friendly output:
|
|
238
|
+
|
|
239
|
+
- Recovery-guided errors — every declared failure carries a `reason`, a JSON-RPC code, and a `recovery.hint` written for that case, so a rejection names what to do next instead of only what broke
|
|
240
|
+
- Size-delta self-correction — every mutating tool returns `previousSizeInBytes` / `currentSizeInBytes`, so a caller can spot an accidental clobber or unexpected upstream behavior without a follow-up read
|
|
241
|
+
- Ambiguity surfaced structurally — a heading leaf name shared by several headings returns `candidates` instead of silently picking one; tag operations report `applied` vs. `skipped` so a caller sees exactly what changed
|
|
242
|
+
- Discriminated output contracts — `format` on `obsidian_get_note`, `operation` on `obsidian_manage_frontmatter` and `obsidian_manage_tags`, `mode` on `obsidian_search_notes` — callers branch on typed fields instead of parsing text
|
|
221
243
|
|
|
222
244
|
## Getting started
|
|
223
245
|
|
|
@@ -259,6 +281,28 @@ Or with npx (no Bun required):
|
|
|
259
281
|
}
|
|
260
282
|
```
|
|
261
283
|
|
|
284
|
+
Or with Docker:
|
|
285
|
+
|
|
286
|
+
```json
|
|
287
|
+
{
|
|
288
|
+
"mcpServers": {
|
|
289
|
+
"obsidian-mcp-server": {
|
|
290
|
+
"type": "stdio",
|
|
291
|
+
"command": "docker",
|
|
292
|
+
"args": [
|
|
293
|
+
"run", "-i", "--rm",
|
|
294
|
+
"-e", "MCP_TRANSPORT_TYPE=stdio",
|
|
295
|
+
"-e", "MCP_LOG_LEVEL=info",
|
|
296
|
+
"-e", "OBSIDIAN_API_KEY=your-local-rest-api-key",
|
|
297
|
+
"ghcr.io/cyanheads/obsidian-mcp-server:latest"
|
|
298
|
+
]
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
The default `OBSIDIAN_BASE_URL` (`http://127.0.0.1:27123`) points at the container's own loopback, not your host — add `-e OBSIDIAN_BASE_URL=http://host.docker.internal:27123` (Docker Desktop) or run with `--network host` (Linux) so the container can reach the plugin.
|
|
305
|
+
|
|
262
306
|
For Streamable HTTP, set the transport and start the server. Inline env vars work for one-off runs; for repeated use, copy values into `.env` (see [`.env.example`](./.env.example)) and run `bun run start:http`.
|
|
263
307
|
|
|
264
308
|
```sh
|
|
@@ -272,7 +316,7 @@ MCP_TRANSPORT_TYPE=http OBSIDIAN_API_KEY=... bun run start:http
|
|
|
272
316
|
- The [Obsidian Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api) plugin, **v4.0.0 through v5.x**, installed and enabled in your vault. Generate an API key in **Settings → Community Plugins → Local REST API** and copy it into `OBSIDIAN_API_KEY`. Plugin v6.0 removes the markdown-patch 1.x wire format this server pins for section-targeted writes and the document map.
|
|
273
317
|
- Periodic-note targets (`target: { "type": "periodic" }`) work across that whole range: natively on plugin **v5.0.1 and earlier**, and on **v5.0.2 and later** — which moved the `/periodic/` routes out of the plugin — once the companion [periodic-notes API extension](https://github.com/coddingtonbear/obsidian-local-rest-api-periodic-notes) is installed. Without that extension on v5.0.2+, periodic targets fail with a `periodic_unsupported` error naming it; `obsidian://status` lists the registered extensions if you want to check first. Every other target type is unaffected.
|
|
274
318
|
- An MCP client that can answer an input request (elicitation). `obsidian_delete_note` always asks for confirmation before deleting, so a client without that support can read and write notes but cannot delete one.
|
|
275
|
-
- This server defaults to `http://127.0.0.1:27123` for simplicity. Enable **"Non-encrypted (HTTP) Server"** in the plugin settings to use it. To use the always-on HTTPS port instead, set `OBSIDIAN_BASE_URL=https://127.0.0.1:27124`; the plugin's self-signed cert is handled by `OBSIDIAN_VERIFY_SSL=false` (the default).
|
|
319
|
+
- This server defaults to `http://127.0.0.1:27123` for simplicity. Enable **"Non-encrypted (HTTP) Server"** in the plugin settings to use it. To use the always-on HTTPS port instead, set `OBSIDIAN_BASE_URL=https://127.0.0.1:27124`; the plugin's self-signed cert is handled by `OBSIDIAN_VERIFY_SSL=false` (the default), which relaxes verification for this server's requests to that endpoint only.
|
|
276
320
|
|
|
277
321
|
### Installation
|
|
278
322
|
|
|
@@ -306,8 +350,8 @@ MCP_TRANSPORT_TYPE=http OBSIDIAN_API_KEY=... bun run start:http
|
|
|
306
350
|
| Variable | Description | Default |
|
|
307
351
|
|:---------|:------------|:--------|
|
|
308
352
|
| `OBSIDIAN_API_KEY` | **Required.** Bearer token for the Obsidian Local REST API plugin. | — |
|
|
309
|
-
| `OBSIDIAN_BASE_URL` | Base URL of the Local REST API plugin. Use `https://127.0.0.1:27124` for the always-on HTTPS port (self-signed cert). | `http://127.0.0.1:27123` |
|
|
310
|
-
| `OBSIDIAN_VERIFY_SSL` | Verify the TLS certificate. Default `false` because the plugin uses a self-signed cert.
|
|
353
|
+
| `OBSIDIAN_BASE_URL` | Base URL of the Local REST API plugin. Use `https://127.0.0.1:27124` for the always-on HTTPS port (self-signed cert). A trailing slash is stripped at startup. When nothing answers there (Obsidian closed, plugin disabled, wrong host or port), calls fail with `obsidian_unreachable` — a `GET`, `PUT`, or `DELETE` after its retries, any other request on the first attempt. | `http://127.0.0.1:27123` |
|
|
354
|
+
| `OBSIDIAN_VERIFY_SSL` | Verify the TLS certificate. Default `false` because the plugin uses a self-signed cert. The relaxation is applied per request, to an `https:` `OBSIDIAN_BASE_URL` only — every other HTTPS connection the process makes still verifies normally, on both Bun and Node. With `true`, a certificate the runtime does not trust fails every call on its first attempt with `certificate_rejected`. | `false` |
|
|
311
355
|
| `OBSIDIAN_REQUEST_TIMEOUT_MS` | Per-request timeout in milliseconds. | `30000` |
|
|
312
356
|
| `OBSIDIAN_ENABLE_COMMANDS` | Opt-in flag for the command-palette pair (`obsidian_list_commands` + `obsidian_execute_command`). Off by default — Obsidian commands are opaque and can be destructive. | `false` |
|
|
313
357
|
| `OBSIDIAN_READ_PATHS` | Comma-separated vault-relative folder allowlist for read operations. Prefix-based with implicit recursion; case-insensitive; trailing slashes normalized. Unset = full vault. Write paths are implicitly readable. | unset |
|
|
@@ -318,7 +362,7 @@ MCP_TRANSPORT_TYPE=http OBSIDIAN_API_KEY=... bun run start:http
|
|
|
318
362
|
| `MCP_HTTP_HOST` | Host for the HTTP server. | `127.0.0.1` |
|
|
319
363
|
| `MCP_HTTP_PORT` | Port for the HTTP server. | `3010` |
|
|
320
364
|
| `MCP_HTTP_ENDPOINT_PATH` | Endpoint path for the JSON-RPC handler. | `/mcp` |
|
|
321
|
-
| `MCP_SESSION_MODE` | Session handling for the HTTP transport: `stateless`, `stateful`, or `auto`.
|
|
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` |
|
|
322
366
|
| `MCP_PUBLIC_URL` | Public origin override for TLS-terminating reverse-proxy deployments (landing page, Server Card, RFC 9728 metadata). | unset |
|
|
323
367
|
| `MCP_AUTH_MODE` | Auth mode: `none`, `jwt`, or `oauth`. | `none` |
|
|
324
368
|
| `MCP_AUTH_SECRET_KEY` | **Required when `MCP_AUTH_MODE=jwt`.** ≥32-char shared secret used to verify incoming JWTs. | — |
|
|
@@ -360,7 +404,7 @@ docker build -t obsidian-mcp-server .
|
|
|
360
404
|
docker run --rm -e OBSIDIAN_API_KEY=your-key -p 3010:3010 obsidian-mcp-server
|
|
361
405
|
```
|
|
362
406
|
|
|
363
|
-
The Dockerfile defaults to HTTP transport,
|
|
407
|
+
The Dockerfile defaults to HTTP transport, stateful session mode (required for the `obsidian_delete_note` confirmation round), and logs to `/var/log/obsidian-mcp-server`. Point `OBSIDIAN_BASE_URL` at `http://host.docker.internal:27123` (Docker Desktop) or run with `--network host` (Linux) so the container reaches the plugin on your host. OpenTelemetry peer dependencies are installed by default — build with `--build-arg OTEL_ENABLED=false` to omit them.
|
|
364
408
|
|
|
365
409
|
The image binds to `0.0.0.0` inside the container (required for Docker port mapping). For any deployment reachable beyond your own machine, set `MCP_AUTH_MODE=jwt` (with `MCP_AUTH_SECRET_KEY`) or `oauth` — otherwise the listener forwards your `OBSIDIAN_API_KEY` to the vault on behalf of every caller.
|
|
366
410
|
|
package/changelog/3.5.x/3.5.3.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
summary: "
|
|
2
|
+
summary: "The .mcpb bundle now passes install-time settings instead of literal ${OBSIDIAN_*} text, and plugin installs no longer blank out exported OBSIDIAN_* vars; empty or placeholder ${...} env values read as unset; dev skills move to framework-skills/ so plugin installs stop loading them; Bun floor rises to >=1.4.0."
|
|
3
3
|
breaking: false
|
|
4
4
|
security: false
|
|
5
5
|
---
|
|
@@ -8,14 +8,15 @@ security: false
|
|
|
8
8
|
|
|
9
9
|
## Changed
|
|
10
10
|
|
|
11
|
-
- **Bun engines floor raised to `>=1.4.0`** — required
|
|
11
|
+
- **Bun engines floor raised to `>=1.4.0`** — required since mcp-ts-core 0.12.9.
|
|
12
12
|
- **Development skills moved from `skills/` to `framework-skills/`** — Claude Code and Codex auto-load a plugin's root `skills/`, so installing this server's plugin no longer loads its development skills into the installing agent ([cyanheads/mcp-ts-core#428](https://github.com/cyanheads/mcp-ts-core/issues/428)).
|
|
13
13
|
|
|
14
14
|
## Fixed
|
|
15
15
|
|
|
16
16
|
- **An empty or whole-value `${...}` env value now reads as unset** — `OBSIDIAN_REQUEST_TIMEOUT_MS=''` starts with the default timeout and `OBSIDIAN_API_KEY='${OBSIDIAN_API_KEY}'` fails at startup as a missing key rather than as a format error; the server's own redundant empty-string guards on `baseUrl`/`omnisearchUrl` were removed as a result ([cyanheads/mcp-ts-core#427](https://github.com/cyanheads/mcp-ts-core/issues/427)).
|
|
17
17
|
- **A cancelled single-request HTTP POST's SSE stream now closes immediately**, instead of waiting for keep-alive or session expiry ([cyanheads/mcp-ts-core#401](https://github.com/cyanheads/mcp-ts-core/issues/401)).
|
|
18
|
-
- **
|
|
18
|
+
- **The `.mcpb` bundle now passes the settings entered at install to the server** — `manifest.json`'s `mcp_config` referenced `${OBSIDIAN_*}`, which the MCPB host does not substitute, so the server received the literal placeholder text; every option is now wired as `${user_config.<key>}`.
|
|
19
|
+
- **Plugin installs no longer blank out exported env vars** — `.claude-plugin/plugin.json` and `.codex-plugin/mcp.json` set `OBSIDIAN_API_KEY`/`OBSIDIAN_BASE_URL` to `""`, replacing a value the user had exported; the Claude Code plugin now declares them as `userConfig` options, and the Codex plugin forwards them through `env_vars`.
|
|
19
20
|
|
|
20
21
|
## Dependencies
|
|
21
22
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "TLS certificate verification is now scoped to each Obsidian request instead of relaxed process-wide on Bun, frontmatter/tag edits refuse to write rather than destroy content on unsafe YAML, and mcp-ts-core moves to ^0.13.6."
|
|
3
|
+
breaking: false
|
|
4
|
+
security: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 3.5.4 — 2026-09-19
|
|
8
|
+
|
|
9
|
+
## Added
|
|
10
|
+
|
|
11
|
+
- **`createApp` declares `sessionMode: 'stateful'` and a `teardown` hook** ([#120](https://github.com/cyanheads/obsidian-mcp-server/issues/120)) — `obsidian_delete_note`'s `ctx.requestInput` confirmation needs a stateful session on a 2025-era HTTP client. Under `stateless` the round trip is refused before any vault call, now as a typed `InvalidRequest` carrying `data.reason: "client_capability_missing"` where mcp-ts-core served a bare `isError` text block before 0.13.5. `teardown` closes `ObsidianService`'s undici dispatcher, releasing keep-alive sockets to the Local REST API and Omnisearch.
|
|
12
|
+
- **Every `errors[]` reason the service layer raises declares `thrownBy: 'service'`** (path policy, periodic-note routing, ambiguous case matches), and `obsidian_delete_note`'s `cancelled` reason carries `severity: 'notice'` — both are mcp-ts-core 0.13.5/0.13.6 error-contract metadata; the entries stay advertised and thrown exactly as before.
|
|
13
|
+
|
|
14
|
+
## Changed
|
|
15
|
+
|
|
16
|
+
- **Tool error text now closes with the terms a caller branches on** — `(reason <reason>)`, joined by ` · ` with `retryable` or `not retryable` where the contract declares it — and drops the `Recovery:` line when the message already states the hint verbatim (mcp-ts-core 0.13.5).
|
|
17
|
+
- **A schema-rejected tool call carries `data.reason: "invalid_arguments"`** and a synthesized `Recovery:` hint naming the offending field (mcp-ts-core 0.13.3).
|
|
18
|
+
- **Tool arguments pass an ordered pre-validation step** before schema parsing — client-added root keys are dropped, an undeclared key is rewritten to a case-folded match, and a JSON-stringified array is repaired and re-parsed once on failure (mcp-ts-core 0.13.4).
|
|
19
|
+
|
|
20
|
+
## Fixed
|
|
21
|
+
|
|
22
|
+
- **`OBSIDIAN_BASE_URL` strips a trailing slash** ([#121](https://github.com/cyanheads/obsidian-mcp-server/issues/121)) — a slash-terminated value doubled the separator on every request path and 404'd the whole vault.
|
|
23
|
+
- **A failed request's log cause names the URL that was actually fetched** ([#122](https://github.com/cyanheads/obsidian-mcp-server/issues/122)), so a malformed base URL no longer reads as a clean 404 against a healthy vault. The client-facing `path` and message are unchanged.
|
|
24
|
+
- **`obsidian_manage_frontmatter` (`delete`) and `obsidian_manage_tags` refuse to write instead of destroying content** when the note's frontmatter doesn't parse as a mapping or the mutation leaves YAML the emitter can't re-emit — a dangling alias, for instance ([#123](https://github.com/cyanheads/obsidian-mcp-server/issues/123), [#124](https://github.com/cyanheads/obsidian-mcp-server/issues/124)). Both tools declare the new `frontmatter_invalid` reason; nothing is written on refusal, and `obsidian_manage_tags` skips the inline half too under `location: "both"`. `obsidian_replace_in_note` already declared that reason and picks up the re-emit half of the check, so a replacement that strands an alias is refused rather than written.
|
|
25
|
+
- **Tag edits mutate an existing `tags:` sequence node in place** ([#123](https://github.com/cyanheads/obsidian-mcp-server/issues/123)) rather than replacing it with a normalized string set, so non-string entries, comments, and untouched item formatting survive.
|
|
26
|
+
- **A rewritten frontmatter block keeps the note's own line ending** (LF or CRLF) instead of always emitting LF, and deleting the last key drops only the blank separator lines rather than the body's leading indentation ([#125](https://github.com/cyanheads/obsidian-mcp-server/issues/125)).
|
|
27
|
+
- **`obsidian://vault/{+path}` percent-decodes the captured path** ([#132](https://github.com/cyanheads/obsidian-mcp-server/issues/132)) — a note name with a space, non-ASCII character, or literal `%` never resolved before; a malformed escape decodes leniently instead of throwing.
|
|
28
|
+
|
|
29
|
+
## Security
|
|
30
|
+
|
|
31
|
+
- **TLS certificate verification is scoped to each request to an `https:` `OBSIDIAN_BASE_URL`** ([#131](https://github.com/cyanheads/obsidian-mcp-server/issues/131)) instead of `OBSIDIAN_VERIFY_SSL=false` setting `NODE_TLS_REJECT_UNAUTHORIZED=0` process-wide on Bun, which disabled certificate verification for every other HTTPS call the process made (a JWKS fetch, for instance). Node's dispatcher option and Bun's per-request `tls` option are both sent on every request, covering both runtimes with no runtime sniffing.
|
|
32
|
+
|
|
33
|
+
## Dependencies
|
|
34
|
+
|
|
35
|
+
- `@cyanheads/mcp-ts-core` ^0.13.0 → ^0.13.6
|
|
36
|
+
- `zod` ^4.6.1 → ^4.6.5
|
|
37
|
+
- `js-yaml` ^5.4.1 → ^5.4.2
|
|
38
|
+
- `yaml` ^2.9.0 → ^2.9.1
|
|
39
|
+
- `@biomejs/biome` ^2.5.13 → ^2.5.14
|
|
40
|
+
- `@types/node` ^26.5.1 → ^26.6.1
|
|
41
|
+
- `tsc-alias` ^1.9.4 → ^1.9.5
|
|
42
|
+
- `vitest` ^5.0.0 → ^5.0.1
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "TLS certificate rejection and an unreachable Local REST API now surface as typed, actionable errors instead of a hintless InternalError, multi-word text search and inline tag matching are fixed, and a section write to a repeated heading path is rejected instead of landing on the wrong occurrence."
|
|
3
|
+
breaking: false
|
|
4
|
+
security: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 3.5.5 — 2026-09-22
|
|
8
|
+
|
|
9
|
+
## Fixed
|
|
10
|
+
|
|
11
|
+
- **A rejected TLS certificate and an unreachable Local REST API now surface as typed errors** — `certificate_rejected` names the runtime's TLS code and points at `OBSIDIAN_VERIFY_SSL` / `OBSIDIAN_BASE_URL`; `obsidian_unreachable` covers a closed port, wrong host, or disabled plugin and keeps its GET/PUT/DELETE retries. Both used to surface as a hintless `InternalError` ([#133](https://github.com/cyanheads/obsidian-mcp-server/issues/133), [#136](https://github.com/cyanheads/obsidian-mcp-server/issues/136)).
|
|
12
|
+
- **Error `data` no longer carries a route string as `path`** — a command-route error now carries `commandId`, and a route with no caller input (`/`, `/tags/`, `/search/`, …) carries neither ([#130](https://github.com/cyanheads/obsidian-mcp-server/issues/130)).
|
|
13
|
+
- **A multi-word `obsidian_search_notes` text query merges its per-token spans into one match location** instead of returning a near-identical context window per token ([#117](https://github.com/cyanheads/obsidian-mcp-server/issues/117)); offsets are also corrected when the context window's edge splits a surrogate pair, and a span's subject (note body or filename) is read from the plugin's `match.source` when it sends one ([#135](https://github.com/cyanheads/obsidian-mcp-server/issues/135)).
|
|
14
|
+
- **`#tag` detection now follows Obsidian's own grammar** — leading-digit and non-ASCII tags are read and written, HTML comments and math spans are skipped, and the left-boundary and all-digit rules match Obsidian's readback ([#127](https://github.com/cyanheads/obsidian-mcp-server/issues/127), [#138](https://github.com/cyanheads/obsidian-mcp-server/issues/138)).
|
|
15
|
+
- **Inline tag scanning stays linear in note length** — the inline-math check finds each span's closer from a single pass over the note, so a paragraph of `$` signs that never close, such as a table of prices, is not rescanned from each one ([#143](https://github.com/cyanheads/obsidian-mcp-server/issues/143)).
|
|
16
|
+
- **A document-map heading locator now round-trips through `obsidian_get_note` section reads at any depth** ([#128](https://github.com/cyanheads/obsidian-mcp-server/issues/128)), and a section write against a full path that repeats in the note is rejected with `ambiguous_section` instead of silently landing on the wrong occurrence ([#137](https://github.com/cyanheads/obsidian-mcp-server/issues/137)); `SectionSchema.target` and the write-tool descriptions document both cases ([#129](https://github.com/cyanheads/obsidian-mcp-server/issues/129)).
|
|
17
|
+
|
|
18
|
+
## Dependencies
|
|
19
|
+
|
|
20
|
+
- `@types/node` ^26.6.1 → ^26.6.2
|
package/changelog/template.md
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
# Required. One-line GitHub Release-style headline. 350 character cap — a
|
|
8
8
|
# ceiling, not a target. Default short and scannable. Don't pad, don't stitch
|
|
9
|
-
# unrelated changes with commas/semicolons into an inventory — pick the
|
|
10
|
-
# headline
|
|
9
|
+
# unrelated changes with commas/semicolons into an inventory — pick the one
|
|
10
|
+
# headline the release is about. Quotes required: unquoted YAML treats
|
|
11
11
|
# `: ` inside the value as a key separator and fails GitHub's strict parser.
|
|
12
12
|
summary: ""
|
|
13
13
|
|
|
@@ -119,11 +119,11 @@ security: false
|
|
|
119
119
|
TAG ANNOTATIONS — the annotated tag body renders as the GitHub Release body
|
|
120
120
|
via `gh release create --notes-from-tag`. It is a condensed digest of this
|
|
121
121
|
entry, never a copy, and its format is owned by the `release-and-publish`
|
|
122
|
-
skill (step 4, "Create the annotated tag"):
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
122
|
+
skill (step 4, "Create the annotated tag"): a short subject line without the
|
|
123
|
+
version, flat headline bullets — no Keep-a-Changelog section headers, no
|
|
124
|
+
gates line — at most one deps line, issue backlinks, and the changelog link
|
|
125
|
+
last. In release-PR mode the `git-wrapup` skill authors those bullets as the
|
|
126
|
+
PR body's `## Changes` and the tag copies them.
|
|
127
127
|
-->
|
|
128
128
|
|
|
129
129
|
## Added
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { z } from '@cyanheads/mcp-ts-core';
|
|
7
7
|
declare const ServerConfigSchema: z.ZodObject<{
|
|
8
8
|
apiKey: z.ZodString;
|
|
9
|
-
baseUrl: z.ZodDefault<z.ZodString
|
|
9
|
+
baseUrl: z.ZodPipe<z.ZodDefault<z.ZodString>, z.ZodTransform<string, string>>;
|
|
10
10
|
verifySsl: z.ZodDefault<z.ZodUnion<readonly [z.ZodBoolean, z.ZodCodec<z.ZodString, z.ZodBoolean>]>>;
|
|
11
11
|
requestTimeoutMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
12
12
|
enableCommands: z.ZodDefault<z.ZodUnion<readonly [z.ZodBoolean, z.ZodCodec<z.ZodString, z.ZodBoolean>]>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-config.d.ts","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAgF3C,QAAA,MAAM,kBAAkB;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"server-config.d.ts","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAgF3C,QAAA,MAAM,kBAAkB;;;;;;;;;;iBAuDtB,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAI9D,wBAAgB,eAAe,IAAI,YAAY,CAa9C;AAED,iEAAiE;AACjE,wBAAgB,iBAAiB,IAAI,IAAI,CAExC"}
|
|
@@ -91,10 +91,17 @@ const ServerConfigSchema = z.object({
|
|
|
91
91
|
.string()
|
|
92
92
|
.url()
|
|
93
93
|
.default('http://127.0.0.1:27123')
|
|
94
|
-
.describe('Base URL of the Obsidian Local REST API. Defaults to http://127.0.0.1:27123 — enable "Non-encrypted (HTTP) Server" in the plugin settings to match. Use https://127.0.0.1:27124 to hit the always-on HTTPS port (self-signed cert; pair with OBSIDIAN_VERIFY_SSL=false).')
|
|
94
|
+
.describe('Base URL of the Obsidian Local REST API. Defaults to http://127.0.0.1:27123 — enable "Non-encrypted (HTTP) Server" in the plugin settings to match. Use https://127.0.0.1:27124 to hit the always-on HTTPS port (self-signed cert; pair with OBSIDIAN_VERIFY_SSL=false). A trailing slash is stripped — writing one is natural and every request path already starts with `/`, so leaving it would double the separator and 404 every endpoint.')
|
|
95
|
+
/**
|
|
96
|
+
* Normalized here rather than at each `${baseUrl}${path}` join: the schema
|
|
97
|
+
* is the one place that runs before anything reads the value, so a single
|
|
98
|
+
* transform covers `#request`, the HEAD size probe, the capability probe,
|
|
99
|
+
* and `deriveOmnisearchUrl` alike (issue #121).
|
|
100
|
+
*/
|
|
101
|
+
.transform((url) => url.replace(/\/+$/, '')),
|
|
95
102
|
verifySsl: envBoolean
|
|
96
103
|
.default(false)
|
|
97
|
-
.describe("Whether to verify the TLS certificate on the Obsidian endpoint. Defaults to false because the plugin uses a self-signed cert.
|
|
104
|
+
.describe("Whether to verify the TLS certificate on the Obsidian endpoint. Defaults to false because the plugin uses a self-signed cert. The relaxation is scoped to this server's own requests to an `https:` OBSIDIAN_BASE_URL — nothing else the process talks to is affected, and a plain-HTTP base URL is unaffected either way."),
|
|
98
105
|
requestTimeoutMs: z.coerce
|
|
99
106
|
.number()
|
|
100
107
|
.int()
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAE/D;;;;GAIG;AACH,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;AAE1D;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,GAAG,CAAC;KAClB,UAAU,CACT,CAAC,GAAG,EAAE,EAAE;IACN,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO;IAC9C,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;QAAE,OAAO,GAAG,CAAC;IACnC,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,GAAG,CAAC;IACxC,IAAI,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO;IAC9B,MAAM,KAAK,GAAG,GAAG;SACd,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC/B,OAAO,KAAK,CAAC;AACf,CAAC,EACD,CAAC;KACE,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;KACjB,QAAQ,EAAE;KACV,SAAS,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;IACxB,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO;IAChC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,GAAG,CAAC,QAAQ,CAAC;YACX,IAAI,EAAE,QAAQ;YACd,OAAO,EAAE,wDAAwD;SAClE,CAAC,CAAC;QACH,OAAO,CAAC,CAAC,KAAK,CAAC;IACjB,CAAC;IACD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC;QACxB,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YAChD,GAAG,CAAC,QAAQ,CAAC;gBACX,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,8CAA8C,GAAG,GAAG;aAC9D,CAAC,CAAC;YACH,OAAO,CAAC,CAAC,KAAK,CAAC;QACjB,CAAC;QACD,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACpC,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YAC5B,GAAG,CAAC,QAAQ,CAAC;gBACX,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,yCAAyC,GAAG,GAAG;aACzD,CAAC,CAAC;YACH,OAAO,CAAC,CAAC,KAAK,CAAC;QACjB,CAAC;QACD,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAC7E,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QACtC,IAAI,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC;YAAE,SAAS;QACnC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QACrB,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACvB,CAAC;IACD,OAAO,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;AAC1C,CAAC,CAAC,CACL;KACA,QAAQ,CACP,+FAA+F,CAChG,CAAC;AAEJ,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,MAAM,EAAE,CAAC;SACN,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,CAAC;SACN,QAAQ,CACP,sGAAsG,CACvG;IACH,OAAO,EAAE,CAAC;SACP,MAAM,EAAE;SACR,GAAG,EAAE;SACL,OAAO,CAAC,wBAAwB,CAAC;SACjC,QAAQ,CACP,ibAAib,CAClb;QACD;;;;;WAKG;SACF,SAAS,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC9C,SAAS,EAAE,UAAU;SAClB,OAAO,CAAC,KAAK,CAAC;SACd,QAAQ,CACP,4TAA4T,CAC7T;IACH,gBAAgB,EAAE,CAAC,CAAC,MAAM;SACvB,MAAM,EAAE;SACR,GAAG,EAAE;SACL,QAAQ,EAAE;SACV,OAAO,CAAC,MAAM,CAAC;SACf,QAAQ,CAAC,sCAAsC,CAAC;IACnD,cAAc,EAAE,UAAU;SACvB,OAAO,CAAC,KAAK,CAAC;SACd,QAAQ,CACP,yKAAyK,CAC1K;IACH,SAAS,EAAE,WAAW,CAAC,QAAQ,CAC7B,yLAAyL,CAC1L;IACD,UAAU,EAAE,WAAW,CAAC,QAAQ,CAC9B,6JAA6J,CAC9J;IACD,QAAQ,EAAE,UAAU;SACjB,OAAO,CAAC,KAAK,CAAC;SACd,QAAQ,CACP,kLAAkL,CACnL;IACH,aAAa,EAAE,CAAC;SACb,MAAM,EAAE;SACR,GAAG,EAAE;SACL,QAAQ,EAAE;SACV,QAAQ,CACP,yTAAyT,CAC1T;CACJ,CAAC,CAAC;AAIH,IAAI,OAAiC,CAAC;AAEtC,MAAM,UAAU,eAAe;IAC7B,OAAO,KAAK,cAAc,CAAC,kBAAkB,EAAE;QAC7C,MAAM,EAAE,kBAAkB;QAC1B,OAAO,EAAE,mBAAmB;QAC5B,SAAS,EAAE,qBAAqB;QAChC,gBAAgB,EAAE,6BAA6B;QAC/C,cAAc,EAAE,0BAA0B;QAC1C,SAAS,EAAE,qBAAqB;QAChC,UAAU,EAAE,sBAAsB;QAClC,QAAQ,EAAE,oBAAoB;QAC9B,aAAa,EAAE,yBAAyB;KACzC,CAAC,CAAC;IACH,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,iBAAiB;IAC/B,OAAO,GAAG,SAAS,CAAC;AACtB,CAAC"}
|