Skip to main content
Glama
maxkuminov

Obsidian MCP (pgvector + Ollama, self-hosted)

by maxkuminov

edit_note

Edit notes in an Obsidian vault: replace content, append text, find-and-replace, or rewrite a section. Atomic writes and expected_hash guard against concurrent overwrites; dry_run previews changes.

Instructions

Edit an existing note in the Obsidian vault. Requires write permission — a readwrite API key, or an OAuth token carrying the readwrite scope.

See get_vault_guide for Obsidian syntax and any vault-specific conventions (naming, folder placement, frontmatter, tags).

Four mutually exclusive modes (set at most one of append/find/section):

  1. Full replace (default): provide only content. content becomes the note's body; an existing valid line-1 YAML frontmatter block is preserved byte-identically ahead of it. Pass replace_frontmatter=True to overwrite the entire file, frontmatter included.

  2. Append: append=True; content is added at the end (preceded by a single newline).

  3. Find & replace: find=<exact text>; replaced with content. Must match exactly once unless replace_all=True. This mode operates on the raw file, so it is the one mode that can edit frontmatter text in place.

  4. Section: section=<heading>; replaces the whole body under the named ATX heading — see "Section mode: what content replaces" below. Use the path-style form Parent/Child to disambiguate when the same heading appears more than once, or the #N ordinal form ("#7", 1-based document order) — the ordinal is the only form that can address duplicate headings sharing one parent, and it is the selector the outline of a truncated read_note advertises. A bare "#N" always selects by position and is never shadowed by a heading whose text happens to be "#N"; reach such a heading by title with "Parent/#N". A selector resolves to the same section in read_note as in this tool on any write this tool admits — that parity is about resolution, not about admission: see the two section-mode refusals below, where a section that reads fine is deliberately not writable. Setext (====/----) headings are not matched.

Frontmatter and the round trip. Read a note, edit the content field of the response, pass it straight back to full replacement: the frontmatter survives. No property of content's shape changes that — a body whose first line is a thematic break ---, or which itself begins with a complete mapping-shaped fenced block, is body. A note with no valid block (no line-1 fence, or a malformed one) is replaced wholesale by default, which is the repair path and needs no flag. To change the frontmatter itself use set_frontmatter, or edit the raw block through find=; a read_note response's frontmatter JSON view is a lossy convenience and must never be written back.

The round-trip guarantee covers a complete, unwindowed whole-note read only — read_note(path) with no section, offset=0 and truncated false in the response. A truncated read must be paged to the end before it is written back, or full replacement will replace the whole body with the fragment.

Section mode: what content replaces.

  • In section mode content is the section's body: the text beginning on the line immediately after the matched heading line, running to the next heading of equal-or-shallower depth or to end of note. The heading line itself is never removed or rewritten.

  • A section write replaces that body whole. Anything content does not resend is deleted — a blank line, a list, and a fenced code block sitting directly under the heading included. There is no third region between the heading line and the body that survives a write.

  • So a blank line you want between the heading and its content belongs in content (send "\ntext", not "text").

  • read_note(path, section=...) is the matching read: its response carries the heading line in the heading field and the body in the content field, and this tool takes exactly that content. Pass the field through unchanged — there is nothing to split off and nothing to strip.

  • Byte-identity holds for notes whose body newlines are LF. Every non-LF terminator inside the selected body (CRLF, or a lone CR) comes back as LF — the read path normalises and this tool writes raw bytes — whether the note uses one dialect throughout or mixes them. Terminators outside the selected body are untouched, so a round trip can leave a note with more mixed endings than it started with.

Section mode resolves headings over the frontmatter-stripped body, exactly as read_note does, so #N ordinals agree between the two and a YAML # comment inside the block is never selectable. A heading inside a fenced code block is not a heading: fences count with up to three spaces of indentation and a closer at least as long as the opener, and an unclosed column-zero fence hides everything below it.

Two shapes refuse a section write outright, naming the problem and writing nothing:

  • a malformed frontmatter block (unclosed fence, YAML error, non-mapping) — the refusal names the defect and the replace_frontmatter=True repair;

  • a fence opener indented by one to three spaces that nothing below it closes — such an opener may sit inside a list item, whose code block ends where the item does, and this server does not parse container blocks, so it will not guess whether the text below is code or content. Close the fence or unindent it to column zero, then reissue.

Both refusals are asymmetric with reads on purpose: read_note(section=…) and the truncation outline keep working on such notes, because a read destroys nothing.

Flags:

  • operation="append": legacy alias for append=True. This is accepted to prevent older clients from silently falling through to full replacement. operation="replace" explicitly selects full replacement.

  • replace_all=True: with find, replace every occurrence rather than failing on multiple matches. Ignored when find is unset.

  • replace_frontmatter=True: full replacement overwrites the entire file including the frontmatter block. Combined with append/find/section it is an error and nothing is written.

  • dry_run=True: compute the would-be result and return a unified diff without writing. Works for all four modes, and diffs the composed result.

Writes are atomic: the composed result is staged in the note's own directory, flushed to disk, and published with a single same-directory rename, so a crash mid-write cannot truncate the destination. The publish is guarded, but only against a change landing inside this call: the file is read here and re-compared immediately before the rename, so a writer racing this tool's own read-modify-write fails with File changed while editing: <name> and nothing is written. That is not a guard on your read — it is the second of the two windows below, and expected_hash is the first. Structured frontmatter mutation is better done via set_frontmatter — PyYAML serialization there discards YAML comments. A path whose final component is a symlink is refused in every mode (dry_run included), naming the link's target; symlinked folders inside the vault work normally.

expected_hash — binding a write to the bytes you read. Optional; omit it and nothing changes, including today's silent overwrite of whatever landed since your read_note. Pass the content_hash a read returned, verbatim and canonical (sha256:<64 lowercase hex>), and the call is refused with nothing written if the file changed in between. It is always the whole file's hash, never a hash of the text you received. Two windows, both live: expected_hash covers your read → this call's read, the server's own pre-publication compare covers this call's read → its publication, and a match on the first does not exempt the second. Every refusal ends with one machine-readable MCP-REFUSAL {"code":…} line — stale_precondition (with the file's current hash, ready to resend), concurrent_write, no_incumbent, malformed_precondition, precondition_unavailable, precondition_required — and each states what resolves it.

It applies in all four modes, dry_run included, and is checked before mode dispatch, the size cap, the diff and every no-op branch, so a stale base never yields a diff or a "no changes" answer. A section= write binds the whole file as well: #N ordinals are positional, so a body-only digest could certify an unchanged body while an insertion above it changed which section the selector names. That makes the narrowest mode the most conflict-prone — an unrelated edit elsewhere refuses it — which is exactly why the argument is optional: bind when you reasoned about what you read, omit when you are appending to a log. Every mode that publishes reports the content_hash of the bytes this call wrote (not of whatever is on disk when you read the message), so an edit→edit chain needs no intervening read; dry_run publishes nothing and reports none.

Args: path: Vault-relative path to the note. content: New body (full replace), replacement text, text to append, or section body. append: If True, append content to the end of the note. operation: Legacy mode selector; accepts "append" or "replace". find: Exact text to find and replace. section: ATX heading text identifying the section whose body to replace. Use Parent/Child to disambiguate repeated headings, or a "#N" ordinal ("#7", 1-based document order) for duplicate siblings. replace_all: With find, replace every match instead of requiring uniqueness. dry_run: Return a unified diff and do not write. replace_frontmatter: Full-replace only. If True, content replaces the entire file including any frontmatter block. Default False preserves an existing valid block. expected_hash: The note's content_hash as you last read it. Refuses the write, changing nothing, if the file has changed since.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
findNo
pathYes
appendNo
contentYes
dry_runNo
sectionNo
operationNo
replace_allNo
expected_hashNo
replace_frontmatterNo

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
resultYes

Schema Changelog

Changes observed during successful MCP inspections.

  1. Changed1 schema field changedv0.8.2
    • addedInput schema / additionalProperties
      Added value: +false
  2. Changed1 schema field changedv0.8.1
    • addedInput schema / properties / expected_hash
      Added value: +{
      +  "anyOf": [
      +    {
      +      "type": "string"
      +    },
      +    {
      +      "type": "null"
      +    }
      +  ],
      +  "default": null,
      +  "title": "Expected Hash"
      +}
  3. Changed1 schema field changedv0.7.2
    • addedInput schema / properties / replace_frontmatter
      Added value: +{
      +  "default": false,
      +  "title": "Replace Frontmatter",
      +  "type": "boolean"
      +}
  4. Addedv0.7.0
  5. Removedv0.5.4
  6. First observed

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description fully bears the burden of behavioral disclosure, and it excels. It reveals atomic write semantics, the concurrency guard ('File changed while editing'), the expected_hash binding, refusal codes, section-mode deletion behavior, line-ending normalization, symlink refusal, and the round-trip guarantee. Nothing about side effects or failure modes is left undisclosed.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is lengthy, but for a tool with 10 parameters and four interacting modes, the detail is justified. It is well-structured with headers, numbered lists, and bolded key terms, and the most critical purpose and mode overview are front-loaded. Some redundancy (e.g., repeated references to section parity) could be trimmed, but overall the structure makes navigation effective.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The description covers every conceivable operational aspect: all four modes, edge cases (fenced code blocks, malformed frontmatter), concurrency, refusals, round-trip guarantees, line endings, and even recommendations for sibling tools. Given the tool's complexity, nothing an agent needs to call it correctly is missing. An output schema exists, and the description focuses on behavior rather than return values.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, but the description compensates with a dedicated 'Args:' section that explains every parameter's meaning, defaults, and interactions (e.g., 'replace_frontmatter: Full-replace only... Default False preserves an existing valid block'). It also clarifies subtle semantics like the canonical expected_hash format and the meaning of '#'N' ordinals. This goes well beyond the bare schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a clear verb+resource: 'Edit an existing note in the Obsidian vault.' It then enumerates four distinct modes with precise conditions, and explicitly differentiates from siblings like set_frontmatter ('To change the frontmatter itself use set_frontmatter'). An agent can immediately tell what this tool does and how it relates to neighboring tools.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage guidance is explicit and exhaustive. The description specifies when to use each mode, when not to (e.g., 'Structured frontmatter mutation is better done via set_frontmatter'), and directs users to get_vault_guide for syntax conventions. It also covers prerequisites like write permission. No ambiguity about selection between alternatives.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.