set_frontmatter
Update, add, or remove YAML frontmatter keys in a note while preserving the body content exactly as written.
Instructions
Mutate a note's YAML frontmatter without touching its body. Requires a readwrite API key.
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.
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.
See get_vault_guide for vault frontmatter conventions.
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 silently ignored.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | ||
| updates | No | ||
| remove | No |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| result | Yes |
Implementation Reference
- src/mcp_server/tools.py:1009-1072 (handler)The actual implementation of set_frontmatter. Parses the note, merges updates, removes keys, serializes frontmatter back, and writes the file atomically.
@_tracked("set_frontmatter", ["path"]) async def set_frontmatter_impl( path: str, updates: dict | None = None, remove: list[str] | None = None, ) -> str: """Merge `updates` into a note's YAML frontmatter and drop keys in `remove`.""" if err := _require_write(): return err updates = dict(updates or {}) remove = list(remove or []) from src.services.vault import ( parse_frontmatter, serialize_frontmatter, validate_path, ) uid = current_user_id.get() try: full_path = validate_path(path, user_id=uid) except ValueError as e: return str(e) if not full_path.is_file(): return f"Note not found: {path}" if not updates and not remove: return f"No changes for {path} (empty updates and remove)" try: raw = full_path.read_text(encoding="utf-8") except Exception as e: return f"Failed to read {path}: {e}" fm, body = parse_frontmatter(raw) set_keys: list[str] = [] for k, v in updates.items(): fm[k] = v set_keys.append(k) removed_keys: list[str] = [] for k in remove: if k in fm: del fm[k] removed_keys.append(k) new_raw = serialize_frontmatter(fm, body) if new_raw == raw: return f"No changes for {path}" try: write_file(path, new_raw, user_id=uid) except ValueError as e: return str(e) summary: list[str] = [] if set_keys: summary.append(f"set: {', '.join(set_keys)}") if removed_keys: summary.append(f"removed: {', '.join(removed_keys)}") if not summary: summary.append("no key changes (whitespace-only)") return f"Updated frontmatter in {path} ({'; '.join(summary)})" - src/mcp_server/server.py:373-400 (registration)The @mcp.tool() decorator registration for set_frontmatter. Defines the public-facing MCP tool with its docstring and args, then delegates to set_frontmatter_impl.
@mcp.tool() async def set_frontmatter( path: str, updates: dict | None = None, remove: list[str] | None = None, ) -> str: """Mutate a note's YAML frontmatter without touching its body. Requires a readwrite API key. 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. 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. See `get_vault_guide` for vault frontmatter conventions. 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 silently ignored. """ return await set_frontmatter_impl(path, updates=updates, remove=remove) - src/mcp_server/server.py:1-23 (registration)Import of set_frontmatter_impl from tools module into the server where it's registered as an MCP tool.
from mcp.server.fastmcp import FastMCP from mcp.server.transport_security import TransportSecuritySettings from src.config import settings from src.mcp_server.tools import ( create_note_impl, delete_note_impl, edit_note_impl, find_orphans_impl, find_related_impl, get_backlinks_impl, get_links_impl, get_neighborhood_impl, get_recent_impl, get_tags_impl, get_vault_guide_impl, list_notes_impl, move_note_impl, read_note_impl, search_notes_impl, semantic_search_impl, set_frontmatter_impl, ) - src/services/vault.py:160-198 (helper)parse_frontmatter: Splits a raw note into YAML frontmatter dict and body string using Obsidian's --- fence convention.
def parse_frontmatter(raw: str) -> tuple[dict, str]: """Split YAML frontmatter from content. The fence (`---`) MUST be on line 1 (Obsidian's rule). Anything else is treated as no frontmatter, even if a `---` fence appears further down. Returns `(metadata, body)`. `body` preserves leading whitespace exactly as it appears after the closing `---\n`; only a single newline separator is consumed. """ if not raw.startswith("---"): return {}, raw # Require the opening fence to occupy line 1 alone (allow trailing CR). first_line_end = raw.find("\n") if first_line_end == -1: return {}, raw first_line = raw[:first_line_end].rstrip("\r") if first_line != "---": return {}, raw # Find the closing fence on its own line. rest = raw[first_line_end + 1:] closing_re = re.compile(r"(?m)^---[ \t]*\r?$") m = closing_re.search(rest) if m is None: return {}, raw yaml_text = rest[:m.start()] body_start = m.end() # Skip the single newline after the closing fence, if present. if body_start < len(rest) and rest[body_start] == "\n": body_start += 1 body = rest[body_start:] try: fm = yaml.safe_load(yaml_text) except yaml.YAMLError: return {}, raw if not isinstance(fm, dict): return {}, raw return fm, body - src/services/vault.py:201-216 (helper)serialize_frontmatter: Reassembles a frontmatter dict + body into a complete note string using PyYAML safe_dump.
def serialize_frontmatter(meta: dict, body: str) -> str: """Re-assemble a note from a frontmatter dict and a body string. Empty / missing `meta` → returns `body` unchanged (no fence is emitted). Otherwise emits `---\\n<yaml>---\\n<body>`. PyYAML `safe_dump` does NOT preserve YAML comments — callers should document this caveat. """ if not meta: return body yaml_text = yaml.safe_dump( meta, default_flow_style=False, sort_keys=False, allow_unicode=True, ) return f"---\n{yaml_text}---\n{body}"