Obsidian MCP (pgvector + Ollama, self-hosted)
The Obsidian MCP server is a self-hosted Model Context Protocol server that turns your Obsidian vault into a searchable, semantic memory layer for AI agents — supporting full-text search, vector similarity, wikilink graph traversal, and structured read/write operations over markdown notes.
Search & Discovery
Keyword search (
keyword_search): Full-text search via PostgreSQLtsvector— best for exact phrases, identifiers, tags, or frontmatter filtersSemantic search (
semantic_search): Vector similarity search using Ollama bge-m3 or OpenAI embeddings — best for conceptual or paraphrased queriesList notes (
list_notes): Browse notes by folder with optional tag/frontmatter filtersRecent notes (
get_recent): Get recently modified notes, optionally filteredTag index (
get_tags): All tags across the vault with note countsVault guide (
get_vault_guide): Obsidian syntax primer plus vault-specific conventions fromCLAUDE.md
Reading & Writing Notes
Read (
read_note): Fetch full content by vault-relative pathCreate (
create_note): Atomically create a new note (refuses to overwrite)Edit (
edit_note): Four modes — full replace, append, find-and-replace, or section replacement; supportsdry_runto preview a unified diffMove/rename (
move_note): Relocate a note and optionally rewrite all incoming wikilinksDelete (
delete_note): Soft-delete to.trash/by default, or hard-delete withpermanent=TrueMutate frontmatter (
set_frontmatter): Add, update, or remove YAML frontmatter keys without touching the note body
Wikilink Graph Traversal
Backlinks (
get_backlinks): Find all notes linking TO a given noteOutgoing links (
get_links): List all links going OUT from a note, including dangling referencesNeighborhood (
get_neighborhood): BFS traversal up to 5 hops to explore a local clusterRelated notes (
find_related): Semantically similar notes via chunk embeddings, independent of explicit linksOrphan finder (
find_orphans): Identify notes with zero incoming and outgoing resolved links
Administration & Security
readandreadwritescoped API keys; OAuth 2.0 PKCE flow for clientsWeb-based control panel: usage logs, vault browser, indexer status, settings
Optional multi-tenant mode with per-user vaults and an admin role
All writes are atomic (tmp file +
os.replace) to prevent corruptionPersistent PostgreSQL/pgvector index with configurable periodic reindexing (~5-minute lag for new files)
Provides tools to search, read, write, and manage an Obsidian vault, enabling AI agents to interact with notes, wikilinks, tags, and frontmatter.
Obsidian MCP Server
A memory system for your AI agents — stored as plain markdown you can open in Obsidian.
A self-hosted Model Context Protocol server that gives every agent you connect a durable, shared place to remember things. The storage isn't a vector database you can't see into: it's a folder of markdown files in your Obsidian vault, backed by full-text and semantic search and by your own wikilink graph. Obsidian is the human window onto it — open a note, read exactly what an agent wrote about you, correct it, delete it, or take the whole folder somewhere else. Self-describing, too — agents read what you read, link what you link, and pick up your folder layout, frontmatter schema, and tag conventions on the first call instead of being briefed from scratch every session.
To be precise about the scope: what the server supplies is MCP-accessible storage, keyword and semantic search, and graph operations over markdown notes, for whatever MCP clients you connect. The agents direct their own reads and writes. There is no automatic extraction, consolidation, or decay pipeline running behind them — an agent remembers something because it wrote a note, and forgets it because someone deleted one.
Stack: Python 3.12, FastAPI, PostgreSQL with pgvector. Pluggable
embeddings (Ollama bge-m3, or OpenAI text-embedding-3-{small,large}).

Contents
Related MCP server: Vault Cortex
Why this exists
There are three things going on here, and they're more interesting together than apart.
1. Agent memory that you can actually read
If you let an agent run for a while, it needs memory. Most setups solve this with an opaque vector store, a SQLite blob, or a managed "memory" service that you can't see into. That works until you want to know what the agent thinks it knows about you, or you need to correct something, or you want to understand why it just made a weird suggestion.
This server gives you a different deal. Agent memory lives as markdown files in your vault. Folder structure, file names, frontmatter, all visible. You can open the file in Obsidian and read it. You can edit it. You can delete it. You can grep it. The agent's "memory" is a human-auditable artifact that sits in the same place as your own notes, with the same tools available.
The home lab is the use case that sold me on this. My vault has notes on the rack, the network, and every Home Assistant integration. I can say "set up a night-light mode in the master bathroom, 1% after 11pm" and a sysadmin agent finds the right config, makes the change, and updates the doc in the same pass. Six months later when I've forgotten how it works, the answer is in the vault, not buried in some chat history I can't search.
The semantic search and wikilink graph still work over that material, so retrieval is fast and conceptual. But the substrate is files you own, not a black box.
2. A shared memory layer between you and your agents
The other half runs the other way: the vault isn't only the agents' memory, it's mine. I think of my Obsidian vault as my exocortex. The "big me" that includes notes, calendars, scripts, search, and AI assistants is substantially more capable than the "small me" of the biological brain alone. It's also where I do most of my thinking, because writing something down is itself a form of thought.
The problem is that until recently, the vault was passive. I had to go find things. Agents that wanted to help me had to be briefed from scratch every session, and they had no way to see what I'd already written about a topic.
This server fixes that. Now the same vault feeds my own daily writing and any agent I plug into it. The agent reads what I read, links what I link, follows the same wikilinks, sees the same frontmatter. When I write a project note on Sunday, my Monday-morning briefing agent already knows about it. When the agent leaves notes from a research session, they show up in my normal Obsidian search.
A concrete version of this: I'll spend a session in Claude Code on a project, wrap up, push the commits, and then just say "update Obsidian." The agent reads the vault guide, figures out where project notes live in my structure, picks the right format and frontmatter, and leaves a session log I can later roll into a status report. No path-passing, no telling it what to write — the conventions are already in the vault, and it follows them.
That's the exocortex idea made concrete: one place that holds context, and both the human and the agents reading and writing into it on the same terms.
3. The vault follows you
The thing that still surprises me is that this is internet-facing. Same vault, same notes, reachable from anywhere I happen to be talking to an agent — Claude in a browser tab, Claude on my phone on the train, an n8n workflow firing on a schedule, a Claude Code session on whatever laptop is in front of me. All behind OAuth or a bearer key, all hitting the same source of truth.
So when I hear an interesting podcast on a commute and want to think out loud about it with Claude on my phone, the agent isn't starting cold. It can pull up what I've already written on adjacent topics, surface a note I half-finished six months ago, and at the end of the conversation suggest updates and write them in. The vault doesn't have to be near me to be the thing I'm working in. (A session away from the keyboard walks through exactly one of these.)
A session at the keyboard
To make the abstract concrete, a short transcript from a real session. Tool calls inline; the user types in plain English and the agent picks the right tool.
you > find anything I've written about why retrieval-augmented
chat is hard
claude
→ semantic_search(query="why retrieval-augmented chat is hard")
• 2025-12-03 - RAG failure modes.md
• 2025-10-18 - Chunking tradeoffs.md
• 2024-06-22 - Vector recall vs precision.md
the closest hit is your December note on RAG failure modes. let
me see what it's connected to.
→ get_neighborhood(path="2025-12-03 - RAG failure modes.md",
depth=1)
8 connected notes. notable ones:
• "Hybrid search" — a re-ranking design you sketched but
didn't ship
• "Out-of-distribution queries" — a half-finished draft from
October
• "Embedding model choice" — links to bge-m3, your current
model
you have three connected pieces here: a failure-modes catalog,
an unshipped hybrid-search sketch, and an abandoned O-O-D draft.
want me to summarize all three together, or pull up the draft
so you can decide whether to finish it?
you > pull up the draft, and tag it #revisit-2026 in frontmatter
while you're there
claude
→ read_note(path="2025-10-18 - OOD queries (draft).md")
→ set_frontmatter(
path="2025-10-18 - OOD queries (draft).md",
updates={"tags": ["draft", "rag", "revisit-2026"]}
)
here's the draft, and the tag is on. you stopped right before
the section on confidence thresholds; the open question you
left yourself was…Two things to notice. First, the agent didn't need to be told what
folder to look in or what tools to use — it picked them. Second, the
write at the end is structured (set_frontmatter mutating YAML, not
a regex over the file body), so the note round-trips cleanly. The
self-describing vault and the wikilink graph are doing the work that
makes this feel natural.
A session away from the keyboard
The transcript above is the easy case: I'm at a desk, I can see what the agent is doing, and Obsidian is one alt-tab away. The session that actually changed how I think about this server had none of that.
I was out walking with a health podcast in my ears — a long one, two people who clearly disagreed with each other, an hour of it. I had my phone and no intention of going home to a laptop. So I pulled the episode's transcript, handed it to Claude on my phone, and we talked it through while I kept walking: what the actual claim was, which parts I already had notes on, where it cut against something I'd decided months ago and written down at the time.
The agent had the vault the whole way. It surfaced what I'd already written on the topic, flagged that two dates in an older note were wrong, and asked whether a decision I'd recorded last year still stood given what the episode argued. By the time I got back it had written all of it in: the health-related decisions I'd actually landed on during the walk, the date corrections in the old note, a couple of new notes on the episode itself — and, because the conversation kept circling back to it, a durable note on how I decide which experts to trust on medical questions in the first place. That last one is the artifact I keep returning to. It wasn't about the episode at all; it was the reasoning underneath a whole class of decisions, and it now sits in the vault where the next agent will find it.
I never opened Obsidian. Not on the walk, not when I got home. The whole session — retrieval, argument, correction, and the writing that came out of it — went through an agent, and the vault is simply where it landed. Obsidian is how I check the work afterwards, not how the work gets done. That inversion is most of the reason this project looks the way it does.
What's in the box
The server exposes 25 MCP tools across five families, plus the auth and ops layer around them.
Search and discovery
keyword_search(query, folder?, tags?, frontmatter?, limit=20), full-text via PostgreSQLtsvector; the text-search config(s) are configurable viaFTS_CONFIGS(see Full-text search language(s))semantic_search(query, folder?, tags?, frontmatter?, limit=15), vector similarity via pgvector, one preview chunk per notelist_notes(folder?, limit=50), sorted by modified timeget_recent(folder?, limit=20), recently changedget_tags(limit=50), tag and countget_vault_guide(), the Obsidian primer plus this vault'sCLAUDE.md, served live
Read and write
read_note(path, section?, offset=0, limit?)returns a structured result —path,title,tags,frontmatter_yamland a JSONfrontmatterview,heading(section reads),content, and truncation as data (truncated,offset,next_offset,total_chars,outline,notice). Bounded byMAX_READ_RESPONSE_CHARS(default 40,000) — see Response size limits.section=<heading>returns one section's body instead of the whole note;offsetcontinues a truncated read.create_note(path, content), atomic write, refuses overwriteedit_note(path, …)with four mutually exclusive modes: full replace (default),append=True,find=…(with optionalreplace_all), orsection=<heading>(ATX headings, supportsParent/Childpath-style and#Nordinal disambiguation).dry_run=Truereturns a unified diff without writing. Legacy clients may useoperation="append";operation="replace"explicitly selects full replace.move_note(from_path, to_path, rewrite_links=False), relocates and optionally rewrites incoming[[Old]],[[Old|alias]],[[Old#anchor]],![[Old]], and[[folder/Old]]references in source notesdelete_note(path, permanent=False), soft-delete to.trash/<YYYYMMDD-HHMMSS>-<basename>-<8 hex>by default, via a single non-replacing rename, so it never overwrites an existing trash entry (a filesystem that cannot do that rename makes the soft delete refuse with a named error rather than fall back).permanent=Trueunlinks.set_frontmatter(path, updates, remove?), structured YAML mutation. Body is byte-identical when only frontmatter changes.
File access (non-markdown)
Raw read/write/browse of arbitrary vault files (PDFs, images, skill assets, data files) — distinct peers to the note tools, which stay markdown-only. Pure byte transport: no server-side PDF/text extraction, no embedding or indexing of non-markdown files.
read_file(path, encoding="auto", offset=0, limit?), returns text-like files as text, images as an inline image block that renders in-client, and other binaries as a base64 string.text/base64force the form. Refuses files overMAX_FILE_READ_BYTES(default 10 MB); text results are additionally bounded byMAX_READ_RESPONSE_CHARSand continue viaoffset.write_file(path, content, encoding="base64", overwrite=False), lands a file in the vault; base64 for binary,textfor UTF-8. No-clobber by default, auto-creates parent dirs, atomic write. Capped atMAX_FILE_WRITE_BYTES(default 25 MB).list_files(folder=".", pattern="*", recursive=False, limit=200),ls-style browse of files and subdirectories with size and mtime, glob-filterable and result-capped.delete_file(path, permanent=False), soft-deletes a non-markdown file to.trash/<YYYYMMDD-HHMMSS>-<basename>-<8 hex>with a single atomic rename. Refuses markdown (that isdelete_note), directories, and symlinks.
All four reuse the path-traversal guard and exclude any path with a
component starting with . (dot-directories and dot-files)
(.obsidian, .git, .trash, …), matching the indexer's visibility
rule.
File transfer
No MCP client can hand a tool the bytes of a file the user is looking
at, so write_file is only usable when the agent already has the
content. These tools close that gap with short-lived capability links,
redeemed over the public /transfer/* routes.
request_upload(path, overwrite=False, expires_in?), mints a single-use link bound to exactly one destination path. The human opens it, picks a file, and it lands atpath— nothing else can be written with it.check_upload(upload_id), reportspending/uploading/completed(with path, size, sha256 and MIME) /unknown(a stream started and the server never recorded how it ended — read the path before re-minting) /revoked(the credential or vault root changed under the link) /expired, scoped to the identity that minted it.request_download(path, expires_in?), mints a link the human can save one vault file from. Usable more than once until it expires, and bound to the file's exact bytes at mint time.import_from_url(url, path, overwrite=False), fetches a public https asset straight into the vault under an explicit outbound deny policy (no private, loopback, link-local, metadata or tunnelled addresses, in any spelling, re-checked at every redirect).
The token travels in the URL fragment, which browsers never send, so
no server-generated request target or access log contains it. Uploads
are claimed before a body byte is read, published atomically with
no-clobber semantics, and bound at mint time to the file state they
were minted against — a link cannot silently undo an edit made while it
was waiting. MCP_HOSTNAME or BASE_URL must be set; without a public
origin the mint tools refuse rather than emit a localhost link.
Wikilink graph
get_backlinks(path, limit=50), notes linking TOpathget_links(path), outgoing links, both resolved and danglingget_neighborhood(path, depth=1, limit=50), undirected BFS over the resolved-link graph, capped at depth ≤ 5 and limit ≤ 200find_related(path, limit=10), semantic neighbors via averaged chunk embeddings and pgvector cosine distance, deduped per notefind_orphans(folder?, limit=50), notes with zero in or out resolved links
Auth and ops
API keys with the
omcp_prefix, stored as SHA-256 hashes, withreadandreadwritepermission scopes. Write tools refuse on read-only keys.OAuth 2.0 PKCE (S256) flow for public and confidential clients, including ChatGPT, Claude Desktop, and claude.ai. Dynamic registration defaults to both vault permission levels; the user chooses the actual grant on the consent screen.
Control panel (Jinja2, htmx, Tailwind) for keys, usage logs, indexer status, embedding-provider info, and a danger-zone reset.
Every tool call is logged to
usage_logswith name, params (truncated to 200 chars), duration, response size, and the calling credential's name — recorded at call time, so the audit trail survives deleting the key or OAuth client it describes./healthis unauthenticated and returnsstatusplus two capability fields:transfer_mount_check_available(the kernel supports the mount check transfer writes need) andvault_named_staging_fallback_active(a write has actually staged under a name on this process).
Every write — note tools, write_file, uploads and imports — stages
the new bytes in a temporary inode, fsyncs them, and only then
publishes. Creation publishes with a kernel-atomic hard link that
refuses to clobber; move_note and the soft delete publish with a
single non-replacing rename; an overwrite is a same-directory rename
onto the destination. The destination directory (and any directory the
call created) is fsynced afterwards, so a crash mid-write can neither
truncate a note nor lose one the server reported as written.
Staging happens in an unnamed inode wherever the filesystem supports
one, so no temporary name is ever visible in the vault. On a mount that
refuses that (some NFS exports do), those writes refuse with an error
naming VAULT_ALLOW_NAMED_STAGING_FALLBACK; setting that flag takes
named staging back on both write paths as a declared, weaker guarantee.
See System requirements.
vs. other Obsidian MCP servers
There are several existing MCP servers for Obsidian, and most of them solve a different problem than this one. The lightweight ones are glue over Obsidian's Local REST API plugin or the filesystem: they let an agent reach the files, but don't build any infrastructure of their own. They're great if "I just want Claude to read my notes" is the goal and you keep Obsidian running locally.
This server is on the other end of the spectrum: a real backend with a persistent index, semantic retrieval, a wikilink graph, OAuth, and an admin UI. The cost is Postgres and Docker. The benefit is everything you can build on top of that.
This server | ||||
Persistent index (Postgres) | ✅ | — | — | — |
Semantic search (vectors) | ✅ | — | — | — |
Wikilink graph queries | ✅ | — | — | partial |
Runs without Obsidian open | ✅ | — | ✅ | — |
OAuth 2.0 client flow | ✅ | — | — | — |
Multi-user / per-user vaults | ✅ | — | — | — |
Admin UI + usage logs | ✅ | — | — | — |
Atomic writes + dry-run diffs | ✅ | — | — | — |
Setup tax | Postgres + Docker | Obsidian + REST plugin | Python only | Obsidian plugin |
Comparison reflects each project's documented features at time of writing; verify the specifics before betting on them.
vs. hosted memory systems
The comparison that matters more, now that most of my vault traffic is agents rather than me, is against memory as a service: your agent calls an API, the service stores what it's told, and it hands back what it judges relevant later. mem0, Zep and Letta are the names people usually reach for. What follows is about that architecture — memory behind a service boundary — not about any one product's current feature list, which moves faster than a README can track.
The difference is where the memory lives and who can open it.
Readability. When memory sits behind a service API, reading it means whatever endpoint or console the service exposes, in whatever shape it stores. Here the memory is the artifact:
Health/2026-08 - Trusting expertise.md, in a folder, in your editor, ingrep. There's no gap between what the agent stored and what you can look at.Shared with you, and between agents. A memory service is generally scoped to an application and its users; the human's own writing is a different system. Here it's one corpus. I write into it by hand, and every connected client — Claude Desktop, Claude Code, Claude on the phone, an n8n workflow — reads and writes the same files on the same terms. A note I type on Sunday is context for an agent on Monday with no import step.
Portability. The exit path from a folder of markdown is
cp -r. No export format, no migration script, no question about what you'd be left holding if a project stopped being maintained. That's a property of files, not something this server does for you.Self-description. The rules live in the corpus rather than in client config.
CLAUDE.mdat the vault root tells every agent, on its first call, where things go and what frontmatter they carry, so conventions are versioned next to the notes they govern.
What the hosted shape buys you in exchange is real, and worth saying plainly. There's no Postgres to run, no pgvector version to keep current, no container to babysit — you get a memory layer by adding a dependency, which is a genuinely better trade for most people. And systems in that class typically do work this server deliberately doesn't attempt: pulling facts out of a conversation automatically, reconciling ones that contradict each other, and scoring relevance or decaying old memories so they stop crowding out new ones. Here an agent remembers something because it decided to write a note, and the judgment about what's worth keeping is the agent's, not the server's. If you want memory that curates itself, that's a fair reason to pick the other shape.
vs. an agent with raw file access
The other baseline isn't an MCP server at all: point Claude Code, a
generic filesystem MCP, or any agent with file tools straight at the
vault folder. That works — until a write goes wrong. An agent
rewriting a whole file from its memory of an earlier read will
eventually clobber a note, follow a symlink somewhere it shouldn't,
or "tidy up" your .obsidian config. Nothing in a raw file API
pushes back. This server's write path is shaped by exactly that kind
of incident, and it assumes the caller will eventually do something
wrong:
Targeted edits instead of rewrites.
edit_notecan address a find-string or a single section rather than replacing the file, anddry_run=Truereturns the unified diff before anything lands.set_frontmattermutates YAML structurally and leaves the body byte-identical.No-clobber defaults.
create_noteandwrite_filerefuse to overwrite an existing file; replacing one is an explicit opt-in.Atomic writes. Content is staged and renamed into place against a descriptor opened at validation time — a note is never left half-written, and the file that gets replaced is the file that was checked.
Reversible deletes.
delete_noteanddelete_filesoft-delete into.trash/with a non-replacing rename;permanent=Trueis the explicit escape hatch, not the default.Kernel-proved containment. Paths resolve under the vault root via
openat2(RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS | RESOLVE_NO_MAGICLINKS), writes refuse a symlink as the final component, and dot-directories (.obsidian,.git,.trash) are out of reach of every tool.Bounded responses. Reads are capped and truncation is data (
truncated,next_offset, an outline) rather than silent loss, so one huge note can't flood an agent's context into a bad edit.An audit trail. Every call is attributed to a key and logged; the control panel shows who touched what, and when.
When an agent misbehaves through this server you get a refused call,
a diff, a trash entry, and a usage-log line. When it misbehaves with
raw file access you get whatever git diff can recover — if the
vault was in git at all.
Who this is for
Homelab folks who already run Postgres and Docker, or are happy to spin them up. The setup tax is the price of admission for the semantic and graph layers.
People who keep an opinionated vault — task placement logic, frontmatter schemas, tag taxonomy — and want agents to follow those conventions on the first call instead of being briefed every session.
Anyone running more than one MCP client (Claude Desktop, Claude Code, Claude in a browser, n8n) against the same notes and tired of re-explaining the vault to each.
Folks who want agent memory to live as plain markdown files they can read, edit, grep, and version-control, not in an opaque vector store or a managed memory service.
Who this isn't for
"I just want Claude to read my notes" with the lightest possible setup. Use one of the filesystem-glue projects above; you don't need this.
Anyone unwilling to run a database. There is no SQLite fallback; pgvector is doing real work, and a managed Postgres with pgvector support is part of the stack.
People who want a turnkey hosted product. This is a self-hosted server you run yourself.
Control panel
The server ships with a built-in admin UI for the parts of operations that are easier to look at than to query: minting keys, watching the indexer, eyeballing tool-call traffic, and resetting embeddings when you switch providers.
Usage
Per-tool-call audit log with a 14-day request histogram. Every MCP call is recorded with the calling key, tool name, duration, and response size — useful for noticing a misbehaving agent burning tokens on something it shouldn't.

API keys and OAuth clients
Bearer keys with read / readwrite scopes for API clients, and a
separate OAuth 2.0 PKCE flow for clients like ChatGPT, Claude Desktop,
and claude.ai that expect a proper authorization-code dance. The OAuth
server supports public (none) and confidential (client_secret_post)
token-endpoint authentication plus refresh tokens.
Each client's page lists its grants — one row per /authorize approval,
not per token — with a Revoke control and a permission select per grant,
so revoking really ends the session instead of leaving a refresh token
to mint a replacement. Revoked and expired rows stay listed, dimmed, for
a week.

Vault browser
A read-only file tree of the mounted vault, mostly for sanity-checking that the container sees what you think it sees.

Settings
Indexer status, current embedding provider and model, vault path, and the danger zone: Reset embeddings (drops and recreates the embeddings column at the configured dimension — use it when switching providers) and Force re-embed (keeps the column, clears every note's embedded-content hash so the next pass re-embeds the vault). Both pause the indexer while they run.
The dashboard separates two things that used to be conflated: Last
run is the indexer's own heartbeat — the last pass that completed,
whether or not anything had changed — and Last change detected is
the newest indexed_at on any note. A quiet vault makes the second one
old while the indexer is perfectly healthy.

Quick start
Deploying on a VPS from scratch? See
DEPLOYMENT.mdfor the full walkthrough: Postgres setup, Caddy and TLS, vault sync via Nextcloud, and the gotchas that bite first-time deploys.
The bundled Caddy configuration fails closed on /admin, /api, and
/authorize; replace its placeholder basic-auth hash before starting it.
Prerequisites
Docker and Docker Compose
A PostgreSQL 16 instance reachable from the container, with
pgvector0.8.0 or newer installedEither an Ollama instance running
bge-m3, or an OpenAI API key. Anything that speaks the OpenAI embeddings protocol works (Azure OpenAI, OpenRouter, Together, etc.).Linux, kernel 5.6 or newer (see below)
System requirements
The server checks these at startup and tells you which one failed rather than misbehaving later.
Linux kernel ≥ 5.6. Every directory below the vault root is opened
with a single openat2(RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS | RESOLVE_NO_MAGICLINKS), which is what makes the kernel — not the
application — prove that a write stayed inside the vault. There is no
fallback: on an older kernel, or under a container seccomp profile that
blocks openat2, the server logs the reason and exits non-zero.
Kernel ≥ 5.8 for file transfer. statx()'s STATX_MNT_ID is how a
publication refuses a destination that sits on a different mount than
the staging directory (a nested bind mount under the vault root would
otherwise fail only after a whole upload body had streamed). Below 5.8
the server logs one warning and starts: request_upload,
import_from_url and PUT /transfer/upload refuse, and everything else
— reads, note writes, search, downloads, the panel, OAuth — is
unaffected. /health reports it as transfer_mount_check_available.
pgvector ≥ 0.8.0. Filtered semantic search needs
hnsw.iterative_scan, which landed in 0.8.0. An older extension accepts
the setting as an unknown placeholder and silently runs a plan that
drops post-filter candidates — silently worse search results — so the
server exits instead. Fix with ALTER EXTENSION vector UPDATE or a
newer database image.
Filesystem. Case-sensitive and non-normalising (ext4, xfs, and the
usual bind mounts). It must support hard links within the vault root and
renameat2(RENAME_NOREPLACE); without those, note creation, move_note
and the soft delete refuse with a named error rather than degrading to a
publish that can clobber. O_TMPFILE is wanted but optional: where it
is unavailable, set VAULT_ALLOW_NAMED_STAGING_FALLBACK=true to accept
named staging instead (see Configuration). macOS and
Windows hosts are out of scope; run the container on a Linux VM.
1. Clone, configure, point at your vault
git clone https://github.com/maxkuminov/obsidian-mcp.git
cd obsidian-mcp
cp .env.example .env
$EDITOR .envIn docker-compose.yml, point the /obsidian volume at your vault:
volumes:
- /path/to/your/vault:/obsidian2. Pick an embedding backend
Option A, OpenAI (zero local infra):
EMBEDDING_PROVIDER=openai
OPENAI_API_KEY=sk-...
EMBEDDING_DIMENSIONS=1024
OPENAI_EMBEDDING_MODEL=text-embedding-3-smallThe server validates OPENAI_API_KEY at startup and refuses to boot
if it's missing.
Option B, Ollama (self-hosted, GPU recommended):
EMBEDDING_PROVIDER=ollama
OLLAMA_URL=http://your-ollama-host:11434
EMBEDDING_MODEL=bge-m3
EMBEDDING_DIMENSIONS=1024This is the default. Omitting EMBEDDING_PROVIDER falls back to
Ollama.
3. Deploy
make init # data dirs and .env from template (skip if you've already edited)
make db-init # create database, user, and pgvector extension
make deploy # build, push to local registry, run migrations, recreate containerThe first deploy backfills the index, the wikilink graph, and the
embeddings. For a 2 to 3k-note vault on Ollama with a GPU this takes
a few minutes. On text-embedding-3-small it's seconds.
4. Connect a client
Mint an API key in the control panel, then point your MCP client at:
URL: https://obsidian-mcp.<your-domain>/mcp
Auth: Bearer omcp_...For Claude Desktop, add to claude_desktop_config.json:
{
"mcpServers": {
"obsidian": {
"url": "https://obsidian-mcp.<your-domain>/mcp",
"headers": { "Authorization": "Bearer omcp_..." }
}
}
}For Claude Code:
claude mcp add obsidian --transport http \
--url "https://obsidian-mcp.<your-domain>/mcp" \
--header "Authorization: Bearer omcp_..."The first thing any agent should do in a new session is call
get_vault_guide(). That's how it learns your folder structure,
naming conventions, and YAML schema before it writes anything.
Cost expectations
If you go the OpenAI route (the realistic path on a CPU-only VPS), the first-index spend is small and the steady state is nearly free. Rough numbers assuming an average note around 1,500 tokens (three 512-token chunks), at OpenAI's published rate at time of writing:
Model | $/1M tokens | 1k notes | 10k notes | 100k notes |
| $0.02 | ~$0.05 | ~$0.50 | ~$5.00 |
| $0.13 | ~$0.30 | ~$3.00 | ~$30.00 |
After the first index, only changed notes are re-embedded. Ongoing cost is proportional to edits — pennies a month for a typical vault.
If you self-host Ollama with a GPU, embedding cost is whatever your power bill is. Ollama on CPU works but is too slow to be usable on a vault of more than a few hundred notes.
The self-describing vault
This is the part most "MCP for Obsidian" projects miss. They stop at read, write, and list. The interesting question isn't "can the agent reach the files," it's "does the agent know the rules?"
If you have an opinionated vault — task placement logic, folder conventions, required frontmatter, tag taxonomy — an agent with write access can do real damage without that context. Tasks land in the wrong folder. Bare-date filenames collide with templates. Wrong tags break Dataview queries. The data layer works fine; the context layer is where the failures show up.
The fix is small. Keep a machine-readable instruction file
(CLAUDE.md at the vault root) that describes the system's own rules.
Expose it as a dedicated tool. Every connecting agent calls it once at
the start of a session and immediately knows how the vault works.
Update the file, every agent sees the change on the next call. No
client-side config. No system-prompt injection. The vault is
authoritative about its own rules.
get_vault_guide() does exactly this. It returns a generic Obsidian
primer (wikilink syntax, embed syntax, tag conventions, common plugin
literals) plus the vault's CLAUDE.md live. The hint to call it first
is baked into the write-tool descriptions so the agent gets pulled
into the right behavior even without prompting.
Multi-user mode
Single-user mode is the default and works exactly as described above — one vault, one set of API keys, no in-app user concept. Multi-user mode is an opt-in flag that turns the same container into a small multi-tenant deployment: in-app username/password login, per-user vault scoping, an admin role for troubleshooting, and a regular-user role that sees only its own keys/OAuth clients/usage. One container, one Postgres, strict isolation between users.
Enable it on an existing deployment with no data loss — your current vault and keys carry over to the bootstrap admin.
Enabling
Set
MULTI_USER_MODE=trueand a strongSECRET_KEYin.env(openssl rand -hex 32is fine). The app refuses to start with a placeholderSECRET_KEYunconditionally — single-user mode included — so this is not something the flag turns on.make deploy(ordocker compose up -d --force-recreate).Visit the panel. Because the
userstable is empty, you're routed to/admin/register— the one-time bootstrap form. It's still behind Traefik'schain-oauth@filemiddleware, so only people Traefik already trusts can claim admin.Register with a chosen username and password. The bootstrap form pre-fills
vault_pathwith whateverVAULT_PATHwas set to, so your existing notes immediately belong to this new admin. No re-index, no re-embed, no data loss — every previously indexed note, API key, OAuth client, and usage log row gets backfilled to the bootstrap user in a single transaction.
Inviting users
Edit
docker-compose.ymlto add a volume mount for the new user's vault under/vaults/<username>. Host paths with spaces must be quoted as a single YAML string:volumes: - "/storage/vaults/alice:/vaults/alice" - "/storage/shared/bob/Obsidian:/vaults/bob"make deployto apply.In the panel,
/admin/users/create— pick a username and set an initial password./admin/users/{id}/edit— set the user'svault_pathto the container path you just mounted (e.g./vaults/bob). The form shows a dropdown of unassigned/vaults/*directories that exist on disk.Share the credentials out-of-band. The user logs in at
/admin/auth/login, gets their own keys/OAuth/usage views, and cannot see other users' notes.
What admins see
Admins see API keys, OAuth clients, and usage logs for all users; they
own the Settings page (embedding provider, indexer trigger, danger
zone) and the Users page. Admins do not browse other users' vault
contents through the panel — that's intentional. Troubleshooting
another user's vault means either inspecting it via docker exec or
temporarily reassigning their vault_path, not UI snooping.
Rolling back
Set MULTI_USER_MODE=false, restart. Existing API keys keep working
(per-user filters skip when no user context is set), the login UI and
session cookies disappear, and the panel falls back to its
Traefik-OAuth-only mode. The schema stays in place, so flipping back
to multi-user later resumes where you left off without re-bootstrapping
(the users table is non-empty, so /admin/register is closed).
Constraints and known limits
The indexer iterates active users sequentially each cycle. Fine for tens of users; hundreds would need parallelization.
Password reset is admin-driven only — there's no email-based self-service flow.
No rate limiting on
/admin/auth/login. The Traefik OAuth gate in front of the panel is the main brute-force defense; if you expose/admin/auth/loginto the open internet, put a rate-limit middleware in front of it.The
vault_pathvalidator does not resolve symlinks, so an admin can technically point a user at host files via a symlinked/vaults/<name>. Treat/vaults/as an admin-trust boundary.
Configuration
Variable | Default | Purpose |
| — |
|
|
| In-container vault mount |
| — | itsdangerous signer key |
|
| Periodic reindex cadence |
|
| In-app login, per-user vaults. See Multi-user mode. |
| — | Public hostname. Derives |
| derived | Explicit public origin. HTTPS except on loopback. |
| derived | CORS origins, JSON list |
| derived | Accepted |
|
| Panel session cookie lifetime, seconds (multi-user mode) |
|
| Panel session cookie name |
|
|
|
|
|
|
|
|
|
|
| Keyword-search text-search config(s). JSON or CSV. See Full-text search language(s). |
|
| Default life of a transfer link. Per-call |
|
| How long one claimed upload may stream before the token is spent |
|
| Simultaneous upload streams |
|
| Let |
|
| Accept named staging on filesystems without |
|
|
|
|
| pgvector column width |
| — | Used when provider is Ollama |
|
| Ollama model name |
|
| How long Ollama keeps the model resident. |
| — | Required when provider is OpenAI |
|
| Override for Azure or proxies |
|
| OpenAI model |
|
| Approx tokens per chunk (4-char heuristic) |
|
| Token overlap between chunks |
|
| Globs skipped by the embedder. Excluded files stay keyword-searchable. |
|
| Registry-eval only. Skips DB, indexer, embedding provider, and |
See .env.example for the full set with comments. For first-index
spend on OpenAI, see Cost expectations above.
The MCP transport's request-body limit is derived, not configured:
max(2 × MAX_FILE_WRITE_BYTES, 6 × 10 MB) + 1 MiB, which is 61 MiB with
the defaults. It has to track the write caps so that every supported
write is refused by the tool — with an actionable message — rather than
by the transport with a bare HTTP 413. Raise MAX_FILE_WRITE_BYTES and
the transport limit follows.
Switching providers
Different models produce non-comparable vectors, so a provider switch requires reindexing.
Whether or not EMBEDDING_DIMENSIONS changes, the steps are the same:
Update
.env—EMBEDDING_PROVIDER, credentials, model name, andEMBEDDING_DIMENSIONSif it differs.make reset-embeddings. The target isdocker compose run --rm, so it starts a one-off container that reads your edited.env— it works whether the service is up or down, and recreates the column at the new dimension.make deploy(ordocker compose up -d --force-recreate). The next indexer pass re-embeds the vault.
When changing EMBEDDING_DIMENSIONS, run the reset before
recreating the container: the startup dimension guard compares the live
column width against EMBEDDING_DIMENSIONS and sys.exit(1)s on a
mismatch, so a recreated-first container just exits until the reset has
run.
You can also use Settings → Danger zone → Reset embeddings in the control panel, which performs the same SQL while the server is running (pauses the indexer, runs the SQL, resumes).
If you change EMBEDDING_DIMENSIONS without running the reset, the
server detects the mismatch at startup and exits non-zero with a
pointer to the reset target.
Full-text search language(s)
keyword_search runs over a PostgreSQL tsvector. The text-search
configuration it uses — the stemmer and stop-word dictionary — is
controlled by FTS_CONFIGS. It defaults to english, which reproduces
the historical behavior exactly, so existing deployments need no action.
FTS_CONFIGS is a list, settable as JSON
(FTS_CONFIGS=["simple","norwegian"]) or comma-separated
(FTS_CONFIGS=simple,norwegian). Each note is indexed under every
listed config, and a query matches if any listed config's parse hits.
This is what makes a mixed-language vault work:
| Behavior |
| English Snowball stemmer (default; |
| Language-agnostic. No stemming or stop-words — matches exact word forms. A principled default for mixed-language vaults: keyword search is the exact-match arm, while |
| Both stemmers applied — keyword-side morphology for two languages at once. |
| Verbatim lexemes plus Norwegian stems. |
The setting is global — applied to every vault (consistent with
EMBEDDING_MODEL, CHUNK_SIZE, etc., which are global too). For a
mixed-language multi-user instance, set a superset (e.g.
["english","norwegian"], or ["simple"]). Per-user FTS config is a
clean future extension but is not implemented.
A typo'd or uninstalled config name fails fast at startup with a message listing the configs available in your Postgres instance, rather than producing silent zero-result searches.
Changing FTS_CONFIGS requires a rebuild. Stored tsvectors are
computed at index time, so they go stale when the config list changes.
After editing .env and redeploying, run:
make rebuild-tsvectorsThis re-reads each note and recomputes its content_tsvector under the
new config(s). It rebuilds the keyword index only — it does not
touch embeddings/vectors and makes no API calls, so it finishes in
seconds for a few thousand notes. (Do not confuse it with the expensive
make reset-embeddings flow.)
Tokenization caveat: the tsvector parser still splits on punctuation and hyphens regardless of config, so
bge-m3tokenizes tobge+m3.simplepreserves word forms, not punctuation-bearing strings; exact-string-with-punctuation matching would need a trigram index and is out of scope.
Response size limits
A tool result is model input. Whatever read_note returns is fed
straight back into the caller's next request, so an unbounded read is
an unbounded prompt — and the caller usually finds out only when its
inference provider rejects the request.
MAX_READ_RESPONSE_CHARS (default 40,000, roughly 10K tokens) bounds
what read_note and the text results of read_file return. It is a
different limit from MAX_FILE_READ_BYTES, which bounds what the
server reads off disk. A 3 MB note is comfortably within the 10 MB read
cap and will still destroy a context window; both caps are needed and
they have different correct values.
It applies per component, not once to the whole response: the
content window gets the cap, the heading outline gets it
independently, and the metadata fields (title, tags,
frontmatter_yaml and its JSON view, heading) share a third. A
truncated read can carry all three, so budget for a worst case of
roughly 3 × MAX_READ_RESPONSE_CHARS plus fixed prose — doubled again
because the MCP result carries both structured content and a JSON text
block, and multiplied by JSON escaping for content that is mostly
control characters.
When a note exceeds the cap you get the first window plus truncation as
data — truncated, the next_offset to continue from, total_chars —
and, for a whole-note read, an outline of the note's sections:
{"entries": [
{"ordinal": 1, "depth": 1, "text": "Client Records",
"size": 2855343, "exceeds_cap": true, "duplicate": false},
{"ordinal": 2, "depth": 2, "text": "Balance Sheet.xlsx",
"size": 391199, "exceeds_cap": true, "duplicate": false},
{"ordinal": 3, "depth": 2, "text": "Lease Agreement.pdf",
"size": 464, "exceeds_cap": false, "duplicate": false},
{"ordinal": 4, "depth": 2, "text": "Invoice 2025-044.pdf",
"size": 1075, "exceeds_cap": false, "duplicate": true}
], "truncated": false}Paging a multi-megabyte note 40K at a time is technically possible and
practically useless, so prefer the outline: read the one section you
want with read_note(path, section="Lease Agreement.pdf"). Sections are
addressable three ways — the #N ordinal shown in the outline, the
Parent/Child path-style form, and exact heading text. The ordinal is
the only form that separates duplicate sibling headings, which share
every ancestor and so cannot be disambiguated by path; notes generated
by bulk extraction tend to be full of them.
A bare #N always selects by position, so an ordinal we hand you in
an outline can never be shadowed by a heading that happens to be titled
#2. Such a heading stays reachable via the path form (Parent/#2) or
via its own ordinal.
The outline is itself bounded by the cap: a note with thousands of
headings gets a truncated listing that reports how many sections were
omitted (omitted) and the full ordinal range (first_ordinal,
last_ordinal), rather than an outline larger than the content window
it accompanies. Metadata that does not fit its budget is dropped whole
and reported in metadata_omissions — never cut short and never marked
inside the field itself, so nothing in a note-controlled field is ever
a prefix or server prose. frontmatter_yaml is the frontmatter block's
YAML source with the fence lines removed, LF-normalized (the same
declared terminator residual content carries); it is the authoritative
copy, and the frontmatter JSON view beside it is a convenience that is
omitted, with a reason, when YAML holds something JSON cannot say.
limit can lower the cap for a single call but never raise it. If your
clients genuinely want larger reads, raise MAX_READ_RESPONSE_CHARS —
that is an operator decision, made once, by someone who knows the
deployment.
Upgrading: two visible contract changes, in two releases.
read_noteon a large note used to return the whole thing; it now truncates. The response is self-describing, so an agent needs no prior knowledge to continue, but a script that assumed whole-note reads should either passsection=or raise the cap.And
read_noteused to return one rendered string — a# <title>/**Path:**header, a\n---\nseparator, then the content. It now returns fields, because every component of that header was note-controlled: a note could forge the separator, so an agent recovering the section body by splitting the response could recover a crafted string and write it back over the section. A client that parsed the old envelope must readcontent(and, for section reads,heading) instead; clients that ignorestructuredContentstill get an unambiguous JSON text block.
Architecture
┌──────────────┐ ┌──────────────────────┐
│ MCP clients │ HTTP + Bearer key │ FastAPI app │
│ Claude Desk │ ────────────────────▶ │ ┌────────────────┐ │
│ Claude Code │ │ │ MCP server │ │
│ n8n agents │ │ │ (25 tools) │ │
│ OpenWebUI │ │ └─────┬──────────┘ │
└──────────────┘ │ ▼ │
│ ┌────────────────┐ │
│ │ Services: │ │
│ │ - vault │ │
│ │ - search │ │
│ │ - embeddings │ │
│ │ - links │ │
│ │ - indexer │ │
│ └─────┬──────────┘ │
│ ▼ │
│ ┌────────────────┐ │
│ │ Postgres + │ │
│ │ pgvector │ │
│ └────────────────┘ │
└──────────┬───────────┘
▼
┌────────────────────┐
│ Embedding │
│ provider │
│ (Ollama / OpenAI) │
└────────────────────┘Indexing pipeline
.md files in vault
↓ skip dot-dirs
parse frontmatter, extract tags (YAML + inline #hashtags)
↓ SHA-256 hash
skip if unchanged
↓
UPSERT notes_metadata (path, title, tags[], frontmatter JSONB,
content_hash, tsvector, modified_at)
↓
extract wikilinks/embeds/markdown-links → resolve targets →
note_links (source_id, target_id or NULL for dangling)
↓
chunk content (512 tokens, no overlap) → embed via provider →
note_embeddings (note_id, chunk_index, chunk_text, embedding[N])
↓
set embedded_content_hash = content_hashThe indexer runs on startup and every INDEX_INTERVAL_SECONDS (5
minutes by default). Hashes are content-only, so the change detector
ignores mtime jitter. Stale embeddings are caught by the
embedded_content_hash != content_hash mismatch.
Database schema
Table | Purpose |
| Path, title, tags, frontmatter, content hash, embedded hash, tsvector, modified time |
| One row per chunk. |
| Wikilink graph: source/target IDs, target_path, kind ( |
| Hashed bearer tokens, prefix for display, permission, expiry |
| Per-tool-call audit |
| OAuth 2.0 PKCE state, including the grant id that ties a consent's tokens together |
| Capability rows behind the |
| Multi-user mode: login, role, per-user |
GIN indexes on content_tsvector and tags[]. B-tree indexes on the
hot foreign keys. pgvector HNSW index on the embedding column
(vector_cosine_ops, m=16, ef_construction=64); queries set
hnsw.ef_search=80 and dedupe per note in Python after a 5x overfetch.
Project layout
src/
main.py FastAPI app, lifespan, MCP mount
config.py pydantic-settings
database.py async SQLAlchemy engine/session
models/db.py ORM models
mcp_server/ MCP server, tools, auth middleware
services/ vault ops, anchored filesystem, search, FTS,
embeddings, links, indexer, transfer
transfer/ public /transfer/* capability-redemption routes
auth/ login, sessions, per-request identity context
api/ control-panel REST endpoints
control_panel/ Jinja2 templates and static assets
oauth/ OAuth 2.0 authorization-code flow
alembic/ database migrations
scripts/ one-off ops scripts (e.g. reset_embeddings.py)
tests/ pytest suite + smoke-test docs
openspec/ change proposals (spec-driven workflow)Development
pip install -r requirements-dev.txt
pytestThe unit-test suite covers the embedding-provider abstraction, OpenAI
batching and retry behavior, config validation, and the
dimension-mismatch startup check. Network-bound tests use respx to
mock httpx, so no real network access is required.
To run the server outside Docker:
DATABASE_URL=... SECRET_KEY=... VAULT_PATH=... uvicorn src.main:app --reloadMake targets
make init First-time setup (data dirs, .env)
make build Build Docker image (no cache)
make build-cached Build Docker image (with cache)
make push Push the image to the configured registry
make image Build and push
make deploy Build, scan, push, backup, migrate, recreate container
make up / down / restart / shell Container lifecycle
make logs Tail container logs
make db-init Create database, user, and pgvector extension
make db-migrate Run alembic migrations
make db-check alembic check — schema vs. ORM models (must be clean)
make test-schema Schema gate: migrations vs. models on a throwaway pgvector container
make db-backup Dump database to backups dir
make db-restore FILE=<path> Restore from a backup
make reindex Explain how to trigger a reindex (panel only; there is no headless trigger)
make reset-embeddings Drop and recreate embedding column at configured dim
make rebuild-tsvectors Recompute keyword index for FTS_CONFIGS (no embeddings, no API calls)
make status Show container and health status
make audit Audit Python dependencies (pip-audit)
make trivy Scan the local image for HIGH/CRITICAL CVEs (SCAN_IMAGE=obsidian-mcp:local for the bundled stacks)
make clean Remove containers and images (data preserved)make deploy runs the whole pipeline: build, image scan, push, database
backup, alembic upgrade head, then recreate the container. Run
make test-schema before any deploy that carries a migration, and
make db-check after one.
Security notes
API keys use the
omcp_prefix and are stored as SHA-256 hashes. The raw key is shown exactly once at creation.The control panel is intended to sit behind an external auth gateway. The included
docker-compose.ymluses Traefik with an OAuth chain. Don't expose/admindirectly to the internet.The OpenAI key is rendered on the settings page as
key[:8] + "..." + key[-4:]and never appears in full in HTML or JS sources.Path traversal is blocked at the service layer, and containment is proved by the kernel: every directory below the vault root is opened with one
openat2(RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS | RESOLVE_NO_MAGICLINKS)from an open root descriptor, and the rest of the operation acts on that descriptor rather than re-walking a name.Mutating tools act on the path as named. A final component that is a symlink is refused (naming the link's target) instead of being followed, so an in-vault alias cannot redirect a write. Reads still follow links, which is what an alias is for.
Every path guard also refuses hidden components, so
.obsidian,.git,.trashand friends are out of reach of every tool.Transfer links carry their token in the URL fragment, which browsers never send, and are redeemed only from an
Authorization: Bearerheader. Keep header logging off at your reverse proxy and APM. Unknown, expired, consumed and revoked tokens all get one identical 404 from the public routes; precise status comes from the authenticatedcheck_uploadtool.import_from_urlfetches only genuinely public addresses, under an explicit deny list re-applied at every redirect.Parameterized queries everywhere. No string interpolation into SQL.
Response headers include HSTS,
X-Content-Type-Options: nosniff, andX-Frame-Options: DENY.
Status
Single-author, in active use as the maintainer's personal exocortex (2,500+ notes, multiple connecting agents). Public for anyone who wants to fork it. Issues and PRs welcome but expect opinionated review. This is a working system, not a generic platform.
License
MIT. See LICENSE.
Available Tools
25 toolscheck_uploadA
Ask what happened to an upload link you minted with request_upload.
Returns one of pending (nothing sent yet), uploading (bytes are in
flight), completed (with the path, size, sha256 and MIME type of what
landed), unknown (a stream started and the server never recorded how it
ended), revoked (the link is dead because the credential or vault root
changed under it), or expired. Use it to confirm a transfer really
finished before you tell the user it did, and to get the sha256 if they
want to verify it.
Visibility is scoped to the principal that minted the link, not to one
credential row. For an API key that is the key itself. For OAuth it is the
whole grant family behind the access token you are calling with, so a handle
stays readable across the hourly token refresh that mints a new row. A
different API key, a different client, or a separate approval of the same
client reads as not found.
uploading names the deadline the stream has. Check again after it: past
that point the answer becomes either completed or unknown. unknown
does not mean nothing arrived — a publish can succeed and still fail to
record its completion — so read or list the path before minting another
link or telling anyone the file did not arrive.
Pass the upload_id itself — the short handle from request_upload, not
the upload URL and not the token after the #. Anything else is refused
without a lookup.
Args:
upload_id: The upload_id that request_upload returned.
| Name | Required | Description | Default |
|---|---|---|---|
| upload_id | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and thoroughly discloses edge behaviors: each status meaning, the `unknown` ambiguity (publish can succeed without recorded completion), the deadline after which status settles, principal-based visibility with OAuth grant families, and refusal of non-upload_id inputs without a lookup. It also warns to read/list the path before minting another link or reporting failure. No contradictory annotation exists.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Every paragraph addresses a distinct operational aspect—purpose, statuses, usage trigger, visibility, timeout semantics, and parameter guidance—without repeating schema or annotation information. The core purpose is front-loaded, and the length is justified by the tool's semantic complexity.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The description covers return statuses and their implications, principal scoping, expiration behavior, the unknown-state caveat, and exact input requirements. Together with the existence of an output schema, an agent has everything needed to call this tool correctly and interpret its result.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The schema provides only a bare string type with 0% coverage. The description compensates by defining the exact expected value ('the short handle from `request_upload`'), explicitly excluding the upload URL and the token after the `#`, and stating that anything else is refused without a lookup.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The first sentence states a specific action ('Ask what happened to an upload link you minted with `request_upload`') and enumerates the possible statuses, distinguishing this status-check tool from transfer-creation siblings like request_upload. It is unmistakable what resource it operates on and what it returns.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It explicitly instructs when to call it: 'Use it to confirm a transfer really finished before you tell the user it did, and to get the sha256 if they want to verify it.' It also clarifies the relationship to request_upload and that handles are scoped to the minting principal, which prevents misuse with other principals.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
create_noteA
Create a new markdown 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).
Refuses a path whose final component is a symlink, naming its target, so a write never lands on a note other than the one named; symlinked folders inside the vault work normally.
The note is published no-clobber: the content is staged out of sight and
linked into place in one kernel-atomic step, so an existing file at path
can never be replaced by this tool. A vault filesystem that cannot stage an
unnamed file refuses the write with an error naming
VAULT_ALLOW_NAMED_STAGING_FALLBACK rather than staging under a visible
name.
Args: path: Vault-relative path for the new note (e.g. "Cards/New Topic.md"). The .md extension is added if missing. content: Full markdown content for the note, including any frontmatter.
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | ||
| content | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full burden and covers side effects and failure modes thoroughly: write-permission requirements, symlink refusal on the final path component, atomic no-clobber publication, and the staging-fallback error naming VAULT_ALLOW_NAMED_STAGING_FALLBACK. An agent can accurately predict the tool's behavior.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The purpose is stated in the first sentence, and the content is organized into clear topical paragraphs. It is longer than strictly necessary due to detailed staging and fallback explanations, but those details are behaviorally relevant and no sentence is clearly wasted.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The description covers permissions, path semantics, symlink behavior, atomic no-clobber semantics, and direction to get_vault_guide for vault conventions — quite complete for a two-parameter tool. It does not describe the success response or the exact behavior when the destination file already exists, though an output schema may cover the former.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The schema has 0% description coverage, but the Args section fully compensates. Path gets vault-relative semantics, a concrete example, and the behavior of appending .md; content gets its markdown and frontmatter scope. Both required parameters are meaningfully explained.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with 'Create a new markdown note in the Obsidian vault', a specific verb, resource, and scope. It clearly indicates creation rather than modification through the word 'new' and the no-clobber guarantee, but it never explicitly contrasts itself with sibling tools like edit_note or write_file, leaving some differentiation to inference.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It clearly states the permission prerequisite ('readwrite' API key or OAuth scope) and points to get_vault_guide for vault-specific conventions, providing solid operating context. It does not explicitly enumerate when to prefer this tool over edit_note or write_file, though the no-clobber statement implicitly rules out updating existing files.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
delete_fileA
Delete a non-markdown file from the vault. Requires write permission — a readwrite API key, or an OAuth token
carrying the readwrite scope.
Peer to delete_note, which stays markdown-only.
By default this is a soft delete: the file moves to
.trash/<YYYYMMDD-HHMMSS>-<basename>-<8 hex> inside the vault, keeping a
copy the user can recover. Two files with the same name deleted in the same
second both survive — the trash never clobbers.
With permanent=True the file is unlinked outright and this server has no
recovery path; the user's backups are the only rollback.
Refuses markdown files (use delete_note, which understands the index and
backlinks), directories, and symlinks. Non-markdown files are not indexed,
so search and embeddings are unaffected either way.
Args:
path: Vault-relative path to the file.
permanent: If True, unlink instead of moving to .trash/.
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | ||
| permanent | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full behavioral burden and does so exceptionally. It explains the soft-delete trash path, the naming format that prevents clobbering, the irreversible nature of `permanent=True`, permission requirements, refusal behavior, and the fact that non-markdown files are not indexed.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is detailed but every sentence earns its place, with clear paragraph breaks for permissions, soft delete, permanent delete, and restrictions. It front-loads the core purpose and then layers essential safety and behavior information in a logical order.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a destructive operation with no annotations, the description covers permissions, side effects, recovery, refusal cases, parameter semantics, and indexing impact. The output schema covers return details, so nothing critical is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, but the description fully compensates by explaining both parameters. `path` is defined as vault-relative, and `permanent` is clarified with its behavioral consequence: unlink instead of moving to `.trash/`. This adds meaning far beyond the bare schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
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: 'Delete a non-markdown file from the vault.' It clearly distinguishes itself from the sibling `delete_note`, which 'stays markdown-only,' so an agent can pick the right tool without further investigation.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explicitly says when to use this tool vs alternatives: non-markdown files only, and it explicitly routes markdown files to `delete_note`. It also states refusal cases — markdown files, directories, symlinks — and the required write permission, leaving no ambiguity about applicability.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
delete_noteA
Delete a note from the vault. Requires write permission — a readwrite API key, or an OAuth token
carrying the readwrite scope.
By default this is a soft-delete: the file is moved to
.trash/<YYYYMMDD-HHMMSS>-<basename>-<8 hex> inside the vault root, by a
single non-replacing rename, so an existing trash entry is never
overwritten and two deletes in the same second land on distinct names. The
indexer skips dot-prefixed directories, so search and embeddings drop the
note automatically on the next reindex pass (≤ 5 minutes). Soft-deleted
files accumulate in .trash/ — emptying that directory is the user's
responsibility.
A vault filesystem that cannot perform that non-replacing rename into
.trash/ makes the soft delete refuse with an error naming the limitation
rather than fall back to a rename that could overwrite; pass
permanent=True to unlink instead.
With permanent=True, the file is unlinked directly with no recovery path
inside this server. Existing backups are the rollback story.
A path whose final component is a symlink is refused, naming its target, so a delete never removes a note other than the one named; symlinked folders inside the vault work normally.
Dangling backlinks left behind by a delete are surfaced via
get_backlinks and find_orphans. See get_vault_guide for context.
Args: path: Vault-relative path to the note. permanent: If True, unlink instead of soft-deleting.
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | ||
| permanent | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description fully carries the behavioral burden and does so exceptionally well. It discloses the trash path and naming scheme, the non-replacing rename guarantee, indexer behavior, reindex timing, accumulation in .trash, permanent unlink irreversibility, symlink refusal, and backlink consequences. This is a model of behavioral disclosure.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is lengthy but every sentence carries operational meaning. The core action is front-loaded, and the technical details about trash naming, rename semantics, symlink handling, and backlinks are all decision-relevant for an agent. No filler is present.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a mutation tool with two parameters and no annotations, the description covers prerequisites, failure modes, recoverability, edge cases, and post-delete effects. An output schema exists, so return-value detail is not required. The tool can be invoked correctly and safely from this description alone.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, but the Args section compensates completely. path is defined as a vault-relative path to the note, and permanent is defined as 'If True, unlink instead of soft-deleting.' Both parameters are fully explained beyond their schema types.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with a clear statement of action and resource: 'Delete a note from the vault.' The detail about soft-delete vs permanent unlink reinforces what the tool does. It does not explicitly differentiate itself from the sibling delete_file tool, so it falls just short of a 5.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description gives clear conditional guidance: use the default soft-delete unless permanent=True is desired, and notes that write permission is required. It does not explicitly explain when to choose delete_note over delete_file or move_note, but the context is strong enough for an agent to use it correctly.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
edit_noteA
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):
Full replace (default): provide only
content.contentbecomes the note's body; an existing valid line-1 YAML frontmatter block is preserved byte-identically ahead of it. Passreplace_frontmatter=Trueto overwrite the entire file, frontmatter included.Append:
append=True;contentis added at the end (preceded by a single newline).Find & replace:
find=<exact text>; replaced withcontent. Must match exactly once unlessreplace_all=True. This mode operates on the raw file, so it is the one mode that can edit frontmatter text in place.Section:
section=<heading>; replaces the whole body under the named ATX heading — see "Section mode: whatcontentreplaces" below. Use the path-style formParent/Childto disambiguate when the same heading appears more than once, or the#Nordinal 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 truncatedread_noteadvertises. 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 inread_noteas 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
contentis 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
contentdoes 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 theheadingfield and the body in thecontentfield, and this tool takes exactly thatcontent. 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=Truerepair;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 forappend=True. This is accepted to prevent older clients from silently falling through to full replacement.operation="replace"explicitly selects full replacement.replace_all=True: withfind, replace every occurrence rather than failing on multiple matches. Ignored whenfindis 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 optimistic,
not locked — the bytes this call read are compared against the file
immediately before that rename, so a note somebody else changed in the
meantime fails with File changed while editing: <name> and nothing is
written; re-read and retry. 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.
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.
| Name | Required | Description | Default |
|---|---|---|---|
| find | No | ||
| path | Yes | ||
| append | No | ||
| content | Yes | ||
| dry_run | No | ||
| section | No | ||
| operation | No | ||
| replace_all | No | ||
| replace_frontmatter | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full behavioral burden and does so thoroughly. It discloses write-permission requirements, frontmatter byte-identity preservation, atomic same-directory rename semantics, optimistic concurrency checks with 'File changed while editing', dry_run diff behavior, and refusal conditions.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is exceptionally detailed and well-structured with bolded section headers and front-loaded purpose, but it is very long and repeats some information in the mode list, section-mode deep dive, and Args list. It earns its length overall, but is not maximally concise.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a nine-parameter mutation tool with no annotations and no schema-level parameter descriptions, the definition is complete. It covers every parameter, mode, failure case, concurrency behavior, and round-trip guarantee; since an output schema exists, the absence of return-value prose is acceptable.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description fully compensates. The Args section explains all nine parameters in prose, including operation's legacy 'append'/'replace' values, section selector forms like 'Parent/Child' and '#N', replace_frontmatter's default behavior, and find uniqueness semantics with replace_all.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Opens with a specific verb and resource: 'Edit an existing note in the Obsidian vault.' It then enumerates four mutually exclusive modes, which clearly differentiates it from siblings like write_file and create_note, and names set_frontmatter for frontmatter-specific mutation.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Provides explicit when-to-use guidance: it points to get_vault_guide for conventions, names set_frontmatter as the better tool for structured frontmatter mutation, explains that a truncated read must be paged before writing back, and documents explicit refusals such as malformed frontmatter and indented unclosed fences.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
find_orphansA
Notes with zero incoming AND zero outgoing resolved links — useful for vault hygiene ("what's disconnected?") and cleanup decisions.
Args: folder: Optional vault-relative folder prefix to scope the search (e.g. "Cards/"). limit: Maximum results (default 50, hard cap 500).
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | ||
| folder | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The description explains the logic (zero incoming and outgoing links) but does not explicitly state it is read-only or mention any side effects. With no annotations, the agent can infer safety from context, but a direct statement would improve transparency.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is concise, with a clear first paragraph for purpose and a second for parameters. No unnecessary words, well-structured.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The description covers purpose, usage, and parameters adequately. Since an output schema exists, return values are documented elsewhere. The tool is simple, and the description is complete for the given context.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The description includes an Args section that explains both parameters: folder as a vault-relative prefix and limit with default and hard cap. This compensates fully for the 0% schema coverage, adding complete meaning.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states it finds notes with zero incoming and zero outgoing resolved links, specifying a verb and resource. It distinguishes from sibling tools like find_related or get_links by focusing on disconnected notes for vault hygiene.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It mentions usefulness for vault hygiene and cleanup decisions, providing context for when to use. However, it does not explicitly state when not to use or compare to alternatives like find_related.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_backlinksA
Notes that link TO path. Use this to discover what references a given
note — projects citing a card, daily notes mentioning a person, etc.
Resolved links only (dangling references are not counted as backlinks).
Args: path: Vault-relative path to the target note (e.g. "Cards/Foo.md"). limit: Maximum results (default 50, hard cap 500).
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | ||
| limit | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries full burden. It discloses that only resolved links are counted (dangling references excluded) and mentions the limit with a hard cap. Additional details like sorting or pagination would improve transparency.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is concise with a clear introduction, a behavioral note, and a structured Args section. It avoids unnecessary words but could be slightly more streamlined.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the presence of an output schema (not detailed here), the description adequately covers purpose, constraints (resolved links), and parameters. It provides sufficient context for using the tool.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Input schema has 0% description coverage, so description compensates. It clarifies 'path' as vault-relative and explains 'limit' default and hard cap, adding meaning beyond the raw schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool retrieves notes linking to a given path, with specific examples like 'projects citing a card'. It effectively distinguishes from siblings like 'get_links' by focusing on incoming references.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explains when to use ('discover what references a given note') but lacks explicit guidance on when not to use or direct mention of alternatives. However, the examples imply appropriate contexts.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_linksA
Outgoing links from path — both resolved and dangling.
Useful for "what does this note depend on?" or finding broken references that need follow-up notes.
Args: path: Vault-relative path to the source note.
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations provided, so description carries full burden. It mentions both resolved and dangling links, but lacks details on output format, whether it includes embeds, or any side effects. Adequate but not thorough.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two concise sentences plus param description. Front-loaded with core purpose. No unnecessary words.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given output schema exists, description doesn't need to explain return values. Covers essential: links type (outgoing, resolved/dangling) and param. Could be slightly more complete on edges (e.g., limit on links).
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 0%, so description compensates: 'path: Vault-relative path to the source note.' Adds meaning beyond type and title, clarifying the expected format.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states 'Outgoing links from path — both resolved and dangling.' It uses specific verb+resource and distinguishes from siblings like get_backlinks.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Provides clear use cases: 'what does this note depend on?' and 'finding broken references'. Does not explicitly exclude alternatives but gives strong context for when to use.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_neighborhoodA
The connected subgraph reachable from path via links or backlinks,
up to depth hops (treated as undirected).
Use this when an agent needs the local cluster around a topic — e.g.
"summarize everything connected to this project". Prefer this over
find_related when explicit links are the signal you want; prefer
find_related when the connection is conceptual rather than linked.
Args: path: Vault-relative path to the seed note. depth: Maximum BFS depth (default 1, capped at 5). limit: Maximum distinct neighbor notes (default 50, hard cap 200).
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | ||
| depth | No | ||
| limit | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Without annotations, the description fully explains the algorithm (BFS, undirected, depth/limit caps at 5 and 200 respectively). It does not describe error handling for missing paths, but for a read-only graph traversal, this is adequate.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is well-structured: a clear first sentence describing functionality, a usage paragraph, and a bulleted parameter list. No redundant or unnecessary text.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool has an output schema, the description focuses appropriately on behavior and parameters. It covers caps and defaults but omits behavior when `path` does not exist. Overall, it is sufficiently complete for an agent to use correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 0%, so description carries full burden. It adds meaning by explaining `path` as vault-relative, `depth` as maximum BFS depth with cap, and `limit` as maximum distinct neighbor count with cap, beyond the schema's defaults and types.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool returns 'the connected subgraph reachable from `path` via links or backlinks, up to `depth` hops (treated as undirected)', which is a specific verb and resource. It distinguishes from sibling `find_related` by contrasting explicit links vs conceptual connections.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicit guidance on when to use this tool (local cluster around a topic) and when to prefer alternatives: 'Prefer this over `find_related` when explicit links are the signal you want; prefer `find_related` when the connection is conceptual rather than linked.'
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_recentA
Get recently modified notes.
Args: limit: Number of recent notes to return (default 20). folder: Optional folder prefix to filter (e.g. "Projects/"). tags: Optional list of tag names; only notes carrying ALL listed tags match (e.g. ["meeting"]). frontmatter: Optional dict of frontmatter key/value pairs; strict type match (e.g. {"status": "active"}).
| Name | Required | Description | Default |
|---|---|---|---|
| tags | No | ||
| limit | No | ||
| folder | No | ||
| frontmatter | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description carries the full burden. It explains the filtering capabilities (folder, tags, frontmatter) and the limit parameter, but does not disclose ordering (presumably descending order), what fields are returned, or any side effects (it is read-only). The transparency is adequate but incomplete.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is concise: a single purpose sentence followed by clear, bullet-like parameter explanations. No extraneous information, and every sentence adds value.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the presence of an output schema, the description does not need to detail return values. However, it omits the ordering of results (recentness implies descending by modification date) and any performance considerations. This is a minor gap for an otherwise complete parameter specification.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The schema has 0% description coverage, so the description adds critical meaning. It explains the default value for limit, the prefix nature of folder, the AND logic for tags, and strict type matching for frontmatter. This goes well beyond the schema alone.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool retrieves recently modified notes, which is a specific verb and resource. It distinguishes from siblings like list_notes (which likely lists all notes) and keyword_search (which searches by content).
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
No guidance is provided on when to use this tool versus alternatives like list_notes or semantic_search. The description focuses solely on parameter details without mentioning use cases or when not to use it.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_tagsA
List all tags used across the vault with note counts.
Args: limit: Maximum number of tags to return (default 50)
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations exist, and the description does not disclose behavioral traits such as side effects, authorization needs, or limitations beyond the limit parameter.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is extremely concise, front-loading the purpose and then detailing the parameter with no extraneous text.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the simple nature of the tool and presence of an output schema, the description is adequate but lacks details on edge cases or behavior when no tags exist.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The description explicitly explains the limit parameter's meaning and default value, adding significant value beyond the empty schema description (0% coverage).
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool lists all tags with note counts, distinguishing it from sibling tools that operate on notes or perform search.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
No guidance is provided on when to use this tool versus alternatives like keyword_search or list_notes.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_vault_guideA
Returns a two-part guide for working with this Obsidian vault:
Obsidian primer — generic syntax (wikilinks, embeds, block refs, heading refs, tags, frontmatter, callouts, comments, highlights, math, mermaid, footnotes, tasks, plugin literals).
Vault-specific conventions — folder structure, naming rules, frontmatter requirements, and tag taxonomy as configured by the vault owner in
CLAUDE.md. IfCLAUDE.mdis absent, the response includes instructions for creating one.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The description details the two parts of the guide and handles the case of missing CLAUDE.md, providing good behavioral context. No annotations were provided, but the description covers the key aspects.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is well-structured with bullet points and covers essential details without being overly verbose, though it could be slightly more concise.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given no parameters and an output schema, the description is fully complete, explaining the guide contents and behavior when CLAUDE.md is absent.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
There are no parameters, so the baseline score of 4 applies. The description does not need to add parameter information.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states that the tool returns a two-part guide for working with the Obsidian vault, distinguishing it from sibling tools that operate on notes.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description implies usage for obtaining guidance about the vault, but does not explicitly state when to use it versus alternatives or exclude scenarios.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
import_from_urlA
Fetch a file from a public https URL straight into the vault. Requires write permission — a readwrite API key, or an OAuth token
carrying the readwrite scope.
Peer to write_file and request_upload — use this one when the bytes are
already somewhere public.
The server does the fetching, so nothing passes through your context: a 20 MB PDF costs one tool call. Returns the path, size, sha256, MIME type and the final URL after any redirects.
Only genuinely public addresses. This server sits on a private network
next to a database and other services, so the fetch is restricted: https
only — plain http is refused unless the operator has set
IMPORT_ALLOW_HTTP=true — no credentials in the URL, no
private/loopback/link-local/metadata addresses in any spelling, and the same
rules re-checked at every redirect.
A refusal names the rule that was violated — that is information about the
URL, not a hint to work around it. Rewriting the URL to evade the check is
never the right next step; ask the user for a public link instead.
Size-capped at MAX_FILE_WRITE_BYTES, with one 30-second deadline for the
whole fetch. No-clobber unless overwrite=True. Nothing is written unless
the whole body arrives intact.
This tool shares the transfer tools' preflight, so it also refuses when the
server has no public origin configured (MCP_HOSTNAME or BASE_URL), even
though it mints no link — that is an operator setting, not something to work
around.
Args:
url: Public https URL of the file.
path: Vault-relative destination (e.g. "Attachments/paper.pdf").
overwrite: If True, allow replacing an existing file at path.
| Name | Required | Description | Default |
|---|---|---|---|
| url | Yes | ||
| path | Yes | ||
| overwrite | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations present, the description carries the full disclosure burden and does so thoroughly. It states permission requirements (readwrite API key or OAuth scope), server-side fetching, security restrictions (https-only, URL rules, redirect re-checking), size cap, 30-second deadline, no-clobber behavior, atomic write guarantee, and preflight conditions. This is comprehensive behavioral transparency with no contradiction.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is long but every sentence earns its place, covering security, operational limits, failure modes, and parameter semantics. It is organized into digestible paragraphs with a clear Args section. The length is proportional to the tool's complexity, and the core purpose is front-loaded.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The description anticipates the full decision space: how to invoke, what permissions are needed, which security rules apply, what is returned, what can go wrong, and what not to do after a refusal. Even the transfer-tool preflight condition is disclosed. An agent has everything needed to call the tool correctly and diagnose failures.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The schema has zero description coverage, so the parameter meanings fall entirely on the description. The 'Args:' section explains all three parameters clearly: url as a public https URL, path with a vault-relative example, and overwrite with its boolean semantics. This exceeds what bare parameter names provide.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with a specific verb-resource pair: 'Fetch a file from a public https URL straight into the vault.' It also differentiates from sibling tools by naming write_file and request_upload and stating exactly when to use this one ('when the bytes are already somewhere public'). This leaves no ambiguity about what the tool does or how it differs from peers.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly names siblings and gives a decision rule: 'Peer to write_file and request_upload — use this one when the bytes are already somewhere public.' It also explains refusal conditions and explicitly tells the agent not to work around restrictions ('Rewriting the URL to evade the check is never the right next step; ask the user for a public link instead'), which is strong usage guidance beyond simple when-to-use.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
keyword_searchA
Full-text keyword search via PostgreSQL tsvector. Use this for exact identifiers, code symbols, proper nouns, or known phrases — anywhere semantic noise hurts.
For conceptual or paraphrased queries, use semantic_search instead.
Args: query: Keywords or phrase to match (websearch tsquery syntax: "foo bar", "foo OR bar", "-bar"). folder: Optional folder prefix (e.g. "Cards/", "Projects/"). limit: Maximum number of results (default 20). tags: Optional list of tag names; only notes carrying ALL listed tags match (e.g. ["project", "active"]). frontmatter: Optional dict of frontmatter key/value pairs; only notes whose JSONB frontmatter contains every pair match. Strict type matching — string "0" does not match integer 0 (e.g. {"status": "draft"}).
| Name | Required | Description | Default |
|---|---|---|---|
| tags | No | ||
| limit | No | ||
| query | Yes | ||
| folder | No | ||
| frontmatter | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are supplied, so the description carries the behavioral burden. It discloses key behaviors: uses tsvector, websearch syntax, strict type matching for frontmatter, and ALL-tags matching. It does not explicitly state read-only nature, but the tool name and search semantics imply it. The disclosed details are valuable and go beyond a generic search description.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is tightly written: a two-sentence purpose and usage intro followed by a structured Args block. Every sentence provides useful information, and the format is easy to scan. No fluffy or redundant content.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With an output schema present, the description correctly avoids explaining return values. It covers all parameters, gives usage guidance, and discloses matching semantics. The tool is moderately complex, and the description fully addresses its context, especially given the lack of annotations.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The schema provides 0% description coverage, but the description compensates fully with an 'Args' section that explains every parameter: query syntax examples, folder prefix, limit, tags requiring ALL match, and strict type matching for frontmatter. This adds substantial meaning beyond the raw schema fields.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with 'Full-text keyword search via PostgreSQL tsvector' and clearly specifies the tool's function: exact keyword matching for identifiers, codes, proper nouns, and known phrases. It explicitly distinguishes from semantic_search, making its purpose unambiguous and well-separated from siblings.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description provides explicit usage guidance: 'Use this for exact identifiers... anywhere semantic noise hurts' and contrasts with 'For conceptual or paraphrased queries, use semantic_search instead.' This directly tells the agent when to choose this tool and when to choose the alternative, naming the sibling tool.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_filesA
Browse the vault filesystem (ls-style), including non-markdown files.
Peer to list_notes, which lists indexed markdown only; list_files reads
the filesystem directly and reports sizes so you can gauge a binary before
read_file.
By default lists the immediate children of folder — subdirectories and
files, each file with size and modification time. pattern is a glob that
filters file entries (e.g. "*.pdf"); recursive=True descends into
subfolders and returns matching files. Anything with a path component
starting with . is hidden — dot-directories (.obsidian, .git,
.trash, …) and dot-files — and a folder with such a component is
rejected.
At most limit entries are returned (default 200, hard cap 1000); the
response indicates when the listing was truncated.
Args: folder: Vault-relative folder (default "." = vault root). pattern: Glob applied to file names (default "*"). recursive: If True, descend into subfolders. Off by default. limit: Maximum entries to return (default 200, hard cap 1000).
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | ||
| folder | No | . | |
| pattern | No | * | |
| recursive | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full behavioral burden, and it delivers thoroughly. It discloses that the tool reads the filesystem directly, reports file sizes and modification times, hides dot-directories and dot-files, rejects folders with such components, and caps results with a default of 200 and a hard cap of 1000, including truncation signaling. This is far beyond minimal disclosure.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is well-structured and front-loaded: the first sentence states the tool's purpose and key differentiator, followed by a sibling comparison and then precise behavioral details. Every sentence adds value; there is no filler or repetition of the schema.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The description is complete for a tool of this complexity. It covers all parameters, defaults, edge cases like hidden files and truncation, and usage context relative to sibling tools. Since an output schema exists, the description does not need to explain return values, and nothing essential is missing for correct invocation.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Although schema description coverage is 0%, the description fully compensates by documenting all four parameters in the Args section: `folder`, `pattern`, `recursive`, and `limit`. It explains defaults, glob semantics, recursion behavior, and the limit cap, adding real meaning beyond the raw schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with a clear, specific statement: 'Browse the vault filesystem (`ls`-style), including non-markdown files.' It names the resource and scope, and immediately differentiates itself from the sibling `list_notes`, which 'lists indexed markdown only.' An agent can tell exactly what this tool does and how it differs from related tools.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explicitly positions the tool relative to `list_notes` and `read_file`, explaining that `list_files` reads the filesystem directly and reports sizes 'so you can gauge a binary before `read_file`.' It also gives concrete behavioral context such as default listing behavior, glob filtering, recursion, and hidden-file handling, making usage conditions clear.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_notesA
List notes in a vault folder, sorted by most recently modified.
Results come from the index, so a note that exists on disk but has not yet been picked up by the indexer will not appear (lag is bounded by the index interval, typically up to 5 minutes).
Args: folder: Vault-relative folder path (e.g. "Cards/", "Projects/"). Empty for vault root. limit: Maximum number of results (default 50). tags: Optional list of tag names; only notes carrying ALL listed tags match (e.g. ["idea"]). frontmatter: Optional dict of frontmatter key/value pairs; strict type match (e.g. {"status": "active"}).
| Name | Required | Description | Default |
|---|---|---|---|
| tags | No | ||
| limit | No | ||
| folder | No | ||
| frontmatter | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and does well: it discloses index lag, sorting order, tag AND-matching, and strict frontmatter type matching. It does not explicitly state that the operation is read-only or describe error behavior, but the read-only nature is strongly implied by 'List'.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is front-loaded with the core purpose, then adds a critical caveat about index lag, then lists parameters in a clean Args block. Every sentence adds value and there is no fluff.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool has four parameters, no annotations, and an output schema, the description covers the essential behavior, filter semantics, and the index-lag caveat. It could be slightly more complete by clarifying whether subfolders are included recursively, but overall it is sufficient for an agent to invoke the tool correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
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 thoroughly. It explains folder paths with examples and root behavior, limit defaults, tag ALL-matching semantics, and strict frontmatter type matching—all beyond the bare schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool lists notes in a vault folder sorted by modification time, which is a specific verb+resource+scope. It does not explicitly contrast itself with sibling tools like get_recent or list_files, so it misses the highest bar for sibling differentiation.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description implies usage by explaining folder scoping and filter semantics, but it never states when to prefer this tool over alternatives such as keyword_search, semantic_search, or get_recent. There are no explicit exclusions or when-not-to-use conditions.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
move_noteA
Move or rename a note inside the vault. Requires write permission — a readwrite API key, or an OAuth token
carrying the readwrite scope.
Updates notes_metadata.file_path for the moved note and note_links.target_path
rows whose stored target matched the old path. Backlinks via target_note_id
keep working without rewriting source notes (the moved note's id is unchanged).
With rewrite_links=True, also opens every source note that linked to this
note and rewrites the link in place: [[Old]] → [[New]],
[[Old|alias]] → [[New|alias]], [[Old#anchor]] → [[New#anchor]],
![[Old]] → ![[New]], path-style [[folder/Old]] → [[new/folder/New]],
and markdown links [text](Old.md) → [text](<new path>), whose href is
written relative to the linking note's own folder (anchors preserved).
Aliases and anchors survive; only the target portion is rewritten.
The moved note's own body is rewritten as well, so a self-reference
does not end up pointing at the old path.
The rewrites are planned before anything changes: if one would push a source note past the 10 MiB note limit the whole move is refused, naming that source, before any file is touched. That preflight is also bounded in aggregate: if the originals plus rewrites for all backlink sources would exceed 256 MiB in memory the move is refused before anything changes, naming the note count and the limit.
The same preflight refuses the whole move, before the rename, when any
source it would rewrite — the moved note's own body included — contains a
fence opener indented by one to three spaces that nothing below it
closes. The refusal names each such source and where its opener sits. A
link under such an opener may be inside a list item's code block, which
this server does not parse, and a rewrite would mutate text whose
code-or-content status had to be guessed. Move with rewrite_links=False
(unaffected by this refusal) and fix the links yourself, or close the
fences first.
A rewrite can still fail after the move has committed. The move is one
rename and the rewrites follow it, so an I/O failure, a vault reassignment,
or a database that cannot be reached to confirm the assignment stops the
remaining rewrites — and the result then reads partial success: …, naming
the sources that still link to the old path. The move is not rolled back
and the index rows describe where the note now is. Treat the link graph as
agreeing with the vault bytes only when the result reports plain success;
on a partial outcome, fix the named sources with edit_note.
Writes are atomic. Either path is refused, naming the link's target, when
its final component is a symlink; symlinked folders inside the vault work
normally and the recorded paths are the real ones behind them. See
get_vault_guide for vault folder conventions.
Args: from_path: Vault-relative path of the existing note. to_path: Vault-relative path of the destination. Must not exist. Parent directories are created automatically. rewrite_links: If True, also rewrite incoming wikilinks and embeds in source notes. Off by default — opting in is destructive (it modifies other notes' bodies).
| Name | Required | Description | Default |
|---|---|---|---|
| to_path | Yes | ||
| from_path | Yes | ||
| rewrite_links | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full burden of behavioral disclosure, and it excels: it explains side effects on notes_metadata and note_links, link rewriting behavior, self-reference handling, preflight refusals, partial-success outcomes, no rollback, atomic writes, and symlink handling. This is far beyond what an agent could assume from the name alone.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Although long, the description is appropriately sized for a tool with complex link-rewriting behavior and multiple failure modes. It front-loads the core action and permission requirement, then uses structured paragraphs and bold warnings for non-obvious consequences. Each sentence carries meaningful information without filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The description is fully complete for an agent to invoke the tool correctly: it covers requirements, parameters, side effects, failure modes, limits, partial success handling, and vault conventions. Since an output schema exists, return-value details are not required. Nothing essential is left to inference.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must explain all parameters, and it does. It gives precise semantics for from_path, to_path, and rewrite_links, including defaults, constraints ('Must not exist'), side effects, and the destructive nature of opting in. This adds substantial meaning beyond the raw input schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
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: 'Move or rename a note inside the vault.' This clearly distinguishes move_note from siblings like create_note, edit_note, and delete_note. It also clarifies scope (vault-relative paths) and link-rewriting behavior, leaving no ambiguity about what the tool does.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description provides clear context on when to use the tool, including the required readwrite permission and the optional destructive rewrite_links mode. It names edit_note as the alternative for fixing partial-success results and points to get_vault_guide for folder conventions. It does not exhaustively contrast every sibling tool, but it gives enough guidance for correct selection and invocation.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
read_fileA
Read any file in the vault — including non-markdown (PDFs, images,
skill HTML/JS, data files). Peer to read_note, which stays markdown-only.
This is pure byte transport: the server does NOT extract or parse PDFs and cannot interpret binary bytes. Non-text/non-image files come back as an opaque base64 string intended for a client-side skill to decode — not as something the model can read directly.
Encoding:
"auto"(default): text-like files (HTML, JSON, CSV, source, …) return as readable text; images (PNG/JPEG/GIF/WebP) return as an inline image block that renders in-client; everything else returns as a labeled base64 string."text": force a UTF-8 text decode; errors if the file is not valid UTF-8."base64": force a raw-bytes base64 string regardless of type.
Files larger than MAX_FILE_READ_BYTES (default 10 MB) are refused with a
size report. Base64 reads pass through the model context and inflate ~33%,
so they are token-heavy — check a file's size with list_files before
reading large binaries. Any path with a component starting with . is
rejected — dot-directories (.obsidian, .git, .trash, …) and dot-files
alike — as is path traversal.
Text results are additionally capped to a context-safe size: the cap bounds
the returned window, and a truncated read appends a short notice carrying
the offset to continue from. Base64 and image results are not windowed.
Args: path: Vault-relative path to the file (e.g. "Reference Docs/spec.pdf"). encoding: One of "auto" (default), "text", or "base64". offset: Character offset to start a text read from (default 0). Use the value the truncation notice reports to continue. limit: Maximum characters to return for a text read. Only lowers the server cap; it cannot raise it.
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | ||
| limit | No | ||
| offset | No | ||
| encoding | No | auto |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries full responsibility for behavioral disclosure—and it excels. It discloses that the server does not parse PDFs or interpret binary bytes, describes base64 as opaque client-side transport, explains the 10 MB size refusal, dot-directory/path-traversal rejection, text windowing with truncation offsets, and the ~33% token inflation for base64. This is exemplary transparency.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Though detailed, the description is logically structured: purpose first, then transport semantics, encoding modes, limits, security restrictions, and an Args section. Every sentence carries operational value—no filler or repetition that doesn't inform invocation. The front-loaded purpose and sibling differentiation give immediate orientation.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given no output schema and no annotations, the description is remarkably complete. It covers return types for each encoding, error conditions (size, dot-paths, traversal), truncation behavior, continuation via offset, and token-cost warnings. An agent has everything needed to call the tool correctly and interpret results.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
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 a dedicated explanation: `path` with a vault-relative example, `encoding` with all three values and their exact behavior, `offset` with instruction to use the truncation notice value, and `limit` clarifying it can only lower the server cap, not raise it. This fully outweighs the lack of schema descriptions.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with 'Read any file in the vault' and immediately differentiates from the sibling `read_note` by noting that `read_note` stays markdown-only. It names the specific resource (vault files including non-markdown types) and the action (read), leaving no ambiguity about what the tool does.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explicitly says it is the peer to `read_note` for non-markdown files, giving the agent a clear selection rule. It also advises checking file size with `list_files` before reading large binaries, and explains when to use each encoding mode. This is direct, actionable guidance with alternatives and conditions.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
read_noteA
Read a note from the Obsidian vault by its relative path.
Returns a structured result, not a rendered document. Metadata and note content sit in separate fields, so there is no envelope to parse and no textual procedure to get wrong — read the fields:
content— the selected note text. Whole-note reads: the body with a valid YAML frontmatter block stripped, which is exactly whatedit_note(path, content)full replacement accepts. Section reads: the section's body only, which is exactly whatedit_note(path, content, section=...)accepts. Pass it straight back; do not add, strip or split anything.heading— section reads only: the matched heading line, with no line terminator. It is not part ofcontent, and a section write must not be sent it — the heading line is never rewritten.path,title,tags— metadata as data.frontmatter_yaml— the frontmatter block's YAML source, fence lines excluded, LF-normalized (a CRLF or lone-CR block comes back with LF terminators — the same declared residualcontentcarries, because this tool normalises and the write tools work on raw bytes;edit_notestill reattaches the original block byte-identically). This is the authoritative copy.frontmatteris a best-effort JSON view of the same block for convenience and may be absent — dates, non-string keys, recursive aliases and unpaired-surrogate escapes have no faithful JSON form, andmetadata_omissionsthen says which and why. To change frontmatter useset_frontmatter, or edit the raw block withedit_note(find=...); never write back a round trip of the JSON view.truncated,offset,next_offset,total_chars— truncation as data.outline(whole-note reads that were truncated) lists every section with its#Nordinal so you can fetch the one you want directly, andnoticecarries the guidance in prose.metadata_omissions— any metadata field this response had to drop, and why. Nothing is ever signalled by a marker inside a field.error— set when the read failed (missing note, badoffset/limit, unknown section). It is a normal result, not a transport error, and the content-bearing fields are absent beside it.
Budgets are per field, not per response: content is bounded by the
server's response cap, the outline by its own equal budget, and the
metadata fields by a third — so a truncated whole-note read can carry
several capped components. Read the one section you need with section=
rather than paging a large note.
Round trips. A whole-note content is byte-exact input for
edit_note(path, content) only when the read is complete and unwindowed
(offset=0 and truncated false); a truncated read must be paged to the
end first, or full replacement replaces the whole body with the fragment.
A section content is byte-exact input for edit_note(section=...) under
the same completeness condition. Byte-identity holds for notes whose body
newlines are LF: terminators inside the selected content come back as LF,
because this tool normalises and the write tools rewrite raw bytes.
Args:
path: Vault-relative path to the note (e.g. "Cards/My Note.md")
section: Optional ATX heading to read instead of the whole note. Plain
text ("Balance Sheet"), a path-style chain ("Parent/Child") when the
heading appears under different parents, or a "#N" ordinal ("#7",
1-based document order) — the ordinal is the only form that can
address duplicate headings sharing the same parent. The outline
returned with a truncated note carries the ordinal for every
section. A bare "#N" always selects by position and is never
shadowed by a heading whose text happens to be "#N"; use
"Parent/#N" to reach such a heading by title.
offset: Character offset to start reading from (default 0). Use the
next_offset the response reports to continue.
limit: Maximum characters of content to return. Only lowers the
server cap; it cannot raise it.
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | ||
| limit | No | ||
| offset | No | ||
| section | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| path | No | |
| tags | No | |
| error | No | |
| title | No | |
| notice | No | |
| offset | No | |
| content | No | |
| heading | No | |
| outline | No | |
| truncated | No | |
| frontmatter | No | |
| next_offset | No | |
| total_chars | No | |
| frontmatter_yaml | No | |
| metadata_omissions | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full behavioral burden and does so exhaustively. It discloses normalization behavior (LF terminators, frontmatter byte-identity restoration), truncation semantics including per-field budgets, the error-result model (normal result, absent content fields), and the exact conditions for byte-exact round trips. Nothing about side effects or return behavior is hidden.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Although long, the description is tightly structured: purpose first, then a field-by-field breakdown, then round-trip caveats, then parameter docs. Each sentence carries essential operational meaning—there is no filler. The density is appropriate for the tool's complexity, and the structure makes the content navigable.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool has no annotations and a complex structured response, the description leaves no agent-facing gap: it documents every response field, all parameter behaviors, truncation and paging, failure modes, and integration with editing workflows. The presence of an output schema does not excuse this because the description adds the crucial 'what do I do with this field' semantics that raw schema cannot.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The input schema provides only parameter titles and zero description coverage, so the description must explain each parameter on its own. It does so in depth: path with a concrete example, section with three addressing forms and the '#N' shadowing caveat, offset with next_offset continuation, and limit with the 'cannot raise it' ceiling. Every parameter's semantics are fully compensated.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The first sentence states a specific verb, resource, and scope: 'Read a note from the Obsidian vault by its relative path.' It further distinguishes itself from a generic file read by emphasizing it returns 'a structured result, not a rendered document,' which separates it from sibling tools like read_file. The purpose is unmistakable.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explicitly routes between alternatives: 'To change frontmatter use set_frontmatter, or edit the raw block with edit_note(find=...)' and warns against writing back the JSON view. It also gives clear within-tool guidance ('Read the one section you need with section= rather than paging a large note') and explains when content is safe to pass directly into edit_note. This fully informs when and how to use the tool.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
request_downloadA
Get a short-lived link a person can use to save a vault file. Peer to
read_file, which returns the bytes to you — use this one when the file is
for the human, not for you.
Handy for anything read_file would waste context on or cannot render: a
PDF, a large image, an archive. Reading works with a read-only key.
The token lives in the URL's # fragment, so it never reaches an access
log. Treat the whole URL as a secret — whoever holds it can read that
one file until it expires. Never put it in a query string.
The link is bound to the file as it is now: if it is edited or replaced, the link stops working rather than serving different content than you described. Unlike an upload link it can be used more than once, so the person can preview and then save.
From a shell: `curl -H "Authorization: Bearer " -o
Args:
path: Vault-relative path of the file to share.
expires_in: Seconds until the link dies. Clamped to 60–3600; defaults
to TRANSFER_TOKEN_TTL_SECONDS (600). A link can never outlive the
credential you are calling with, so the deadline in the result may
be earlier than you asked for — it says so when that happens.
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | ||
| expires_in | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full burden of behavioral disclosure, and it does so richly. It discloses that the token lives in the URL's `#` fragment, that the URL is a secret, that the link is bound to the file's current state and stops working if the file changes, and that it can be used more than once. It also warns about expiration clamping and the credential lifetime limit, which are non-obvious and important behaviors.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is front-loaded with the core purpose and usage distinction, then adds security and edge-case behavior in an organized way. The shell example is practical and earns its place. No sentence is filler, and the length is justified by the security sensitivity and behavioral nuance.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool's complexity and the absence of annotations, the description covers all decision-relevant aspects: when to use it, how it behaves, security constraints, parameter semantics, and a usage example. The mention that the result may provide an earlier deadline than requested adds important context. Since an output schema is present, the lack of a return-value breakdown is acceptable.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The schema provides almost no semantic detail, with 0% description coverage, but the description explains both parameters fully. It defines `path` as a vault-relative path and `expires_in` with its unit, default value, clamping bounds, and interaction with the credential's lifetime. This fully compensates for the bare schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description uses a specific verb and resource: 'Get a short-lived link a person can use to save a vault file.' It immediately contrasts with the sibling `read_file`, which returns bytes to the caller, making the tool's unique niche clear. Even without reading the schema, an agent can distinguish this tool from the other file-related siblings.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It explicitly states when to choose this tool over `read_file`: use it when the file is for a human, not for the caller, and when `read_file` would waste context or cannot render the content. It also contrasts with upload links and clarifies that this is a read operation usable with a read-only key. The guidance is concrete and actionable.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
request_uploadA
Get a short-lived link a person can use to put a file into the vault.
Requires write permission — a readwrite API key, or an OAuth token
carrying the readwrite scope.
Peer to write_file, which takes the bytes directly — use this one when you
do not have them.
No MCP client can hand a tool the bytes of a file the user is looking at,
and your shell cannot reach their machine. This mints a link bound to
exactly one destination path: hand it to the person you are helping, they
open it and pick a file, and it lands at path. Nothing else can be
written with it.
The token lives in the URL's # fragment, which browsers never send to a
server, so it stays out of access logs. Treat the whole URL as a secret
— whoever holds it can write that one path, once, until it expires. Never
put it in a query string: that would log it.
Single use, and no-clobber unless you ask otherwise. With
overwrite=True the link also remembers what the file looked like now and
refuses to publish if it changed in the meantime, so a stale link cannot
silently undo an edit someone made while it was waiting.
From a shell you can upload without the page:
curl -H "Authorization: Bearer <token>" -T <file> <base>/transfer/upload.
Then call check_upload(upload_id) to confirm the bytes landed and get
their sha256. See get_vault_guide for how files fit into the vault.
Args:
path: Vault-relative destination (e.g. "Attachments/photo.png").
overwrite: If True, allow replacing an existing file at path.
expires_in: Seconds until the link dies. Clamped to 60–3600; defaults
to TRANSFER_TOKEN_TTL_SECONDS (600). A link can never outlive the
credential you are calling with, so the deadline in the result may
be earlier than you asked for — it says so when that happens.
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | ||
| overwrite | No | ||
| expires_in | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full burden and exceeds it: it discloses the short-lived nature, readwrite permission requirement, single-use/no-clobber behavior, overwrite concurrency guard, expiration clamping, and the security-critical fact that the URL must be treated as a secret because the token lives in the fragment. It even warns against putting the token in a query string.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is long but intentionally so — every section earns its place: purpose, usage context, security warning, overwrite semantics, shell example, and parameter breakdown. Critical guidance is front-loaded, and the structure guides the reader from what to why to how.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a tool with three parameters, no annotations, and an output schema, the description provides everything needed to call it correctly: prerequisites, security handling, edge cases, a concrete curl upload path, and a pointer to the confirmation step check_upload. Nothing an agent needs to select and invoke this tool is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
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 fully. It does: path is explained with a vault-relative example, overwrite is defined as allowing replacement, and expires_in gets detailed semantics including the 60–3600 clamp, the 600-second default, and the nuance that a link cannot outlive the calling credential.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with a specific verb+resource+mechanism: 'Get a short-lived link a person can use to put a file into the vault.' It immediately sets the tool apart from the sibling write_file by noting it is a peer that takes bytes directly, so an agent can tell them apart without inspecting schemas.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicit selection criteria are given: 'Peer to write_file, which takes the bytes directly — use this one when you do not have them.' It also explains why alternatives are not viable ('No MCP client can hand a tool the bytes... your shell cannot reach their machine') and points to check_upload for follow-up and get_vault_guide for context.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
semantic_searchA
Vector similarity search over the vault's chunk embeddings. Use this for conceptual or paraphrased queries — anywhere exact word matching would miss the point.
For exact identifiers, code symbols, proper nouns, or known phrases, use keyword_search instead.
Each result is one note (deduped) with its best-matching chunk as a ~200-character preview.
Call read_note on a result's path to get the full note content.
Args: query: Natural language description of what you're looking for. limit: Maximum number of distinct notes to return (default 15). folder: Optional folder prefix (e.g. "Projects/"). tags: Optional list of tag names; only notes carrying ALL listed tags match (e.g. ["product"]). frontmatter: Optional dict of frontmatter key/value pairs; strict type matching — string "0" does not match integer 0 (e.g. {"status": "active"}).
| Name | Required | Description | Default |
|---|---|---|---|
| tags | No | ||
| limit | No | ||
| query | Yes | ||
| folder | No | ||
| frontmatter | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden. It clearly discloses that results are deduped notes with a ~200-character best-matching chunk preview, key behavioral detail. It also explains matching semantics for tags ('ALL') and frontmatter (strict type matching). It does not explicitly state non-mutation, but the verb 'search' implies it, so the description goes well beyond a bare schema.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is dense but efficiently structured: purpose first, then usage guidance, result format, next step, then a cleanly formatted Args list. No filler or redundancy; every sentence earns its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool's moderate complexity, the description covers purpose, when to use it, result shape, parameter semantics, and a suggested follow-up action. An output schema exists, so return values are already structured externally. There are no meaningful gaps for an agent to invoke this tool correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must document all parameters itself. It does so thoroughly for all five: query semantics, limit with default, folder prefix example, tags with ALL-match behavior, and frontmatter with a strict-type-matching example. This far exceeds what the schema alone provides.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The first sentence states a specific verb ('search'), resource ('vault's chunk embeddings'), and method ('vector similarity'). It explicitly contrasts with keyword_search, making it easy for an agent to distinguish this tool from its closest sibling without inspecting schemas.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description gives explicit when-to-use ('conceptual or paraphrased queries'), when-not-to-use ('exact identifiers, code symbols... use keyword_search instead'), and even a follow-up action ('Call read_note on a result's path'). This fully routes the agent between alternatives.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
set_frontmatterA
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.
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).
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | ||
| remove | No | ||
| updates | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full burden, and it excels: it discloses write requirements, partial-merge behavior, exact handling of missing frontmatter, strict refusal of malformed blocks, no-op semantics, YAML comment loss, and symlink refusal. This is exceptional behavioral disclosure beyond any structured metadata.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is long but meticulously organized with bolded section leads, a clear Args block, and no filler. Every sentence adds operational value, and the most important facts (purpose, permission, safety guarantees) are front-loaded before edge-case details.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a mutation tool with no annotations, the description covers permissions, exact mutation semantics, error behavior, return/no-op behavior, serialization caveats, symlink handling, and cross-references to relevant sibling tools. An output schema exists, so return-value documentation is reasonably delegated; nothing essential is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, and the description fully compensates with an Args section explaining path as vault-relative, updates as a mapping with empty-dict semantics, and remove as a list whose missing keys are ignored. It also clarifies behavioral nuances like type-sensitive comparison and no-op writes, giving the agent far more than the bare schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
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.' It clearly differentiates this from general content editing tools and even points to edit_note as the repair path for malformed frontmatter, so an agent can distinguish it from siblings.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description gives concrete usage context: write permission is required, symlinked final path components are refused, and get_vault_guide is referenced for frontmatter conventions. It stops short of explicitly stating 'use this tool when you need to modify frontmatter only, use edit_note otherwise,' but the repair reference to edit_note provides a clear alternative for malformed blocks.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
write_fileA
Write a file into the vault — including non-markdown (e.g. save a
generated PDF or image). Requires write permission — a readwrite API key, or an OAuth token
carrying the readwrite scope.
Peer to create_note/edit_note, which stay markdown-only.
content carries the bytes: with encoding="base64" (default) it is
base64-decoded to raw bytes; with encoding="text" it is written verbatim
as UTF-8. The write is atomic — the bytes are staged and flushed before
anything is published — missing parent folders are created, and content over
MAX_FILE_WRITE_BYTES (default 25 MB, decoded length) is refused.
No-clobber by default: writing over an existing file requires
overwrite=True. The default publishes by linking a staged, never-named
inode into place in one kernel-atomic step, so an existing file cannot be
replaced; overwrite=True publishes with a single same-directory rename
instead. A vault filesystem that cannot stage an unnamed file refuses the
no-clobber write with an error naming
VAULT_ALLOW_NAMED_STAGING_FALLBACK, rather than staging under a visible
name. Any path with a component starting with . (dot-directories and
dot-files alike) and path traversal are rejected; invalid
base64 errors without writing anything. A path whose final component is a
symlink is refused, naming its target, so overwrite=True cannot clobber a
file through an alias; symlinked folders inside the vault work normally.
The MCP transport also bounds the whole request body (sized so a base64
write at the cap always gets through). Base64 is therefore the always-safe
encoding: encoding="text" content whose JSON escaping inflates past that
bound is rejected by the transport with a bare HTTP 413 before this tool
runs — send such content as base64 instead.
Args: path: Vault-relative destination path (e.g. "Outputs/report.pdf"). content: File contents — base64 string (default) or UTF-8 text. encoding: "base64" (default) or "text". overwrite: If True, replace an existing file. Off by default.
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | ||
| content | Yes | ||
| encoding | No | base64 | |
| overwrite | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description carries the full behavioral burden. It thoroughly discloses permissions, atomic writes, no-clobber defaults, overwrite semantics, dot-path and traversal rejection, symlink handling, size limits, and transport-level 413 behavior. This is exceptionally transparent.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is front-loaded with purpose and permission, and every section adds useful behavioral meaning. However, it is dense and includes implementation-level details like kernel-atomic inode linking, VAULT_ALLOW_NAMED_STAGING_FALLBACK, and HTTP 413 mechanics that could be trimmed without losing agent-relevant guidance.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool's complexity, no annotations, and zero schema description coverage, the description is remarkably complete. It covers permissions, parameter semantics, success/failure conditions, edge cases, size limits, overwrite behavior, and encoding safety. The presence of an output schema means return values do not need to be explained.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
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. The Args section explains each parameter beyond the schema: path is vault-relative, content is base64 or UTF-8 text, encoding defaults to base64, and overwrite is off by default. Prose also clarifies encoding behavior and byte-decoding semantics.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
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: 'Write a file into the vault' and immediately clarifies it includes non-markdown files. It also names and distinguishes sibling tools create_note/edit_note as 'markdown-only', so an agent can tell this tool apart without inspecting schemas.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description gives clear context: use this for vault files including non-markdown content, and notes that create_note/edit_note are peers that stay markdown-only. It explains when overwrite is needed and gives encoding guidance, but does not explicitly state 'use create_note/edit_note for markdown-only writes', so the routing guidance is slightly implicit.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
TDQS
Most tools have clearly distinct purposes, and overlapping pairs like keyword_search/semantic_search and get_neighborhood/find_related contain explicit cross-references that prevent misselection. A few pairs require careful reading—read_note/read_file and list_notes/list_files both operate on markdown, and write_file overlaps with create_note—so the set is not perfectly unambiguous.
The server overwhelmingly follows a readable verb_noun snake_case pattern (read_note, create_note, delete_file, set_frontmatter). Minor deviations exist: keyword_search and semantic_search invert the pattern, get_recent omits its noun, and retrieval verbs are split among get_, list_, and find_ without a strict convention.
At exactly 25 tools, the server sits at the top of the 'feels heavy' range. The count is justified by the broad scope—note CRUD, search, graph traversal, generic file operations, and human-mediated transfers—but an agent must navigate many similar file/note and upload/download variants.
The tool surface is exceptionally complete for an Obsidian vault: full note lifecycle (create, read, edit, move, delete), frontmatter mutation, keyword and semantic search, tag and recent-note discovery, link-graph exploration, orphan detection, generic file operations, and even human-in-the-loop upload/download flows with upload confirmation. There are no obvious dead ends or missing core operations.
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
Markdown-based note-taking with a hosted MCP server. Your notes serve you and your AI.
Personal knowledge base MCP server with semantic search, auto-categorization, metadata extraction
Self-hosted AI-native knowledge workspace with hybrid search, GraphRAG, and MCP.
Serve a folder of Markdown notes as an MCP server: hybrid search, reading, and sourced answers.
Related MCP Servers
- AlicenseBqualityCmaintenanceHeadless semantic MCP server for Obsidian, Logseq, Dendron, Foam, and any markdown folder. Features built-in hybrid semantic search, surgical AST editing, template scaffolding, zero-config local embeddings, and workflow tracking.532311MIT
- AlicenseAqualityAmaintenanceMCP server for Obsidian vaults — search, memory, link graph, 23 tools, OAuth-protected. Runs locally via Docker or remotely with Obsidian Sync + OAuth 2.1.43369816MIT
- AlicenseNot gradedqualityBmaintenanceSelf-hosted MCP server that provides embedding-powered semantic search with graph context over Obsidian vaults, supporting multiple vaults, local embeddings, and a web dashboard.MIT
- AlicenseNot gradedqualityAmaintenanceLocal-first MCP server for retrieval over markdown wikilink vaults, offering hybrid vector+lexical search, note reading, neighbor expansion, and recent activity tracking with fully local embeddings and no network egress.MIT
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/maxkuminov/obsidian-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server