A Model Context Protocol (MCP) server for managing Markdown notes in Obsidian vaults with full Zettelkasten workflow support.
This is a Go rewrite inspired by mcp-obsidian using the MCP Go SDK v1.4.
- Search Notes: Find notes by filename using case-insensitive matching with regex support
- Read Notes: Read the content of one or more notes with error handling per file
- Create Notes: Create notes with
YYYY-MM-DD_slug.mdnaming convention and YAML frontmatter - Update Notes: Replace or append content to existing notes
- Delete Notes: Safely remove markdown notes from the vault
- Search Content: Full-text search across note bodies with line numbers and snippets
- Get Backlinks: Discover all
[[wikilink]]references to a given note - List Tags: Collect and count tags from YAML frontmatter and inline
#tags - Structured Logging: Uses Go's standard
log/slogwith leveled, key-value output to stderr - Security: Built-in path validation to prevent directory traversal and access to hidden files
- Performance: Native Go static binary with zero CGO dependencies
git clone https://github.com/nsega/mcp-obsidian.git
cd mcp-obsidian
make buildThe binary is written to build/mcp-obsidian.
go install github.com/nsega/mcp-obsidian@latestRun the server with the path to your vault:
./build/mcp-obsidian /path/to/your/vaultThe server communicates over stdin/stdout using the MCP protocol.
Add the following to your Claude Desktop configuration file:
macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"obsidian": {
"command": "/path/to/mcp-obsidian",
"args": ["/path/to/your/vault"]
}
}
}Add to your ~/.claude/settings.json or project .claude/settings.json:
{
"mcpServers": {
"obsidian": {
"command": "/path/to/mcp-obsidian",
"args": ["/path/to/your/vault"]
}
}
}Add to your .vscode/mcp.json:
{
"mcpServers": {
"obsidian": {
"command": "/path/to/mcp-obsidian",
"args": ["/path/to/your/vault"]
}
}
}Search for notes by filename using case-insensitive matching.
Input:
query(string, required): Search query or regex pattern
Example:
{
"query": "meeting"
}Returns up to 200 matching file paths.
Read the content of one or more notes.
Input:
paths(array of strings, required): File paths to read
Example:
{
"paths": [
"/path/to/vault/note1.md",
"/path/to/vault/note2.md"
]
}Returns the content of each note with error handling for individual files.
Create a new note with the Zettelkasten naming convention and YAML frontmatter.
Input:
title(string, required): Title for the note (used in filename slug and heading)content(string, optional): Markdown body contentfolder(string, optional): Subfolder within the vault (e.g.30_Permanent,10_FleetingNote)tags(array of strings, optional): Frontmatter tags
Example:
{
"title": "GTD Zettelkasten Flowchart",
"content": "A note about combining GTD with Zettelkasten.",
"folder": "30_Permanent",
"tags": ["zettelkasten", "gtd", "productivity"]
}Creates a file like 30_Permanent/2026-02-15_gtd-zettelkasten-flowchart.md with YAML frontmatter containing tags, created, and updated dates.
Update an existing note's content.
Input:
path(string, required): Full path to the notecontent(string, required): New content to write or appendmode(string, optional):replace(default) orappend
Example:
{
"path": "/path/to/vault/30_Permanent/2026-02-15_my-note.md",
"content": "\n## New Section\nAdditional thoughts.",
"mode": "append"
}Delete a markdown note from the vault.
Input:
path(string, required): Full path to the note to delete
Example:
{
"path": "/path/to/vault/00_Inbox/2026-02-15_scratch.md"
}Only .md files can be deleted. Directories cannot be deleted.
Full-text search across note bodies. Returns matching file paths, line numbers, and snippets.
Input:
query(string, required): Search query or regex pattern
Example:
{
"query": "Zettelkasten"
}Returns up to 200 matches with file path, line number, and the matching line content.
Find all notes that link to a given note via [[wikilinks]].
Input:
note_name(string, required): Note name without.mdextension or path
Example:
{
"note_name": "my-permanent-note"
}Returns source file paths and the lines containing the wikilinks. Supports both [[note]] and [[note|alias]] syntax.
List all tags found across the vault from YAML frontmatter and inline #tags.
Input:
prefix(string, optional): Filter tags by prefix
Example:
{
"prefix": "project"
}Returns a sorted, deduplicated list of tags with their occurrence counts.
The server implements several security measures:
- Path Validation: All file operations are restricted to the specified vault directory
- Hidden Files: Access to files and directories starting with
.is denied - Symlink Resolution: Symlinks are resolved and validated to prevent directory escape attacks
- Error Handling: Individual file read failures don't halt operations (logged at
Debuglevel viaslog)
main.go CLI entry point, slog setup, wiring
internal/
├── note/
│ ├── types.go Input/Output structs for all 8 tools
│ ├── util.go Slugify, GenerateFrontmatter, ParseFrontmatterTags
│ └── util_test.go
├── vault/
│ ├── vault.go Vault struct, path validation, symlink resolution
│ └── vault_test.go
├── handler/
│ ├── handler.go 8 MCP tool handler methods (with injected logger)
│ └── handler_test.go
├── server/
│ └── server.go MCP server creation, tool registration, logger passthrough
└── testutil/
└── testutil.go Shared test vault setup/cleanup helpers
- Go 1.26.1 or later
- golangci-lint (for linting)
make build # Build binary to build/mcp-obsidian
make build-all # Cross-compile for linux/darwin/windows (amd64/arm64)make test # Run all tests with race detector
make coverage # Run tests with HTML coverage reportmake lint # Run golangci-lint
make check # Run fmt + vet + lint + testmake run VAULT=/path/to/your/vaultRun make help for the full list of targets.
The easiest way to interactively test the server is with the official MCP Inspector:
npx @modelcontextprotocol/inspector ./build/mcp-obsidian ~/my-vaultThis opens a web UI at http://localhost:5173 where you can test all 8 tools and view the JSON-RPC messages.
You can also test by piping JSON-RPC messages via stdin:
(printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}\n'; \
sleep 0.5; \
printf '{"jsonrpc":"2.0","method":"notifications/initialized"}\n'; \
sleep 0.3; \
printf '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}\n'; \
sleep 1) | ./build/mcp-obsidian ~/my-vault 2>/dev/nullServer doesn't start:
- Verify the vault path exists and is readable
- Ensure the binary is executable:
chmod +x build/mcp-obsidian
Tools not appearing in Claude Desktop / Claude Code:
- Check the configuration file path and JSON syntax
- Restart the client after configuration changes
- Check logs for error messages:
- Claude Desktop:
~/Library/Logs/Claude/mcp*.log(macOS) - Claude Code: check terminal output
- Claude Desktop:
Permission errors:
- Ensure the vault directory is readable
- Hidden files (starting with
.) are denied by design - Use absolute paths, not relative
No results from search:
- Verify your vault contains
.mdfiles - Search is case-insensitive and supports regex
MIT
Contributions are welcome! Please feel free to submit a Pull Request.