Imported from MichelKerkmeester/skilled-agent-harness_spec-driven-loops (
.opencode/skills/mcp-tooling/mcp-obsidian/SKILL.md). Install upstream withnpx skills add MichelKerkmeester/skilled-agent-harness_spec-driven-loops --skill mcp-obsidian. Copyright stays with the author.
name: mcp-obsidian description: Makes AI use inside Obsidian effective: vault and note operations across the headless notesmd-cli, the app-backed official obsidian CLI, and the cyanheads MCP, plus deep plugin and theme knowledge (Tables, BRAT, Health.md, Iconic, Charts, Dataview, Git, Outliner, Minimal, Notion Bases, Make.md, Meta Bind) operated at the file layer. Embedded install and agent safety invariants. allowed-tools: [Bash, Edit, Glob, Grep, mcp__code_mode__call_tool_chain, Read, Write] version: 0.23.0.0
mcp-obsidian Skill
The skill that makes AI use inside Obsidian effective. It operates notes and vaults through three surfaces (headless notesmd-cli, the app-backed official obsidian CLI, and the cyanheads MCP) and knows the twelve plugin and theme file formats at the file layer, so an agent can read, write, search, and extend what the vault contains without guessing.
1. WHEN TO USE
Activation Triggers (explicit user phrases)
- "obsidian", "obsidian vault", "notesmd-cli", "obsidian-mcp", "mcp-obsidian"
- "note", "notes", "markdown note", "daily note", "vault"
- "create a note", "open a note", "search notes", "search my vault"
- "add a tag to a note", "manage frontmatter", "delete a note", "move a note"
- "obsidian plugin", "make.md", "make-md", "obsidian tables", "brat"
- "health", "health data", "apple health", "health chart", "health-md", "health.md"
- "iconic", "icons", "icon color", "file icons", "folder icons", "icon rules", "iconic rulebook", "iconic ruleset", "iconic data json", "icon automation"
- "local rest api", "obsidian api key"
Automatic Triggers (keyword patterns)
notesmd-cliorobsidian(the CLI binary) appears in the requestobsidian+ any action verb (create, open, search, list, print, move, delete, tag)- "vault" in a note-management context
- "daily note" or "note frontmatter" in a personal-knowledge context
- MCP tool names:
obsidian_get_note,obsidian_write_note,obsidian_search_notes
When NOT to Use
- StevenStavrakis
obsidian-mcp— a different (filesystem-based) MCP server; this skill's MCP path uses the cyanheadsobsidian-mcp-server. The Stavrakis server is documented only as the headless MCP alternative. - Generic markdown authoring with no Obsidian vault — use
@markdown/sk-doc. - Non-Obsidian note apps (Notion, Bear, Apple Notes) — wrong surface.
- Editing arbitrary repo files that merely happen to be markdown — use
Read/Editdirectly.
2. SMART ROUTING
Resource Loading Levels
ALWAYS: SKILL.md (this file)
ON_DEMAND: references/obsidian-cli-commands.md (notesmd-cli + official obsidian CLI command details)
references/official-cli-agent-usage.md (official obsidian CLI: preflight, exit-0 contract, key=value syntax, 106-command surface)
references/mcp-tools.md (cyanheads MCP tool catalog + invocation)
references/troubleshooting.md (error, auth, REST API, or install issue detected)
references/notion-migration.md (Notion→Obsidian migration reconstruction method)
Plugin operation logic:
references/plugins/plugin-operation-logic.md (plugin-driven note automation)
Installed plugins roster:
references/plugins/installed-plugins.md (all 21 enabled vault plugins; which carry dedicated docs)
Obsidian Tables:
references/plugins/obsidian-tables/obsidian-tables.md (plugin index)
references/plugins/obsidian-tables/data-model.md
references/plugins/obsidian-tables/workflows.md
references/plugins/obsidian-tables/troubleshooting.md
Obsidian42 BRAT:
references/plugins/obsidian42-brat/obsidian42-brat.md (plugin index)
references/plugins/obsidian42-brat/data-model.md
references/plugins/obsidian42-brat/workflows.md
references/plugins/obsidian42-brat/troubleshooting.md
Health.md Visualizations:
references/plugins/health-md/health-md.md (plugin index)
references/plugins/health-md/data-model.md
references/plugins/health-md/workflows.md
references/plugins/health-md/troubleshooting.md
Iconic:
references/plugins/iconic/iconic.md (plugin index)
references/plugins/iconic/data-model.md
references/plugins/iconic/workflows.md
references/plugins/iconic/troubleshooting.md
Charts:
references/plugins/charts/charts.md (plugin index)
references/plugins/charts/data-model.md
references/plugins/charts/workflows.md
references/plugins/charts/troubleshooting.md
Dataview:
references/plugins/dataview/dataview.md (plugin index)
references/plugins/dataview/data-model.md
references/plugins/dataview/workflows.md
references/plugins/dataview/troubleshooting.md
Obsidian Git:
references/plugins/git/git.md (plugin index)
references/plugins/git/data-model.md
references/plugins/git/workflows.md
references/plugins/git/troubleshooting.md
Outliner:
references/plugins/outliner/outliner.md (plugin index)
references/plugins/outliner/data-model.md
references/plugins/outliner/workflows.md
references/plugins/outliner/troubleshooting.md
Themes (theme system):
references/themes/themes.md (theme system index)
references/themes/customization.md
references/themes/theme-development.md
references/themes/plugin-development.md
Notion Bases:
references/plugins/notion-bases/notion-bases.md (plugin index)
references/plugins/notion-bases/data-model.md
references/plugins/notion-bases/workflows.md
references/plugins/notion-bases/troubleshooting.md
Make.md:
references/plugins/make-md/make-md.md (plugin index)
references/plugins/make-md/data-model.md
references/plugins/make-md/workflows.md
references/plugins/make-md/troubleshooting.md
Local REST API:
references/plugins/obsidian-local-rest-api/obsidian-local-rest-api.md (plugin index)
references/plugins/obsidian-local-rest-api/data-model.md
references/plugins/obsidian-local-rest-api/workflows.md
references/plugins/obsidian-local-rest-api/troubleshooting.md
Advanced Canvas:
references/plugins/advanced-canvas/advanced-canvas.md (plugin index)
references/plugins/advanced-canvas/data-model.md
references/plugins/advanced-canvas/workflows.md
references/plugins/advanced-canvas/troubleshooting.md
Claudian:
references/plugins/claudian/claudian.md (plugin index)
references/plugins/claudian/data-model.md
references/plugins/claudian/workflows.md
references/plugins/claudian/troubleshooting.md
Meta Bind:
references/plugins/meta-bind/meta-bind.md (plugin index)
references/plugins/meta-bind/data-model.md
references/plugins/meta-bind/workflows.md
references/plugins/meta-bind/troubleshooting.md
Two Decisions This Router Makes
- CLI vs MCP — filesystem/terminal work goes to a CLI; live-app note reads/writes/tags/search go to the MCP.
- Which CLI profile — headless
notesmd-cli(default, no running app) vs the app-backed officialobsidianCLI (a remote control for an already-running desktop app; it does not start one, and it exits 0 even on failure). Preflight and result-handling contract:references/official-cli-agent-usage.md.
Execution Profile Selection
| Surface | Binary / transport | Needs a running app? | Best for |
|---|---|---|---|
| Headless CLI | notesmd-cli (Bash) |
No — operates on the vault filesystem | Daily note ops anywhere: open, daily, search, create, list, print, move, delete, frontmatter, vault management |
| App-backed CLI | obsidian (Bash) |
Yes — the app must ALREADY be running; the CLI does not launch it | App-only outcomes: resolved link graph (backlinks/orphans/unresolved), computed tags and tasks, Bases queries, sync and file history, plugin/theme state, in-app opens |
| MCP | obsidian.obsidian_* via Code Mode |
Yes — plus Local REST API + OBSIDIAN_API_KEY |
Structured note reads/writes, tag management, semantic/global search over a live vault |
def resolve_execution_profile(request, runtime):
"""Pick the execution surface for an Obsidian operation.
Best-effort runtime probes — never fabricated:
runtime.app_running -> an Obsidian desktop app is live
(probe: `obsidian version` prints a version number)
runtime.rest_api_up -> Local REST API plugin reachable on OBSIDIAN_BASE_URL
runtime.api_key -> OBSIDIAN_API_KEY present in the Code Mode environment
"""
# MCP path needs a live app + Local REST API + token. It is the only surface
# for structured note reads/writes, tag management and global/semantic search.
if wants_structured_note_ops(request) and runtime.app_running \
and runtime.rest_api_up and runtime.api_key:
return "MCP" # cyanheads obsidian-mcp-server via Code Mode
# The official obsidian CLI is a remote control for an ALREADY-RUNNING app: it
# does not launch one. Probe with `obsidian version` (exit 0 == registered AND
# app up), then use it for app-only outcomes the filesystem cannot answer.
# Its in-app failures exit 0, so callers must read stdout, not the status.
if runtime.app_running and needs_app_command(request):
return "OBSIDIAN_CLI" # official `obsidian` CLI
# Default: headless, filesystem-only, works with no running app or token.
return "NOTESMD_CLI" # Yakitrak notesmd-cli
Operation-to-Tool Routing Table
| Operation | Primary Surface | Command / Tool | Alternative |
|---|---|---|---|
| Open a note | notesmd-cli | notesmd-cli open "<name>" |
obsidian CLI (in live app) |
| Open/append daily note | notesmd-cli | notesmd-cli daily |
MCP obsidian_write_note |
| Search by note name | notesmd-cli | notesmd-cli list + filter (search title-lookup is broken in v0.3.6) |
MCP obsidian_search_notes |
| Search note contents | notesmd-cli | notesmd-cli search-content "<query>" |
MCP obsidian_search_notes |
| List notes | notesmd-cli | notesmd-cli list |
MCP obsidian_search_notes |
| Print a note to stdout | notesmd-cli | notesmd-cli print "<name>" |
MCP obsidian_get_note |
| Create a note | notesmd-cli | notesmd-cli create "<name>" |
MCP obsidian_write_note |
| Move / rename a note | notesmd-cli | notesmd-cli move "<a>" "<b>" |
n/a |
| Delete a note | notesmd-cli | notesmd-cli delete "<name>" |
MCP obsidian_delete_note |
| Edit frontmatter | notesmd-cli | notesmd-cli frontmatter "<name>" |
MCP obsidian_manage_tags |
| Vault registration | notesmd-cli | notesmd-cli add-vault / set-default-vault |
n/a |
| Structured note read | MCP | obsidian_get_note |
notesmd-cli print |
| Structured note write | MCP | obsidian_write_note |
notesmd-cli create |
| Tag management | MCP | obsidian_manage_tags |
notesmd-cli frontmatter |
| Global / semantic search | MCP | obsidian_search_notes |
notesmd-cli search-content |
| In-app open | obsidian CLI | obsidian open file="<name>" |
n/a |
| Resolved link graph | obsidian CLI | obsidian backlinks file="<name>", orphans, unresolved |
n/a |
| Vault tags / tasks index | obsidian CLI | obsidian tags, obsidian tasks |
notesmd-cli search-content (text only) |
| Bases query | obsidian CLI | obsidian base:query file="<base>" format=json |
n/a |
| Sync / file history | obsidian CLI | obsidian sync:status, obsidian history file="<name>" |
n/a |
| Plugin / theme state | obsidian CLI | obsidian plugin:enable id=<id>, obsidian theme:set name="<t>" |
n/a |
| Plugin-driven automation | reference | references/plugins/* |
n/a |
Smart Router Pseudocode
from pathlib import Path
SKILL_ROOT = Path(__file__).resolve().parent
RESOURCE_BASES = (SKILL_ROOT / "references",)
DEFAULT_RESOURCE = "references/obsidian-cli-commands.md"
# Fallback-only: DEFAULT_RESOURCE is a defer-time suggestion, never unioned
# into a route's loaded set. Scored routes load exactly RESOURCE_MAP[intent];
# zero-score routes load nothing and ask for disambiguation instead.
DEFAULT_RESOURCE_SEMANTICS = "fallback-only"
UNKNOWN_FALLBACK_CHECKLIST = [
"Confirm whether the request is for headless notesmd-cli, the app-backed obsidian CLI, the cyanheads MCP, a plugin, install/setup, or troubleshooting",
"Provide the note name, vault path, command, error text, or target plugin",
"Confirm whether an Obsidian desktop app is running and whether Local REST API + OBSIDIAN_API_KEY are configured",
"Confirm the verification command before completing any write action",
]
INTENT_SIGNALS = {
"NOTES_CLI": {
"weight": 5,
"keywords": ["note", "notes", "open", "daily", "daily note", "search",
"search-content", "search content", "list", "print", "create",
"move", "rename", "delete", "frontmatter", "vault", "add-vault",
"remove-vault", "list-vaults", "set-default-vault", "default vault",
"new note", "find note", "jot", "capture", "markdown note"],
},
"OFFICIAL_CLI": {
"weight": 6,
"keywords": ["official cli", "official obsidian cli", "obsidian cli",
"app-backed", "app backed", "register cli", "live app",
"running app", "command line interface", "obsidian version",
"obsidian help", "key=value", "backlinks", "orphans",
"unresolved links", "deadends", "base:query", "bases query",
"sync history", "file history", "obsidian eval", "eval code=",
"devtools",
"plugin:enable", "theme:set", "cli preflight",
"unable to find obsidian"],
},
"MCP_ADVANCED": {
"weight": 5,
"keywords": ["get_note", "write_note", "search_notes", "manage_tags",
"delete_note", "manage tags", "add a tag", "remove a tag",
"semantic search", "global search", "live app", "rest api note",
"structured note", "read note via mcp", "write note via mcp"],
},
"PLUGIN_MAKEMD": {
"weight": 5,
"keywords": ["make.md", "make-md", "makemd", "make.md space", "make.md context",
"make.md database", "obsidian spaces", "space context", "make.md table",
"notion-like obsidian"],
},
"PLUGIN_TABLES": {
"weight": 5,
"keywords": ["obsidian tables", "obsidian-tables", ".table.md", "table plugin",
"agentable", "columns", "rows", "formula column"],
},
"PLUGIN_BRAT": {
"weight": 5,
"keywords": ["brat", "obsidian42", "obsidian42-brat", "beta plugin",
"community plugin install", "install plugin from github", "frozen version",
"beta theme"],
},
"PLUGIN_ICONIC": {
"weight": 5,
"keywords": ["iconic", "iconic rulebook", "icon rules", "icon automation",
"file icons", "folder icons", "icon color", "iconic data json",
"iconic ruleset"],
},
"PLUGIN_CHARTS": {
"weight": 5,
"keywords": ["charts", "chart", "obsidian charts", "render block", "chart block",
"bar chart", "line chart", "pie chart", "chart json", "advanced-chart"],
},
"PLUGIN_DATAVIEW": {
"weight": 5,
"keywords": ["dataview", "dql", "dataviewjs", "inline field", "metadata query",
"frontmatter query", "task query", "list query", "table query", "dataview query"],
},
"PLUGIN_GIT": {
"weight": 5,
"keywords": ["obsidian git", "obsidian-git", "git plugin", "auto backup",
"vault git", "commit vault", "push vault", "pull vault", "git status", "git log"],
},
"PLUGIN_OUTLINER": {
"weight": 5,
"keywords": ["outliner", "obsidian outliner", "list editing", "list zoom",
"outline plugin", "fold list", "list indentation"],
},
"THEME_SYSTEM": {
"weight": 5,
"keywords": ["theme", "themes", "obsidian theme", "css theme", "community theme",
"appearance", "css snippet", "snippets", "css variable", "theme development",
"build a theme", "plugin development", "minimal theme"],
},
"PLUGIN_HEALTH": {
"weight": 5,
"keywords": ["health-md", "health.md", "health data", "apple health",
"android health", "health chart", "health visualization",
"healthkit", "health export", "healthmd"],
},
"PLUGIN_NOTION_BASES": {
"weight": 5,
"keywords": ["notion bases", "notion-bases plugin", "obsidian-notion-bases-plugin",
"lookup column", "two-way relation", "notion bases rollup",
"notion bases relation", "notion bases view", "database.md schema",
"self-relation subtask"],
},
"PLUGIN_LOCAL_REST_API": {
"weight": 5,
"keywords": ["obsidian-local-rest-api", "local rest api plugin", "coddingtonbear",
"rest api plugin", "vault rest api", "obsidian_verify_ssl",
"vault_read", "vault_write", "vault_patch"],
},
"PLUGIN_ADVANCED_CANVAS": {
"weight": 5,
"keywords": ["advanced canvas", "advanced-canvas", "canvas node", "canvas edge",
"canvas portal", "canvas presentation", "canvas group", "flowchart node",
"edge pathfinding", ".canvas json"],
},
"PLUGIN_CLAUDIAN": {
"weight": 5,
"keywords": ["claudian", "realclaudian", "claude code plugin", "in-vault agent",
"coding agent in obsidian", "claudian slash command", "claudian skill",
"claudian mcp", "agent working directory vault"],
},
"PLUGIN_META_BIND": {
"weight": 5,
"keywords": ["meta bind", "meta-bind", "metabind", "obsidian-meta-bind-plugin",
"input field", "inline button", "button block", "meta bind button",
"frontmatter button", "js engine", "js-engine", "task timer",
"start timer", "stop timer", "note timer"],
},
"PLUGINS": {
"weight": 5,
"keywords": ["plugin", "plugin automation", "community plugin"],
},
"NOTION_MIGRATION": {
"weight": 5,
"keywords": ["migration", "migrate", "notion import", "obsidian import",
"relation recovery", "rollup recovery", "comment reconstruction",
"parity verification"],
},
"INSTALL": {
"weight": 6,
"keywords": ["install notesmd-cli", "install obsidian", "setup", "not found",
"not installed", "api key", "api token", "local rest api",
"enable cli", "brew install", "scoop install",
"add-vault", "getting started", "onboarding", "configure",
"configuration", "mcp config", "obsidian_base_url", "how do i install"],
},
"TROUBLESHOOT": {
"weight": 6,
"keywords": ["error", "failed", "not working", "401", "403", "404", "slow",
"timeout", "empty", "no notes", "broken", "doesn't work",
"isn't working", "won't load", "stuck", "unauthorized",
"forbidden", "connection refused", "can't connect",
"connection failed", "cert", "ssl", "vault not found",
"no default vault"],
},
}
# NOTE: no "DEFAULT" entry — route_obsidian_resources() never indexes RESOURCE_MAP
# by that key. The selected `intent` is one of the twenty-two INTENT_SIGNALS keys above.
# Specific plugin intents always supersede generic PLUGINS whenever any specific signal matches: the highest specific score wins, a tie between specific intents disambiguates, and generic PLUGINS is considered only when no specific plugin signal matches.
# The no-match case is owned by DEFAULT_RESOURCE, whose declared
# fallback-only semantics mean it is SUGGESTED beside the disambiguation checklist,
# never loaded — so obsidian-cli-commands.md can never leak into the MCP_ADVANCED /
# PLUGINS / INSTALL / TROUBLESHOOT routes.
RESOURCE_MAP = {
"NOTES_CLI": ["references/obsidian-cli-commands.md"],
"OFFICIAL_CLI": ["references/official-cli-agent-usage.md",
"references/obsidian-cli-commands.md"],
"MCP_ADVANCED": ["references/mcp-tools.md"],
"PLUGIN_MAKEMD": ["references/plugins/plugin-operation-logic.md",
"references/plugins/make-md/make-md.md",
"references/plugins/make-md/data-model.md",
"references/plugins/make-md/workflows.md",
"references/plugins/make-md/troubleshooting.md"],
"PLUGIN_TABLES": ["references/plugins/plugin-operation-logic.md",
"references/plugins/obsidian-tables/obsidian-tables.md",
"references/plugins/obsidian-tables/data-model.md",
"references/plugins/obsidian-tables/workflows.md",
"references/plugins/obsidian-tables/troubleshooting.md"],
"PLUGIN_BRAT": ["references/plugins/plugin-operation-logic.md",
"references/plugins/obsidian42-brat/obsidian42-brat.md",
"references/plugins/obsidian42-brat/data-model.md",
"references/plugins/obsidian42-brat/workflows.md",
"references/plugins/obsidian42-brat/troubleshooting.md"],
"PLUGIN_ICONIC": ["references/plugins/plugin-operation-logic.md",
"references/plugins/iconic/iconic.md",
"references/plugins/iconic/data-model.md",
"references/plugins/iconic/workflows.md",
"references/plugins/iconic/troubleshooting.md"],
"PLUGIN_CHARTS": ["references/plugins/plugin-operation-logic.md",
"references/plugins/charts/charts.md",
"references/plugins/charts/data-model.md",
"references/plugins/charts/workflows.md",
"references/plugins/charts/troubleshooting.md"],
"PLUGIN_DATAVIEW": ["references/plugins/plugin-operation-logic.md",
"references/plugins/dataview/dataview.md",
"references/plugins/dataview/data-model.md",
"references/plugins/dataview/workflows.md",
"references/plugins/dataview/troubleshooting.md"],
"PLUGIN_GIT": ["references/plugins/plugin-operation-logic.md",
"references/plugins/git/git.md",
"references/plugins/git/data-model.md",
"references/plugins/git/workflows.md",
"references/plugins/git/troubleshooting.md"],
"PLUGIN_OUTLINER": ["references/plugins/plugin-operation-logic.md",
"references/plugins/outliner/outliner.md",
"references/plugins/outliner/data-model.md",
"references/plugins/outliner/workflows.md",
"references/plugins/outliner/troubleshooting.md"],
"THEME_SYSTEM": ["references/themes/themes.md",
"references/themes/customization.md",
"references/themes/theme-development.md",
"references/themes/plugin-development.md"],
"PLUGIN_HEALTH": ["references/plugins/plugin-operation-logic.md",
"references/plugins/health-md/health-md.md",
"references/plugins/health-md/data-model.md",
"references/plugins/health-md/workflows.md",
"references/plugins/health-md/troubleshooting.md"],
"PLUGIN_NOTION_BASES": ["references/plugins/plugin-operation-logic.md",
"references/plugins/notion-bases/notion-bases.md",
"references/plugins/notion-bases/data-model.md",
"references/plugins/notion-bases/workflows.md",
"references/plugins/notion-bases/troubleshooting.md"],
"PLUGIN_LOCAL_REST_API": ["references/plugins/plugin-operation-logic.md",
"references/plugins/obsidian-local-rest-api/obsidian-local-rest-api.md",
"references/plugins/obsidian-local-rest-api/data-model.md",
"references/plugins/obsidian-local-rest-api/workflows.md",
"references/plugins/obsidian-local-rest-api/troubleshooting.md"],
"PLUGIN_ADVANCED_CANVAS": ["references/plugins/plugin-operation-logic.md",
"references/plugins/advanced-canvas/advanced-canvas.md",
"references/plugins/advanced-canvas/data-model.md",
"references/plugins/advanced-canvas/workflows.md",
"references/plugins/advanced-canvas/troubleshooting.md"],
"PLUGIN_CLAUDIAN": ["references/plugins/plugin-operation-logic.md",
"references/plugins/claudian/claudian.md",
"references/plugins/claudian/data-model.md",
"references/plugins/claudian/workflows.md",
"references/plugins/claudian/troubleshooting.md"],
"PLUGIN_META_BIND": ["references/plugins/plugin-operation-logic.md",
"references/plugins/meta-bind/meta-bind.md",
"references/plugins/meta-bind/data-model.md",
"references/plugins/meta-bind/workflows.md",
"references/plugins/meta-bind/troubleshooting.md"],
"PLUGINS": ["references/plugins/plugin-operation-logic.md",
"references/plugins/installed-plugins.md",
"references/plugins/obsidian-tables/obsidian-tables.md",
"references/plugins/obsidian42-brat/obsidian42-brat.md",
"references/plugins/iconic/iconic.md",
"references/plugins/health-md/health-md.md",
"references/plugins/charts/charts.md",
"references/plugins/dataview/dataview.md",
"references/plugins/git/git.md",
"references/plugins/outliner/outliner.md",
"references/themes/themes.md",
"references/plugins/notion-bases/notion-bases.md",
"references/plugins/make-md/make-md.md",
"references/plugins/obsidian-local-rest-api/obsidian-local-rest-api.md",
"references/plugins/advanced-canvas/advanced-canvas.md",
"references/plugins/claudian/claudian.md",
"references/plugins/meta-bind/meta-bind.md"],
"NOTION_MIGRATION": ["references/notion-migration.md"],
"INSTALL": ["references/troubleshooting.md"],
"TROUBLESHOOT": ["references/troubleshooting.md"],
}
def discover_markdown_resources() -> set[str]:
docs = []
for base in RESOURCE_BASES:
if base.exists():
docs.extend(path for path in base.rglob("*.md") if path.is_file())
return {doc.relative_to(SKILL_ROOT).as_posix() for doc in docs}
def _guard_in_skill(relative_path: str) -> str:
resolved = (SKILL_ROOT / relative_path).resolve()
resolved.relative_to(SKILL_ROOT)
if resolved.suffix.lower() != ".md":
raise ValueError(f"Only markdown skill resources are routable: {relative_path}")
return resolved.relative_to(SKILL_ROOT).as_posix()
def load_if_available(relative_path, loaded, seen, inventory) -> None:
guarded = _guard_in_skill(relative_path)
if guarded in inventory and guarded not in seen:
load(guarded)
loaded.append(guarded)
seen.add(guarded)
def route_obsidian_resources(request: str) -> dict:
"""Score intent labels and load available Obsidian reference docs."""
inventory = discover_markdown_resources()
loaded, seen = [], set()
request_lower = request.lower()
scores = {}
for intent, config in INTENT_SIGNALS.items():
score = sum(
config["weight"] for kw in config["keywords"]
if kw in request_lower
)
if score > 0:
scores[intent] = score
if not scores:
# Fallback-only: nothing is loaded on a zero-score route; the default
# reference is offered as a suggestion beside the disambiguation ask.
return {
"load_level": "UNKNOWN_FALLBACK",
"needs_disambiguation": True,
"disambiguation_checklist": UNKNOWN_FALLBACK_CHECKLIST,
"suggested_fallback": DEFAULT_RESOURCE,
"resources": loaded,
}
# Error/install keywords boost TROUBLESHOOT/INSTALL regardless of other signals.
if scores.get("TROUBLESHOOT", 0) > 3:
intent = "TROUBLESHOOT"
elif scores.get("INSTALL", 0) > 4:
intent = "INSTALL"
else:
specific_plugin_intents = ("PLUGIN_MAKEMD", "PLUGIN_TABLES", "PLUGIN_BRAT", "PLUGIN_ICONIC",
"PLUGIN_CHARTS", "PLUGIN_DATAVIEW",
"PLUGIN_GIT", "PLUGIN_OUTLINER", "THEME_SYSTEM", "PLUGIN_HEALTH",
"PLUGIN_NOTION_BASES", "PLUGIN_LOCAL_REST_API",
"PLUGIN_ADVANCED_CANVAS", "PLUGIN_CLAUDIAN", "PLUGIN_META_BIND")
matched_specific_plugin_intents = [
plugin_intent
for plugin_intent in specific_plugin_intents
if scores.get(plugin_intent, 0) > 0
]
if matched_specific_plugin_intents:
max_specific_score = max(
scores[plugin_intent] for plugin_intent in matched_specific_plugin_intents
)
top_specific_plugin_intents = [
plugin_intent
for plugin_intent in matched_specific_plugin_intents
if scores[plugin_intent] == max_specific_score
]
if len(top_specific_plugin_intents) > 1:
return {
"needs_disambiguation": True,
"candidate_intents": top_specific_plugin_intents,
"disambiguation_checklist": UNKNOWN_FALLBACK_CHECKLIST,
"resources": loaded,
}
intent = top_specific_plugin_intents[0]
else:
intent = max(scores, key=scores.get)
for resource in RESOURCE_MAP[intent]:
load_if_available(resource, loaded, seen, inventory)
if not loaded:
return {
"load_level": "UNKNOWN_FALLBACK",
"notice": f"No Obsidian reference docs available for intent '{intent}'",
"disambiguation_checklist": UNKNOWN_FALLBACK_CHECKLIST,
"suggested_fallback": DEFAULT_RESOURCE,
"resources": loaded,
}
return {"intent": intent, "resources": loaded}
3. HOW IT WORKS
Surface Comparison
| Dimension | Headless CLI (notesmd-cli) |
App-backed CLI (obsidian) |
Cyanheads MCP |
|---|---|---|---|
| Activation | notesmd-cli <command> in Bash |
obsidian <command> key=value in Bash |
Code Mode call_tool_chain() |
| Running app | Not required — filesystem only | Required, and NOT launched for you (exit 1 when down) | Required + Local REST API |
| Best for | Daily note ops anywhere | In-app open / URI actions | Structured reads/writes, tags, search |
| Auth | None — reads the vault directory | None — controls the local app | OBSIDIAN_API_KEY bearer token |
| Install | brew install yakitrak/yakitrak/notesmd-cli |
Ships with Obsidian desktop v1.12.4+ | npx -y obsidian-mcp-server@latest |
| Config store | ~/.config/obsidian/obsidian.json |
In-app: Settings → Command line interface | .utcp_config.json manual obsidian |
| Output | Human-readable text | Human-readable text on stdout, exit 0 even on failure | Structured JSON |
Headless CLI — notesmd-cli (Default Path)
Yakitrak's notesmd-cli (binary notesmd-cli, renamed from "obsidian-cli" to avoid confusion with the official one) operates directly on the vault filesystem, so it works with no running app.
Step 1: Verify installation
notesmd-cli --version
notesmd-cli list-vaults # Shows registered vaults + default
Step 2: Install if missing
bash .opencode/skills/mcp-tooling/mcp-obsidian/scripts/install.sh
# or directly:
brew tap yakitrak/yakitrak && brew install yakitrak/yakitrak/notesmd-cli
Step 3: Register a vault (one-time)
notesmd-cli add-vault "/path/to/Vault" # writes ~/.config/obsidian/obsidian.json
notesmd-cli set-default-vault "Vault"
Step 4: Daily note operations
notesmd-cli list # List notes in the default vault
notesmd-cli search "meeting" # Search by note name
notesmd-cli search-content "action item" # Full-text content search
notesmd-cli print "Inbox" # Print a note to stdout
notesmd-cli create "New Idea" # Create a note
notesmd-cli daily # Open/append today's daily note
App-backed CLI — official obsidian (Live-App Path)
The official obsidian CLI shipped GA in Obsidian desktop v1.12.4 (Feb 2026). It is a remote control for an already-running app, not a headless tool and not a launcher: with the app down it prints The CLI is unable to find Obsidian to stderr and exits 1. There is no npm/brew package — it ships with the desktop app.
Enable in-app: Settings → General → Command line interface → toggle on → "Register CLI" (auto-adds obsidian to PATH on macOS/Linux; on macOS this symlinks /usr/local/bin/obsidian into the app bundle).
Syntax is obsidian <command> key=value, with no POSIX flags: obsidian version, not obsidian --version. Use it for outcomes only the live app can produce — the resolved link graph, the computed tag and task index, Bases queries, sync and file history, plugin and theme state, and in-app opens.
Two contracts before scripting it. Preflight with obsidian version | grep -qE '^[0-9]+\.[0-9]+' (a version number means registered AND running, where exit 0 does not, because a starting app answers 0 with an error). Then stop trusting $?: once the app is up the CLI exits 0 even on failure and prints Error: ... to stdout. Both, plus the 106-command surface and the active-file hazard, are in references/official-cli-agent-usage.md.
Cyanheads MCP — obsidian-mcp-server (Structured Path)
The default MCP is cyanheads' obsidian-mcp-server (npm @3.5.0), launched over stdio via npx -y obsidian-mcp-server@latest (it also supports http on 127.0.0.1:3010/mcp). It talks to the vault through the Local REST API plugin v4.0.0+, so it needs a running Obsidian + the plugin enabled + an API key.
Prerequisites:
- Code Mode MCP configured, with the
obsidianmanual in.utcp_config.json(notopencode.json, that file is for native/non-Code-Mode MCP tools) - Obsidian running with the Local REST API plugin enabled and an API key generated
obsidian_OBSIDIAN_API_KEY(required),obsidian_OBSIDIAN_BASE_URL(defaulthttp://127.0.0.1:27123),obsidian_OBSIDIAN_VERIFY_SSL(defaultfalse) available to Code Mode
Configuration (.utcp_config.json, manual_call_templates):
{
"name": "obsidian",
"call_template_type": "mcp",
"config": {
"mcpServers": {
"obsidian": {
"transport": "stdio",
"command": "npx",
"args": ["-y", "obsidian-mcp-server@latest"],
"env": {
"OBSIDIAN_API_KEY": "${obsidian_OBSIDIAN_API_KEY}",
"OBSIDIAN_BASE_URL": "${obsidian_OBSIDIAN_BASE_URL}",
"OBSIDIAN_VERIFY_SSL": "${obsidian_OBSIDIAN_VERIFY_SSL}"
}
}
}
}
}
The obsidian_ env prefix matches the manual name obsidian, so ${obsidian_OBSIDIAN_API_KEY} resolves correctly. This registration is already in place: the obsidian manual lives in .utcp_config.json and the obsidian_OBSIDIAN_* keys in .env.example. This skill documents that wiring; it does not rewrite those files.
Tools: the server exposes 14 obsidian_* tools. The core five are obsidian_get_note, obsidian_write_note, obsidian_search_notes, obsidian_manage_tags, obsidian_delete_note; the full 14-tool catalog with per-tool inputs lives in references/mcp-tools.md. Confirm every name with tool_info()/list_tools() before calling — never guess it from a description.
Invocation via Code Mode (call_tool_chain takes a single code string):
// Tool naming: obsidian.obsidian_{tool_name}
const result = await call_tool_chain({
code: `
const note = await obsidian.obsidian_get_note({ path: "Inbox/Idea.md" });
return note;
`,
});
When to prefer the MCP:
- Structured note reads/writes where you need JSON back, not terminal text
- Tag management (
obsidian_manage_tags) across a live vault - Global or semantic search over an active, plugin-backed vault
Limitations:
- Requires a running Obsidian + Local REST API plugin + API key (headless CLI has none of these needs)
- Requires Code Mode MCP to be configured
OBSIDIAN_VERIFY_SSLdefaults tofalse; only enable it behind a trusted TLS endpoint
Alternative MCP servers (two exist — do not conflate the tool names): (1) Local REST API's own built-in MCP — the obsidian-local-rest-api plugin (v5.1.0+) serves a Streamable HTTP MCP at https://127.0.0.1:27124/mcp/ exposing 16 vault_* tools (vault_read/vault_write/vault_patch/search_simple/tag_list/…) — validated working, needs no extra npm package, and is a DIFFERENT surface from the cyanheads obsidian_* tools. (2) StevenStavrakis' obsidian-mcp (npm @1.0.6) is filesystem-based — treat its no-app/no-token claim as VERIFY.
4. RULES
✅ ALWAYS
- Resolve the execution profile before acting — headless
notesmd-cliis the default; only route to the app-backedobsidianCLI or the MCP when a live app (and, for the MCP, Local REST API +OBSIDIAN_API_KEY) is actually available. Probe, do not assume. - Register and confirm the vault first — run
notesmd-cli list-vaultsand, if empty,notesmd-cli add-vault <path>+set-default-vault <name>before any note operation. - Preview destructive note ops — for
moveanddelete, confirm the exact note name withnotesmd-cli list/searchbefore running the write; deletions are hard to reverse. - Quote note names and paths — note titles and vault paths contain spaces; always wrap them in quotes in Bash.
- Run
notesmd-cli --version(and, for the MCP path, confirm the Local REST API is reachable) as preflight before starting an Obsidian session. - Treat empty
notesmd-cli list/searchresults as valid — an empty vault or no-match search is not an error. Verify the default vault and query spelling before escalating. - Confirm MCP tool names with
tool_info()before callingobsidian.obsidian_*— the callable form isobsidian.obsidian_{tool_name}.
⛔ NEVER
- Never assume a running app for the default path —
notesmd-cliis filesystem-only and needs no app; do not launch Obsidian just to run a headless note op. - Never confuse the two CLIs —
notesmd-cli(Yakitrak, headless) andobsidian(official, app-backed) are different binaries with different behavior. The official one requires and controls a running app. - Never use the StevenStavrakis
obsidian-mcpas the default MCP — this skill's MCP path is the cyanheadsobsidian-mcp-server. The Stavrakis server is only the documented headless alternative. - Never auto-modify
.utcp_config.json,.env.example,opencode.json, or hub files — theobsidianmanual is already registered there; this skill only documents it. If the wiring ever needs changing, print the config for the user; never write these files programmatically. - Never hardcode the API key or base URL in commands or notes — read them from
obsidian_OBSIDIAN_API_KEY/obsidian_OBSIDIAN_BASE_URLin the Code Mode environment. - Never fabricate notes — if
notesmd-cli list/searchreturns empty, the vault or query is genuinely empty. Report it clearly. - Never enable
OBSIDIAN_VERIFY_SSLblindly — it defaults tofalse; only turn it on behind a trusted TLS endpoint.
⚠️ ESCALATE IF
notesmd-cliis not installed andscripts/install.shfails → report the OS/package-manager (brew/scoop/AUR) and Go toolchain statenotesmd-cli list-vaultsshows no vault or no default → direct toadd-vault+set-default-vault- The MCP connection fails → verify the
obsidianmanual in.utcp_config.jsonlaunchesnpx -y obsidian-mcp-server@latestover stdio, that Obsidian is running with Local REST API v4.0.0+ enabled, and thatobsidian_OBSIDIAN_API_KEY/obsidian_OBSIDIAN_BASE_URLare set - A
401/403from the MCP → the API key is missing, wrong, or the Local REST API plugin is disabled - A connection-refused / SSL error on
OBSIDIAN_BASE_URL→ the plugin is not listening on the expected port, orOBSIDIAN_VERIFY_SSLis mismatched with the endpoint
5. SUCCESS CRITERIA
-
notesmd-cli --versionprints a version string -
notesmd-cli list-vaultsshows at least one registered vault with a default -
notesmd-cli listreturns notes for the default vault (empty list is valid) -
notesmd-cli search-content "<query>"returns body matches (titlesearchis broken in v0.3.6 — uselist+ filter for names) - For app-backed work:
obsidian versionexits 0 before any other official-CLI call, and each result is checked for a leadingError:on stdout rather than by exit status - For MCP work: a Code Mode
obsidian.obsidian_get_notecall returns note content as JSON
6. INTEGRATION POINTS
Gate 2 (Skill Routing): This skill activates at ≥0.8 confidence for Obsidian note/vault requests. The skill advisor matches on: obsidian, notesmd-cli, obsidian vault, note management, markdown notes, local rest api.
Code Mode MCP: The cyanheads MCP tools are invoked via mcp__code_mode__call_tool_chain. Tool naming convention: obsidian.obsidian_{tool_name}. See references/mcp-tools.md for the full 14-tool catalog.
Memory: Save Obsidian workflow context (default vault name, base URL, active plugin set) using /speckit:save when switching sessions.
Tool Usage: Use Bash for notesmd-cli and the official obsidian CLI. Use mcp__code_mode__call_tool_chain for cyanheads MCP operations. Use Read to load references on demand.
7. QUICK REFERENCE
notesmd-cli Command Cheat Sheet (headless)
| Category | Command | Description |
|---|---|---|
| Vaults | notesmd-cli add-vault "<path>" |
Register a vault |
notesmd-cli remove-vault "<name>" |
Unregister a vault | |
notesmd-cli list-vaults |
List registered vaults + default | |
notesmd-cli set-default-vault "<name>" |
Set the default vault | |
| Read | notesmd-cli list |
List notes in the default vault |
notesmd-cli search "<query>" |
Search by note name | |
notesmd-cli search-content "<query>" |
Full-text content search | |
notesmd-cli print "<name>" |
Print a note to stdout | |
| Write | notesmd-cli create "<name>" |
Create a note |
notesmd-cli daily |
Open/append today's daily note | |
notesmd-cli move "<a>" "<b>" |
Move / rename a note | |
notesmd-cli delete "<name>" |
Delete a note | |
notesmd-cli frontmatter "<name>" |
Edit a note's frontmatter | |
| Open | notesmd-cli open "<name>" |
Open a note |
Official obsidian CLI (app-backed)
Syntax is obsidian <command> key=value. No POSIX flags. Global option vault=<name>, accepted before or after the command.
| Command | Description |
|---|---|
obsidian version |
Preflight. Exit 0 means registered AND app running; exit 1 means the app is down |
obsidian help |
The authoritative 106-command list from the installed binary |
obsidian read file="<name>" |
Print note content. Always pass a target, or it reads the human's open note |
obsidian create name="<name>" content="..." |
Create a note. On a name collision it makes <name> 1.md rather than failing |
obsidian append file="<name>" content="..." |
Append to a named note |
obsidian search query="..." format=json |
Search; JSON returns an array of paths |
obsidian search:context query="..." format=json |
Search with matching lines |
obsidian property:set file="<n>" name=k value=v |
Set a frontmatter property |
obsidian backlinks file="<name>" |
Resolved backlinks — no filesystem equivalent |
obsidian tags / obsidian tasks |
The app's computed vault-wide index |
obsidian base:query file="<base>" format=json |
Query a Base |
(Enable first: Settings → General → Command line interface → Register CLI. Ships with Obsidian desktop v1.12.4+. Every command above exits 0 even when it fails — check stdout for a leading Error:.)
Cyanheads MCP Call Pattern (Code Mode)
// Tool naming: obsidian.obsidian_{tool_name} — 14 tools total
await call_tool_chain({
code: `
const hit = await obsidian.obsidian_search_notes({ query: "roadmap" });
const note = await obsidian.obsidian_get_note({ path: hit[0].path });
await obsidian.obsidian_manage_tags({ path: hit[0].path, add: ["reviewed"] });
return note;
`,
});
8. REFERENCES AND RELATED RESOURCES
Reference Files (load on demand via router):
references/obsidian-cli-commands.md— Fullnotesmd-cli+ officialobsidianCLI command reference with agent patternsreferences/official-cli-agent-usage.md— Official app-backed CLI: preflight, the exit-0 failure contract,key=valuesyntax, the 106-command surface, safety invariants, and where the vendor docs disagree with the binaryreferences/mcp-tools.md— Cyanheadsobsidian-mcp-server14-tool catalog, priorities, andcall_tool_chain()invocationreferences/troubleshooting.md— Install, vault, auth, Local REST API and MCP failuresreferences/notion-migration.md— Notion→Obsidian migration reconstruction method: 8-step method, division of labor, relation/rollup/formula recovery, comment reconstruction, verification protocolreferences/plugins/plugin-operation-logic.md— How plugin-driven note automation is operatedreferences/plugins/obsidian-tables/obsidian-tables.md— Obsidian Tables plugin indexreferences/plugins/obsidian-tables/data-model.md—.table.mdenvelope, columns, rows, formulas, and viewsreferences/plugins/obsidian-tables/workflows.md— Obsidian Tables file-layer recipesreferences/plugins/obsidian-tables/troubleshooting.md— Obsidian Tables failure and recovery recipesreferences/plugins/obsidian42-brat/obsidian42-brat.md— BRAT beta-plugin install/update indexreferences/plugins/obsidian42-brat/data-model.md— BRATdata.jsonand release-policy data modelreferences/plugins/obsidian42-brat/workflows.md— BRAT stage, register, activate, and update recipesreferences/plugins/obsidian42-brat/troubleshooting.md— BRAT release, asset, compatibility, and path recovery recipesreferences/plugins/iconic/iconic.md— Iconic icon/color rulebook plugin index (usage companion:assets/plugins/iconic/iconic-rules.full.md; canonical full rule payload:assets/plugins/iconic/iconic-rules.full.json, 21 file + 11 folder rules)references/plugins/iconic/data-model.md— Iconicdata.jsonkeys, rule schema, and the merge-only rulebook contractreferences/plugins/iconic/workflows.md— Iconic file-layer recipes: add/edit/disable rules, flip toggles, apply the canonical rulebookreferences/plugins/iconic/troubleshooting.md— Iconic failure and recovery recipesreferences/plugins/notion-bases/notion-bases.md— Notion Bases plugin index (repobgarciamoura/obsidian-notion-bases-plugin, version pin v1.5.0+)references/plugins/notion-bases/data-model.md—_database.mdschema: two-way relation columns, the 7 rollup functions, lookup columns, self-relation subtasks, and the 7 view typesreferences/plugins/notion-bases/workflows.md— Notion Bases file-layer recipes plus a Dataview supplement for aggregations the plugin doesn't coverreferences/plugins/notion-bases/troubleshooting.md— Notion Bases failure and recovery recipes: schema mismatch, missing back-reference, unsupported view typereferences/plugins/make-md/make-md.md— Make.md plugin index (repoMake-md/makemd, idmake-md, v1.3.5+): Notion-like Spaces + Contextsreferences/plugins/make-md/data-model.md— Make.md.spaceon-disk format:def.json,context.mdb/views.mdbSQLite (m_schema/m_fields), field types, and frontmatter→column mappingreferences/plugins/make-md/workflows.md— Make.md file-layer recipes: create a Space, configure a table/board/chart, and the golden-sample clone approachreferences/plugins/make-md/troubleshooting.md— Make.md failure and recovery recipes: mobile performance, stale.makemdcache, columns not showingreferences/plugins/obsidian-local-rest-api/obsidian-local-rest-api.md— Local REST API plugin index (repocoddingtonbear/obsidian-local-rest-api), the HTTP/HTTPS backend the cyanheads MCP rides onreferences/plugins/obsidian-local-rest-api/data-model.md—data.jsonconfig surface, theOBSIDIAN_API_KEY/OBSIDIAN_BASE_URL/OBSIDIAN_VERIFY_SSLenv keys, and the two loopback endpointsreferences/plugins/obsidian-local-rest-api/workflows.md— Enable the plugin, read the API key, wire the MCP, and the app-must-be-running boundaryreferences/plugins/obsidian-local-rest-api/troubleshooting.md— MCP-not-found, auth, SSL/OBSIDIAN_VERIFY_SSL, and port-conflict recovery recipesreferences/plugins/installed-plugins.md— Roster of all 21 enabled vault plugins: file-layer plugins (with dedicated docs) vs UI/automatic plugins (no dedicated docs)references/plugins/advanced-canvas/advanced-canvas.md— Advanced Canvas plugin index (repodeveloper-mike/obsidian-advanced-canvas), extends the native.canvasJSONreferences/plugins/advanced-canvas/data-model.md— The extended.canvasnode/edge schema: node types, edge pathfinding, presentation order, portals, groupsreferences/plugins/advanced-canvas/workflows.md— Advanced Canvas file-layer recipes: styled/typed nodes, portals, presentations, exportreferences/plugins/advanced-canvas/troubleshooting.md— Advanced Canvas failure and recovery recipesreferences/plugins/claudian/claudian.md— Claudian plugin index (repoYishenTu/claudian, manifest idrealclaudian), embeds coding-agent CLIs in the vaultreferences/plugins/claudian/data-model.md— Claudian in-vault artifacts: slash commands, skills/prompt templates, provider and MCP config, and where they live on diskreferences/plugins/claudian/workflows.md— Claudian recipes: register a provider CLI, author a reusable skill/command, connect an MCP server, plan mode and @-mentionsreferences/plugins/claudian/troubleshooting.md— Claudian failure and recovery recipes, the three-name disambiguation, and the resolved-but-unconfirmed in-app-search issuereferences/plugins/meta-bind/meta-bind.md— Meta Bind plugin index (repomProjectsCode/obsidian-meta-bind-plugin), the inline input/button/view field syntaxreferences/plugins/meta-bind/data-model.md— Meta Bind field syntax:INPUT[]/BUTTON[]/VIEW[]widgets, button action lists, and the JS Engine companion for computed timestampsreferences/plugins/meta-bind/workflows.md— Meta Bind recipes: the Notion-style task-timer build (Notion Bases DB + Start/End buttons + Total-Time formula) and frontmatter-driven buttonsreferences/plugins/meta-bind/troubleshooting.md— Meta Bind failure and recovery recipes: button action errors, JS Engine evaluation, and field-binding pitfalls
Install guide (front door): INSTALL-GUIDE.md — condensed top-level install doc for both CLI profiles and the MCP; references/troubleshooting.md is the router's INSTALL/TROUBLESHOOT-intent target.
Scripts:
scripts/install.sh— Installsnotesmd-cliand prints the MCP config snippetscripts/doctor.sh— Diagnoses the CLI + MCP setup, including a liveobsidian versionprobe that separates "not registered" from "registered but the app is down"
Embedded Servers:
mcp-servers/obsidian-cli/README.md—notesmd-cliinstall pointer (brew/scoop/AUR/source); runsetup.shmcp-servers/obsidian-mcp/README.md— Cyanheadsobsidian-mcp-serverconfiguration notes (stdio vianpx -y obsidian-mcp-server@latest,OBSIDIAN_API_KEY/OBSIDIAN_BASE_URL/OBSIDIAN_VERIFY_SSL)
Examples:
examples/README.md— Guide to the example note-workflow scriptsexamples/headless-notes-workflow.sh—notesmd-cliround trip with no running appexamples/official-cli-workflow.sh— Official app-backed CLI round trip with the preflight and the exit-0 result wrapper
Related Skills:
mcp-click-up— Structural sibling; same two-path (CLI + MCP) orchestrator patternmcp-code-mode— Code Mode MCP orchestration (used for cyanheads MCP invocation)
External:
- notesmd-cli repository: https://github.com/Yakitrak/obsidian-cli
- cyanheads obsidian-mcp-server: https://github.com/cyanheads/obsidian-mcp-server
- Obsidian Local REST API plugin: https://github.com/coddingtonbear/obsidian-local-rest-api