Multi-vault, multi-user MCP server giving Claude read/write access to Obsidian vaults on a VPS — over ssh stdio and an OAuth-protected HTTP transport, where each user's login routes to their own vault.
The server provides 9 well-named tools covering read and write operations on Obsidian vaults. All tools have clear verb-noun naming (list_notes, read_note, search_notes, write_note, etc.) and descriptions. However, input parameter descriptions are minimal or missing entirely, most parameters have only a type and brief label, lacking the context and constraints LLMs need to use them correctly. Output schemas are not documented. Error handling wraps VaultError exceptions into clean messages but provides no recovery guidance. The server demonstrates solid foundation work but falls short of production-grade tool design.
Tools (9)
append_to_notewritesource verified63/100
Append text to the end of a note (creating it if missing).
create_notewritesource verified63/100
Create a new note. Fails if a note already exists at that path.
delete_notedestructivesource verified60/100
Delete a note by its vault-relative path.
edit_notewritesource verified65/100
Replace an exact snippet in a note. oldText must appear exactly once — include enough surrounding context to be unique.
list_notesread onlysource verified75/100
List notes (markdown/canvas files) in the vault recursively, as vault-relative paths. Optionally limit to a subfolder.
move_notereversiblesource verified62/100
Move or rename a note from one vault-relative path to another.
read_noteread onlysource verified68/100
Read the full contents of a note by its vault-relative path.
Input parameter descriptions are missing or minimal. Parameters like 'path', 'content', 'text', 'oldText', 'newText', 'from', 'to' in write_note, create_note, append_to_note, edit_note, delete_note, and move_note lack context about format expectations, constraints, and usage. LLMs cannot infer whether 'path' accepts absolute or relative paths, or how deeply nested folders should be structured.
Output schemas are not documented. Tools return plain-text success messages ('Wrote X', 'Created Y') but LLMs have no formal schema describing what fields to expect, whether responses are structured or free-text, or how to chain results to downstream tools. This violates the pattern requirement that tool outputs be formally documented.
Add detailed descriptions to all input parameters. For 'path', specify: 'Vault-relative path (e.g. Folder/Note.md). Uses forward slashes as separators. Parent folders are created if missing for write operations.' For 'content' and 'text', clarify: 'Markdown content. Trailing newlines are preserved.' For 'oldText' and 'newText', explain: 'Text must match exactly. Use surrounding context to ensure uniqueness, oldText must appear exactly once in the note.'
Document output schemas for all tools. For list_notes, specify: 'Returns a newline-separated list of vault-relative paths, or "(no notes found)" if empty.' For read_note, clarify: 'Returns the full file contents as plain text, preserving line breaks and formatting.' For write_note and create_note, return a JSON object with { path, success, timestamp } rather than plain text, enabling downstream tools to chain results.
Implement confirmation flow for delete_note. Modify the tool to accept an optional 'confirm' parameter (boolean, default false). When confirm=false, return a preview: '{ path, action: "delete", status: "confirmation_required", message: "About to delete Folder/Note.md (200 bytes). Call delete_note again with confirm=true to proceed." }' This enables agents to preview before destroying data.
Enhance error messages with recovery guidance. When a note is not found, return: 'Note "Folder/Note.md" not found. Check the path spelling or call list_notes("Folder") to see available notes.' When path traversal is detected, return: 'Invalid path: contains .. or absolute paths. Use vault-relative paths like "Folder/Note.md".' When oldText in edit_note is not found exactly, return: 'oldText does not appear exactly once in the file. Current content snippet: [first 200 chars]. Ensure oldText matches exactly, including whitespace.'
Spec posture evidence
Inferred effective spec: 2026-07-28+.
Relies on Dynamic Client Registration (deprecated) - use Client ID Metadata Documents (CIMD)
Score history
Overall score trend
First recorded score · v2 rubric
65/100
Scored
Grade
Overall
Spec posture
Rubric
2026-09-23
C
65
2026-07-28+
v2
search_notesread onlysource verified73/100
Case-insensitive literal full-text search across the vault. Returns 'path:line: text' hits.
write_notewritesource verified63/100
Create or overwrite a note with the given content. Creates parent folders as needed.
Destructive operation (delete_note) lacks confirmation or dry-run capability. The tool directly deletes notes without a confirmation step or preview. Agents can irreversibly destroy data without recovery guidance. Error messages do not suggest undo steps or recovery paths.
Error handling does not categorize errors as retryable, user-fixable, or fatal. The wrap() function catches VaultError and returns fail() responses with generic error text, but provides no guidance to the agent on whether to retry, ask the user for clarification, or abort. This violates pattern:error-classification.
No pagination or result limits documented for list_notes. The tool recursively lists all markdown/canvas files in the vault. If a vault contains thousands of notes, the response could be arbitrarily large and exhaust context windows. The description does not mention result limits or pagination support.
search_notes max_results parameter has type and bounds (1 - 2000) but lacks a description explaining why a limit exists or when to use non-default values. The parameter is defined in code but the description is empty in the tool registration.
No tool annotations (readOnlyHint, destructiveHint, idempotentHint) are present. The server implements read-only mode (vault.readOnly) and manages destructive operations (delete_note), but does not declare these intentions via tool annotations, leaving clients unable to infer safety properties statically.
Add max_results parameter description to search_notes: 'Maximum number of results to return (1 - 2000, default 200). Use higher limits for comprehensive searches; lower limits for fast, focused results. Larger result sets consume more context tokens.'
Implement pagination for list_notes. Add optional 'limit' (1 - 100, default 50) and 'offset' (0 or positive integer) parameters. Return { notes: [...], total: number, offset, limit, hasMore: boolean }. This prevents context window exhaustion for large vaults.
Add tool annotations to server registration. For read tools (list_notes, read_note, search_notes), set readOnlyHint=true. For delete_note, set destructiveHint=true. For move_note, set idempotentHint=false (moving is not safe to retry if the destination already exists). Use McpServer.registerTool() with annotations parameter.
Validate input paths against path traversal attacks. Reject paths containing '..', absolute paths (starting with '/'), or null bytes. Return 'Invalid path: must be relative and not contain .. or /. Example: Folder/Subfolder/Note.md'
Document the behavior of file extensions. The description mentions 'markdown/canvas files' but does not specify: 'Valid extensions: .md (Markdown) and .canvas (Canvas). Other extensions are silently ignored by list_notes.'
Add idempotency guarantees to tool descriptions. For create_note, clarify: 'Returns error if path already exists, this tool is not idempotent. Use write_note to safely overwrite existing notes.' For write_note, state: 'Overwrites existing files. This operation is idempotent: calling twice with the same path and content produces the same final state.'