Add frontmatter tools, append, delete + pyyaml dep
This commit is contained in:
parent
39fae1501b
commit
b27728d75a
2 changed files with 146 additions and 1 deletions
|
|
@ -1 +1,2 @@
|
||||||
fastmcp>=2.0.0
|
fastmcp>=2.0.0
|
||||||
|
pyyaml>=6.0
|
||||||
144
server.py
144
server.py
|
|
@ -15,6 +15,7 @@ from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
import yaml
|
||||||
from fastmcp import FastMCP
|
from fastmcp import FastMCP
|
||||||
|
|
||||||
# ── Config ────────────────────────────────────────────────────────────────
|
# ── Config ────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -599,6 +600,149 @@ def move_document(vault_name: str, src_path: str, dest_path: str) -> dict[str, A
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def read_frontmatter(vault_name: str, doc_path: str, key: str | None = None) -> dict[str, Any]:
|
||||||
|
"""Read YAML frontmatter from a markdown document.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
vault_name: Name of the vault.
|
||||||
|
doc_path: Relative path to the document.
|
||||||
|
key: Optional specific key to read. If None, returns all frontmatter.
|
||||||
|
|
||||||
|
Returns the frontmatter dict or specific key value.
|
||||||
|
"""
|
||||||
|
target = _resolve_doc(vault_name, doc_path)
|
||||||
|
if not target.exists():
|
||||||
|
return {"error": f"Document not found: {doc_path}", "vault": vault_name}
|
||||||
|
|
||||||
|
content = target.read_text(encoding="utf-8", errors="replace")
|
||||||
|
fm = _parse_frontmatter(content)
|
||||||
|
if fm is None:
|
||||||
|
return {"vault": vault_name, "path": doc_path, "frontmatter": {}, "note": "No frontmatter found"}
|
||||||
|
|
||||||
|
if key:
|
||||||
|
return {"vault": vault_name, "path": doc_path, "key": key, "value": fm.get(key)}
|
||||||
|
return {"vault": vault_name, "path": doc_path, "frontmatter": fm}
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def write_frontmatter(vault_name: str, doc_path: str, key: str, value: Any = None) -> dict[str, Any]:
|
||||||
|
"""Set a YAML frontmatter key on a markdown document. Creates frontmatter if absent.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
vault_name: Name of the vault.
|
||||||
|
doc_path: Relative path to the document.
|
||||||
|
key: Frontmatter key to set.
|
||||||
|
value: Value to set (string, number, boolean, list, or dict). Pass null/None to delete.
|
||||||
|
|
||||||
|
Returns the updated frontmatter state.
|
||||||
|
"""
|
||||||
|
target = _resolve_doc(vault_name, doc_path)
|
||||||
|
if not target.exists():
|
||||||
|
return {"error": f"Document not found: {doc_path}", "vault": vault_name}
|
||||||
|
|
||||||
|
content = target.read_text(encoding="utf-8", errors="replace")
|
||||||
|
fm = _parse_frontmatter(content)
|
||||||
|
|
||||||
|
if fm is None:
|
||||||
|
# No frontmatter — create one
|
||||||
|
if value is None:
|
||||||
|
return {"vault": vault_name, "path": doc_path, "action": "noop", "note": "No frontmatter to delete from"}
|
||||||
|
new_fm = {key: value}
|
||||||
|
new_content = "---\n" + yaml.dump(new_fm, default_flow_style=False, allow_unicode=True).strip() + "\n---\n\n" + content
|
||||||
|
target.write_text(new_content, encoding="utf-8")
|
||||||
|
return {"vault": vault_name, "path": doc_path, "key": key, "value": value, "action": "created"}
|
||||||
|
|
||||||
|
# Existing frontmatter
|
||||||
|
if value is None:
|
||||||
|
fm.pop(key, None)
|
||||||
|
action = "deleted"
|
||||||
|
else:
|
||||||
|
fm[key] = value
|
||||||
|
action = "set"
|
||||||
|
|
||||||
|
new_fm_block = "---\n" + yaml.dump(fm, default_flow_style=False, allow_unicode=True).strip() + "\n---"
|
||||||
|
body = _body_after_frontmatter(content)
|
||||||
|
target.write_text(new_fm_block + "\n\n" + body, encoding="utf-8")
|
||||||
|
|
||||||
|
return {"vault": vault_name, "path": doc_path, "key": key, "value": value, "action": action}
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def append_to_document(vault_name: str, doc_path: str, content: str) -> dict[str, Any]:
|
||||||
|
"""Append content to the end of a markdown document. Creates the file if it doesn't exist.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
vault_name: Name of the vault.
|
||||||
|
doc_path: Relative path to the document.
|
||||||
|
content: Markdown content to append.
|
||||||
|
|
||||||
|
Returns the updated document info.
|
||||||
|
"""
|
||||||
|
if not doc_path.lower().endswith(".md"):
|
||||||
|
doc_path = doc_path.rstrip("/\\") + ".md"
|
||||||
|
|
||||||
|
target = _resolve_doc(vault_name, doc_path)
|
||||||
|
|
||||||
|
if not target.exists():
|
||||||
|
target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
target.write_text(content + "\n", encoding="utf-8")
|
||||||
|
action = "created"
|
||||||
|
else:
|
||||||
|
existing = target.read_text(encoding="utf-8", errors="replace")
|
||||||
|
separator = "\n" if existing.endswith("\n") else "\n\n"
|
||||||
|
target.write_text(existing + separator + content + "\n", encoding="utf-8")
|
||||||
|
action = "appended"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"vault": vault_name,
|
||||||
|
"path": doc_path.lstrip("/\\").replace("\\", "/"),
|
||||||
|
"action": action,
|
||||||
|
"size_bytes": target.stat().st_size,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def delete_document(vault_name: str, doc_path: str) -> dict[str, Any]:
|
||||||
|
"""Delete a document from a vault.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
vault_name: Name of the vault.
|
||||||
|
doc_path: Relative path to the document.
|
||||||
|
|
||||||
|
Returns deletion confirmation.
|
||||||
|
"""
|
||||||
|
target = _resolve_doc(vault_name, doc_path)
|
||||||
|
if not target.exists():
|
||||||
|
return {"error": f"Document not found: {doc_path}", "vault": vault_name}
|
||||||
|
|
||||||
|
target.unlink()
|
||||||
|
return {"vault": vault_name, "path": doc_path.lstrip("/\\").replace("\\", "/"), "action": "deleted"}
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_frontmatter(content: str) -> dict[str, Any] | None:
|
||||||
|
"""Parse YAML frontmatter from markdown content. Returns None if not found."""
|
||||||
|
if not content.startswith("---"):
|
||||||
|
return None
|
||||||
|
parts = content.split("---", 2)
|
||||||
|
if len(parts) < 3:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return yaml.safe_load(parts[1]) or {}
|
||||||
|
except yaml.YAMLError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _body_after_frontmatter(content: str) -> str:
|
||||||
|
"""Return the body content after YAML frontmatter."""
|
||||||
|
if not content.startswith("---"):
|
||||||
|
return content
|
||||||
|
parts = content.split("---", 2)
|
||||||
|
if len(parts) < 3:
|
||||||
|
return content
|
||||||
|
return parts[2].lstrip("\n")
|
||||||
|
|
||||||
|
|
||||||
# ── Entrypoint ────────────────────────────────────────────────────────────
|
# ── Entrypoint ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue