obsidian-mcp-server
Version:
MCP server for Obsidian vaults — read, write, search, and surgically edit notes, tags, and frontmatter via the Local REST API plugin. STDIO or Streamable HTTP.
32 lines (21 loc) • 4.17 kB
Markdown
---
summary: "Typed error contracts catch up to wire reality on `obsidian_get_note`, `obsidian_patch_note`, and `obsidian_append_to_note`; `obsidian_manage_tags` default `location` flips from `both` to `frontmatter`; `obsidian_search_notes` drops the opaque text-mode `score` field."
breaking: true
security: false
---
# 3.2.1 — 2026-05-21
A pass over agent-facing error reasons and a default-safety nudge on `obsidian_manage_tags`. Three typed reasons now flow through the contract that previously surfaced as generic `NotFound` / `ValidationError`. The default tag write target is now the frontmatter `tags:` array — opt into body mutation with `location: 'inline'` or `'both'`.
## Changed
- **`obsidian_manage_tags` default `location` is now `'frontmatter'`, not `'both'`** ([#64](https://github.com/cyanheads/obsidian-mcp-server/issues/64)) — frontmatter is the canonical Obsidian tag location and leaves the body untouched. Callers wanting inline `#tag` syntax in the body or reconciliation across both surfaces opt in explicitly with `location: 'inline'` or `'both'`. The tool description and README are updated to coach `both` as opt-in reconciliation.
- **`obsidian_search_notes` `query` / `logic` descriptions name the right field for the wrong mode** ([#63](https://github.com/cyanheads/obsidian-mcp-server/issues/63)) — the `.describe()` text now states which mode the field belongs to and points at the sibling field for the others, so an LLM reading the tool catalog can self-correct before a generic Zod error fires.
- **`obsidian_open_in_ui` `note_missing` recovery hint leads with verification** ([#62](https://github.com/cyanheads/obsidian-mcp-server/issues/62)) — was "Pass `failIfMissing: false` to create on open." (the only suggestion), now "Verify the path with `obsidian_list_notes` or `obsidian_search_notes` first — a typo would otherwise materialize as an empty file. If creation is intended, retry with `failIfMissing: false`."
## Removed
- **`obsidian_search_notes` text-mode `score` field** ([#65](https://github.com/cyanheads/obsidian-mcp-server/issues/65)) — text mode doesn't rank results, but the upstream Local REST API returns a constant (`-0.1665` across every hit). The field carried no signal and misled consumers; it's stripped from `TextSearchHit`, the tool's `output` schema, and the `format()` markdown. Omnisearch-mode `score` (real BM25, "higher is more relevant") is unaffected.
## Fixed
- **`obsidian_get_note` section-miss errors now carry a typed `section_missing` reason** ([#61](https://github.com/cyanheads/obsidian-mcp-server/issues/61)) — `format: "section"` with a non-existent heading, block reference, or frontmatter field previously rethrew the upstream `NotFound` with `data.reason: null` and `data.recovery: null`. A new `section_missing` entry in `obsidian-get-note.tool.ts`'s `errors[]` contract and a module-scoped `reclassifyAsSectionMiss` helper route the `extractSection` throw through `ctx.recoveryFor`, so the response now points at `format: "document-map"` and explains nested-heading `Parent::Child` syntax. The reclassifier lives outside the handler so `JsonRpcErrorCode.NotFound` doesn't trigger the `error-contract-prefer-fail` lint.
- **`obsidian_patch_note` / `obsidian_append_to_note` content-preexists upstream response no longer misclassifies as `section_target_missing`** ([#60](https://github.com/cyanheads/obsidian-mcp-server/issues/60)) — the Local REST API returns HTTP 400 with `content-already-preexists-in-target` for retries that hit `applyIfContentPreexists: false` (the idempotent default). The 400 branch in `ObsidianService.#throwForStatus` now checks for this case first — before the broader "could not be applied" match — so retries with identical content surface `content_preexists` (ValidationError) with recovery pointing at `patchOptions.applyIfContentPreexists: true`. Both tools' `errors[]` contracts gain the new reason so the recovery hint flows through `ctx.recoveryFor`.
## Dependencies
| Package | Change |
|:---|:---|
| `@types/node` | `^25.8.0` → `^25.9.1` (devDependency) |
| `vitest` | `^4.1.6` → `^4.1.7` (devDependency) |