Skip to main content
Glama
maxkuminov

Obsidian MCP (pgvector + Ollama, self-hosted)

by maxkuminov

set_frontmatter

Modify YAML frontmatter keys in an Obsidian note while leaving the body intact; add or update fields, remove keys, and use an optional content hash to guard against overwriting concurrent changes.

Instructions

Mutate a note's YAML frontmatter without touching its body. Requires write permission — a readwrite API key, or an OAuth token carrying the readwrite scope.

Parses the existing frontmatter, merges in updates (overwriting matching keys, adding any new ones), then drops keys listed in remove. The note body is preserved byte-for-byte. If the note has no frontmatter (no --- fence on line 1), a fresh block is prepended ahead of the unchanged body.

A malformed block is refused, never worked around. An unclosed line-1 fence, YAML that fails to parse, and YAML that is not a mapping (null, ~, comments only, a list, a scalar) each return an error naming the defect and pointing at edit_note(path, content, replace_frontmatter=True) as the repair. Nothing is written — in particular no second block is prepended above the broken one — and remove= refuses identically rather than silently doing nothing. This is reported even for a call with no updates and no remove. An empty fenced block (--- immediately followed by ---) is valid: it is a valid empty mapping and is updated in place.

Only an effective change writes. updates that set every named key to the value it already holds (compared type-sensitively, so true is not 1) together with remove naming only absent keys report no changes and leave the file byte-identical. Removing the last key removes the block entirely — no fences, no separator, exactly the prior body.

Re-serialization uses yaml.safe_dump(default_flow_style=False, sort_keys=False, allow_unicode=True). Caveat: PyYAML does NOT preserve YAML comments — any # comment in the original frontmatter will be lost on the first set_frontmatter call.

A path whose final component is a symlink is refused, naming its target, so the frontmatter of an unnamed note is never rewritten; symlinked folders inside the vault work normally.

See get_vault_guide for vault frontmatter conventions.

expected_hash — binding a write to the bytes you read. Optional; omit it and nothing changes. 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.

The comparison runs ahead of the malformed-block diagnosis and ahead of the no-op check, so a stale base never yields "no changes" or a defect report about bytes you have not seen. A write reports the content_hash of the bytes this call published; a no-op publishes nothing and reports none.

Args: path: Vault-relative path to the note. updates: Mapping of keys to set. Use the empty dict (or omit) to skip. remove: List of keys to delete from the frontmatter. Missing keys are ignored (and, on their own, make the call a no-op rather than a write). expected_hash: The note's content_hash as you last read it. Refuses the write, changing nothing, if the note has changed since.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
pathYes
removeNo
updatesNo
expected_hashNo

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. Addedv0.7.0
  4. Removedv0.5.4
  5. First observed

TDQS

A4.8/5.0
Behavior5/5

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

With no annotations present, the description carries the full burden, and it does so thoroughly: write-permission requirements, byte-for-byte body preservation, malformed-block refusal, no-op behavior, block removal, PyYAML comment loss, symlink refusal, expected_hash semantics, refusal codes, and check ordering are all disclosed. This far exceeds baseline and leaves little hidden behavior for an agent to discover at runtime.

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

Conciseness5/5

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

The description is long, but every block earns its place: permission, mutation semantics, malformed-input behavior, no-op rules, serialization caveat, symlink edge case, and expected_hash contract. Bold section headers and a final Args list make the density navigable without burying the core action.

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?

For a mutating tool with preconditions and subtle edge cases, the description covers all required context: when a write happens, when it is refused, what error codes look like, what is serialized back, and what the call returns (content_hash on write, none on no-op). The output schema exists, so not restating the full response shape is acceptable.

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%, so the description must compensate, and it does. Each parameter gets an Args entry that goes beyond type: path is vault-relative, updates is a mapping with empty-dict/omit semantics, remove is a list with missing-key behavior, and expected_hash is explained with canonical format, staleness refusal, and coverage over the whole file. This is more than the schema alone provides.

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 specific verb and resource: 'Mutate a note's YAML frontmatter without touching its body.' This precisely distinguishes it from body-editing tools like edit_note and write_file. The scope (frontmatter only) is unambiguous.

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

Usage Guidelines4/5

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

The description clearly establishes when set_frontmatter is appropriate (frontmatter-only mutation) and references edit_note(path, content, replace_frontmatter=True) as the repair path for malformed blocks. It does not enumerate every sibling alternative, but the context and the explicit edit_note alternative give solid usage direction.

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