ragnarok
The ragnarok MCP server gives Claude Code local, private search and reading tools over indexed Obsidian vault notes.
Run hybrid semantic and keyword search across notes, with filters for vault, project, area, type, private notes, and archived notes.
Find earlier projects that match or continue a prompt, even when the project is not named.
List all notes in a project with metadata such as type, status, description, stage, state, and next action.
Show open projects grouped by state, newest update first.
Open a note or a specific section by heading, including frontmatter, links, backlinks, and outlines for long notes.
Find notes containing a Jira ticket key.
Protect private notes by refusing them and redact credentials as
[REDACTED].
Indexes the Markdown notes of one or more local Obsidian vaults and exposes hybrid search over them: a dense vector search plus a SQLite FTS5/BM25 keyword search fused by reciprocal rank. Supports heading-based chunking, metadata extracted from frontmatter or path rules (vault, project, area, type, status), filtered search, project discovery and listing, opening a note or a single section with its links and backlinks, lookup by ticket key, private-note exclusion, and credential redaction. The index stays local in a single SQLite file and is refreshed from changed notes on every tool call.
Uses a local Ollama embedding model to embed note chunks for the dense half of the hybrid search, so no vault content leaves the machine; the model and vector dimension are configurable and a command sets up/checks the Ollama service.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@ragnarok search my notes for the authentication design doc"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
ragnarok
Local RAG search over Obsidian vaults for Claude Code.
ragnarok indexes the Markdown notes of one or more Obsidian vaults on your computer. A local MCP server gives Claude Code search tools over the index. No note leaves the computer: the embeddings come from a local Ollama model, and the index is one SQLite file.
Features
Hybrid search. Each query runs a dense search (embeddings in
sqlite-vec) and a keyword search (SQLite FTS5 with BM25). Reciprocal rank fusion joins the two lists. The unit of rank is the note, with its best section.Heading chunks. The splitter cuts each note at its headings and joins very short sections.
Metadata from frontmatter or path. Each note gets a vault, a project, an area, a type, a status and ticket keys. The frontmatter gives these values first. For a note without them, path rules in the configuration give them.
Project search.
find_projectsfinds the earlier projects that a prompt continues or resembles, also when the prompt does not name the project.list_projectgives all notes of a project.Always current. Each tool call first scans the vaults for changed notes (usually less than 100 ms) and embeds them. There is no watcher process.
Private notes. Notes under the configured private paths are in the index, but the server does not return them. Only the user can open them, with
allow_private = truein the configuration.Credential redaction. The indexer and
open_notereplace literal passwords, tokens and keys with[REDACTED]. The vault files do not change.Frontmatter backfill.
ragnarok backfill proposewrites a proposal of the missing frontmatter keys for a vault.ragnarok backfill exportwrites a JSON file with the title and the first lines of each public note that needs a description (credentials removed). Claude writes the descriptions into one or more JSON files,{path: {"description": "...", "type": "..."}}.ragnarok backfill merge <vault> <file>...puts them into the proposal, removes credentials, reports each description of more than 25 words and each row with no description, and replaces a default type with a valid type from Claude. You examine the proposal, thenragnarok backfill applymakes a backup archive of the vault and adds only the missing keys.Evaluation.
ragnarok evalcompares grep, keyword, dense and hybrid search on your own question sets.ragnarok eval agentcompares a Claude Code agent with grep and an agent with the MCP tools.One-command setup.
ragnarok initinstalls the model and the index, and registers the MCP server in Claude Code when the Claude Code plugin does not give it.ragnarok doctorchecks the setup.
Related MCP server: obsidian-mcp
Requirements
macOS with Homebrew. The setup uses
brew servicesfor Ollama.Ollama:
brew install ollama.uv:
brew install uv. uv installs Python 3.12.setup.shinstalls uv if it is missing.
Setup
ragnarok is on PyPI as ragnarok-mcp and in the
MCP Registry as io.github.freyr/ragnarok.
With the Claude Code plugin (no clone)
In Claude Code:
/plugin marketplace add freyr/ragnarok-bundle
/plugin install ragnarok@ragnarokThen in a terminal:
uvx ragnarok-mcp initThe plugin starts the server with uvx ragnarok-mcp serve and adds the search guidance to each session. The home
folder is ~/.ragnarok. To use a different folder, set RAGNAROK_HOME in the env of
~/.claude/settings.json and in your shell.
From a checkout
git clone https://github.com/freyr/ragnarok-bundle.git
cd ragnarok-bundle
./setup.sh # installs uv if needed, then runs `ragnarok init`The checkout is the home folder.
What init does
ragnarok init does six steps. It skips each step that is already done:
It writes
config.tomlfromconfig.example.toml. It asks for the vault root, the folder that holds your vault folders, and adds each subfolder as a vault.It checks that Ollama is installed.
It runs Ollama as a login service with
brew services start ollama(it asks first). If Ollama runs outside brew services, for exampleollama servein a terminal, init stops it and starts the service.It pulls the embedding model.
It builds the index. A vault with 1,500 notes takes approximately 12 minutes on an Apple Silicon Mac.
When the plugin
ragnarokgives the MCP server, it removes a registration at the user scope (it asks first). Else it registers the MCP serverragnarokin Claude Code at the user scope (it asks before it replaces another registration).
Restart Claude Code after step 6. ragnarok doctor checks the same six parts and prints the fix for each
failure.
Before step 5, examine config.toml: remove a folder that is not a vault, and add your private paths. A later
change of the configuration applies at the next ragnarok index or MCP tool call, with no restart.
Use in Claude Code
The server gives six tools:
Tool | What it does |
| Hybrid search over the notes. Filters: vault, project, area, type, archived. |
| The projects that match a prompt, with the summary and the matching notes of each project. |
| All notes of one project. |
| The open projects ( |
| The text of a note or of one section, with its links and backlinks. A note of more than 4,000 words gives an outline: each section with its heading path, words and first sentence. |
| The notes that contain a ticket key, for example |
The plugin adds the guidance below to each session itself, with the plugin tool names
mcp__plugin_ragnarok_ragnarok__*. Without the plugin, Claude Code loads MCP tools on demand. To make Claude
use them before grep, add a short instruction to your ~/.claude/CLAUDE.md file, for example:
## Finding notes: ragnarok first
The `ragnarok` MCP server indexes my Obsidian vaults. Use it before Grep or Glob whenever you look for
notes, earlier work or context:
- `find_projects`: the prompt continues or resembles earlier work and names no project. Then call
`list_project` on the best match (look at the top 3).
- `search`: facts, decisions, how something was done. Filters: vault, project, area, type.
- `open_note`: read a note or one section, with its links and backlinks. A long note gives its outline first; open the section you need by heading.
- `open_projects`: which projects are in progress or paused.
- `find_by_jira`: a ticket key such as `OPS-77`.
The tools are deferred. Load them in one call before the first use:
`ToolSearch` with `select:mcp__ragnarok__search,mcp__ragnarok__find_projects,mcp__ragnarok__list_project,mcp__ragnarok__open_projects,mcp__ragnarok__open_note,mcp__ragnarok__find_by_jira`.
If the tools are absent (for example on a computer without ragnarok), fall back to Grep and say so.
Results can be unrelated: read the text before you rely on it, and cite note paths. Private notes are
refused by design; do not work around the refusal with Read or Grep.Adapting to your vaults
docs/vault-requirements.md tells how a vault must look to get the most from
ragnarok: headings, project folders, SUMMARY.md, frontmatter and private folders.
The default path rules expect <vault>/<area>/<project>/note.md. If your vaults look different, the repo
skill .claude/skills/adapt-vault/ guides Claude Code through the changes. Open Claude Code in the checkout
and ask it to adapt ragnarok to your vaults. The skill:
runs
ragnarok surveyon each vault;agrees the private folders with you, before any other change and before a new vault goes into
vaults;writes the path rules into
config.toml, checked withragnarok inspect;runs the frontmatter backfill, if you agree, with your review before
apply;writes a small evaluation set from your notes and runs
ragnarok eval mechanical;adds the guidance above to your
~/.claude/CLAUDE.md, after it asks you.
It changes only config.toml and adds frontmatter keys. It never moves, renames or deletes a note or a folder.
Configuration
config.toml is in the home folder. A relative path in it is relative to the home folder.
config.example.toml explains each key. The most important keys:
Key | What |
| The folder that holds the vaults, and the vault folders to index. |
| The folder of a vault that is not in |
| Glob patterns of private notes, for example |
| The Ollama embedding model and its vector size. A dimension below the native dimension of the model gives a shorter vector (Ollama |
| Override the query prefix and the document template of the model profile. |
| How long Ollama keeps the embedding model in memory after a call: an integer of seconds, |
| The chunk size limits of the splitter. The splitter also cuts a chunk to the token limit of the model profile (4 characters for each token), and a text that Ollama refuses as too long is embedded in two halves; Ollama never truncates a text. A change needs |
|
|
| How the chunks of a note give its rank: |
| The bench: a variant overrides |
| The bench: a |
| A folder that holds one of these files is a project folder (default |
| The ticket prefixes that |
| The path rules of one vault: which folder gives the area and the project. |
Note metadata
ragnarok reads these frontmatter keys. Each key is optional:
---
project: heating-upgrade # the project slug; default: from the path rules
area: home # a wide subject; default: from the path rules
type: plan # drivers, summary, research, spec, plan, brainstorm, review, decision,
# guide, reference, meeting, log, template or note
status: active # active or archived; a note in an _archive folder, or in a project
# whose SUMMARY.md has state: superseded or abandoned, is archived; state: done is current
description: One sentence about the note, which improves find_projects.
jira: [ABC-123] # ticket keys, in addition to the keys in the text
---Without frontmatter, the path rules give the metadata. The default rules work for a vault with the layout
<vault>/<area>/<project>/note.md. For a different layout, add a [vault_rules.<vault>] table. Use
ragnarok inspect <note path> to see the metadata of a note.
Commands
Command | What it does |
| Sets up this computer (see Setup). |
| Checks the setup. |
| Adds, changes and removes notes in the index. After a change of the path rules, the private paths or the markers, it derives the metadata of all notes again, with no new embeddings. When a |
| Shows the size and the state of the index. |
| Searches from the terminal. |
| Shows the metadata of notes. |
| Shows the chunks of notes, or the totals. |
| Runs the MCP server on stdio. Claude Code starts it. |
| Proposes, describes and applies missing frontmatter keys. The order is |
| Prints the folders, frontmatter keys, marker files, name patterns and ticket prefixes of a vault folder. Run it before you write |
| Runs an evaluation on the question sets in |
| Builds or updates the index of a variant on a corpus in |
| Runs each question of a set through the MCP search ( |
| Gives recall@5 and MRR of each variant, by stratum and by area, and the difference B − A with a 95% interval from a cluster bootstrap (10,000 samples, fixed seed). On a synthetic set, the interval level follows one Holm correction over each pair of a B and a measure (recall@5 and MRR), so two measures do not give a B two chances to win. On a real set, each B has the level 0.10 with no correction. It lists the share of discordant questions and each changed question with its diagnostics. It ends with one verdict line for each B (see the set format). |
| Converts one or more old sets to the set format. It reads the draft format of |
The set format
A set is one JSON file:
{
"name": "pilot", "kind": "real", "version": 2, "corpus": "real", "hash": "<SHA-256 of the questions>",
"questions": [
{"id": "q01", "question": "What is a field stop?", "expected": ["Private/Astro/Field Stop.md"],
"stratum": "lookup", "area": "astro", "cluster": "Private/Astro/Field Stop.md", "notes": "optional"}
]
}kindissyntheticorreal.corpusis a table[corpora.<name>]of the config.hashis the SHA-256 of thequestionslist as JSON with sorted keys and no spaces. The bench refuses a set whose hash does not agree.expectedholds one or more note paths. A continuation question lists theSUMMARY.mdof its project.clusteris the path of the main gold note. The bootstrap draws clusters, not questions.recall@5 is 1 when min(5, the number of expected notes) of the expected notes are in the first 5 notes, else 0. MRR uses the rank of the first expected note; no expected note in the first 20 gives 0.
The verdict of bench compare:
A synthetic set: B wins when the interval of B − A at its Holm level is above 0 for recall@5 or for MRR, the point difference of the other measure is not below 0, and the p95 search time (scan included) is below 500 ms. The report also gives the sign of the difference in each vault.
A real set does not decide a win. It checks that B does not get worse: the one-sided 95% lower end of B − A (the lower end of the interval at the level 0.10, with no Holm correction, because each B has its own check) must be above −margin for recall@5 and for MRR. It also checks the p95 time.
The synthetic set
ragnarok synth makes a synthetic vault with known answers in a work folder, one folder for each seed. Run the
commands in a checkout: they read the prompts in bench/synthetic/prompts/ and call claude -p (no tools, no
settings, no CLAUDE.md).
Command | What it does |
| Builds the seeded fact graph of the vault |
| Writes each note with |
| Writes each question from its fact tuple only, with the forbidden words and the instruction of its stratum, and gives it to a blind |
| Checks the graph hash, the property sets, the strata and the halves, that each value is in its note and in no other note (a distractor holds no gold value), that each note states the property values that its questions use (a sibling note its own values), the length of each long note and the position of its fact, the forbidden words and the answer of the blind check. It lists each failed check, writes |
| Freezes verified work folders into one set folder: |
The deny list holds the terms that must never be in a synthetic set: vault names, real names, real paths. It stays
outside the repo, in RAGNAROK_SYNTH_DENY, else data/synth-deny.txt of the home folder: one term on each line, #
starts a comment, no case, whole words for a term of letters. The tests marked local call claude -p and read the
deny list; CI skips them, uv run pytest -m local runs them.
To measure a frozen set, unpack vault.tar.gz into a folder and add [corpora.<set>] with that folder as root and
the vaults seed<n> of the manifest.
Run each command with uvx ragnarok-mcp, for example uvx ragnarok-mcp search "boiler", or with uv run in the
checkout.
Where things are
Everything is in the home folder: RAGNAROK_HOME, else the checkout, else ~/.ragnarok. In a checkout, git
ignores config.toml, data/ and logs/.
Path | What |
| The configuration of this computer. |
| The SQLite index. Copy it and |
| The embedding cache that all index files share. Its key is the model, the dimension, the document template and the text hash. |
| The index of a bench variant on a corpus. The MCP server never uses it. |
| The results of |
| The vault archives that |
| The output of |
| Your question sets ( |
| The MCP server log, one line for each tool call with its time. |
Ollama keeps the model in ~/.ollama.
Repository layout
This repository holds the MCP server and the Claude Code plugin marketplace ragnarok.
src/ragnarok/ tests/ the MCP server, the PyPI package ragnarok-mcp
pyproject.toml server.json the package and the MCP Registry entry
CHANGELOG.md the changelog of the server up to 2.1.1
.claude-plugin/marketplace.json the marketplace "ragnarok": lists each plugin and its folder
plugins/<name>/ one folder for each plugin, with .claude-plugin/plugin.json
plugins/CHANGELOG.md the changelog of the plugins up to 0.5.0
templates/plugin/ the skeleton for a new plugin
scripts/ the validation of the pluginsThe server and the plugin have separate versions and tags: server-vX.Y.Z and ragnarok--vX.Y.Z.
The GitHub releases are the changelog.
Development
Server
uv run pytest --cov # fails below the coverage floor (fail_under in pyproject.toml)
uv run ruff check . && uv run ruff format --check .
uv run mypy # strict type check of src/
uv audit --preview-features audit-commandPlugins
Load a plugin from its folder for one session:
claude --plugin-dir plugins/<name>Or add the checkout as a local marketplace. Then /reload-plugins reads your edits with no new version:
claude plugin marketplace add <path of the checkout>To add a plugin, copy templates/plugin/ to plugins/<name>/, replace PLUGIN_NAME and
ONE_LINE_DESCRIPTION, and add an entry to .claude-plugin/marketplace.json. scripts/validate-plugins.sh
validates all plugins. Add the plugin as a package to .github/release.json. Do not change a plugin
version by hand.
Pull requests and releases
All changes to main go through a pull request. The PR is merged with squash, so the PR title is the commit
title on main. The PR title follows Conventional Commits and sets the
release:
PR title | Release | Example |
| major |
|
| minor |
|
| patch |
|
| none |
|
The scope names the package: server or ragnarok (the plugin). .github/release.json gives the packages,
their folders, their tags and their version files:
Package | Files | Tag | Version files |
| the repository, except |
|
|
|
|
|
|
Only a package with changed files gets a release. A change to a shipped file of the plugin is a change of
its behavior, so the plugin gets at least a patch release, also for a docs: or refactor: title.
The
versionjob (pr.yml) computes the next version of each changed package from the PR title and commits the version files to the PR branch. Do not change a version by hand.The required checks are
pr-title,versionandci-gate(ci.yml).After the merge,
release.ymlcreates the tag and the GitHub release of each package whose version changed. It pushes no commit. The GitHub release is the changelog.A tag
server-vX.Y.Zstartspublish.yml. It checks that the versions match the tag, builds the package, publishes it to PyPI with trusted publishing, then publishesserver.jsonto the MCP Registry.
The tags v1.0.0 to v2.1.0 are the server releases before the plugins moved into this repository. The tag
plugin-v0.5.0 is the last plugin release before the tag form ragnarok--vX.Y.Z.
License
MIT. See LICENSE.
Available Tools
6 toolsfind_by_jiraC
The notes that contain a Jira key, for example ABC-123.
Inputs: key (required). Returns: object
find_projectsB
Find the earlier projects that a prompt continues or resembles, ranked by their matching notes.
Each project has its slug, vault, folder, description, stage, state, next action and matching notes.
Inputs: query (required), limit, vault. Returns: object
list_projectC
All notes of a project in any folder, with type, status and description, with stage, state, next action.
Inputs: project (required), vault. Returns: object
open_noteA
The text of a note (or of one section, by heading), its frontmatter, links and backlinks.
outline=true gives the sections (heading path, words, first sentence) instead of the text. A note of more
than 4000 words gives the outline unless outline=false; then open the section you need by heading, as the
last heading or the full path such as "Results > Inferences".
Credentials in the text are replaced with [REDACTED]. Private notes are refused.
Inputs: path (required), heading, outline. Returns: object
open_projectsA
The projects that are open, grouped by the state in their SUMMARY.md, newest update first.
states defaults to in-progress and paused; the other values are done, superseded and abandoned. Each project has its slug, vault, folder, description, stage, state, gate, updated date and next action.
Inputs: vault, states. Returns: object
searchB
Hybrid search (meaning and keywords) over the vault notes. Returns notes, each with its best section.
Filters: vault (the name of a vault folder), project (folder slug), area, type (research,
spec, plan, summary, drivers, guide, note, ...). include_private works only when the user allows it.
Inputs: query (required), area, type, limit, +4 more. Returns: object
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 6 tools
The three project tools overlap heavily: find_projects and open_projects both return the same project shape (slug, vault, folder, description, stage, state, next action), differing only in ranking vs. state filter, so an agent can easily pick the wrong one. 'open_projects' also sounds like it opens something but actually lists, which blurs the line with open_note, and search vs. find_by_jira can both plausibly surface notes by key.
All names are snake_case, but the verb vocabulary is inconsistent: search, find_*, list_*, open_* are mixed, and 'open' means two different things (list projects vs. read note text). Readable, but not a predictable verb_noun pattern.
Six tools is well-scoped for a read-oriented notes vault: discovery (search, find_projects, find_by_jira), project views (open_projects, list_project), and retrieval (open_note). Each tool has a job and nothing looks padded.
The surface is entirely read-only: there is no way to create, append, or update a note, change project state, or move a note between stages, which are natural lifecycle operations for this domain. There is also no tool to enumerate vaults/folders or browse structure, so agents must reach everything through search filters.
Maintenance
Related MCP Connectors
Search and reason over your Obsidian-style Markdown vault, right from ChatGPT.
Connect AI assistants to your GitHub-hosted Obsidian vault to seamlessly access, search, and analy…
Search your Obsidian vault to quickly find notes by title or keyword, summarize related content, a…
Work on your own computers from Claude or ChatGPT: run commands, edit and search files.
Related MCP Servers
- AlicenseAqualityCmaintenanceEnables Claude Code read/write access to an Obsidian vault, including creating, editing, searching, and browsing notes.8MIT
- AlicenseNot gradedqualityCmaintenanceConnects Claude to a local Obsidian vault, enabling listing, reading, searching, and optionally editing notes via natural language, with all operations running locally.3,512 npmMIT
- AlicenseNot gradedqualityBmaintenanceLets Claude Code and Claude Desktop retrieve and explore an Obsidian vault through tools for semantic search, reading notes, following graph neighbors, and listing topics.MIT
- AlicenseNot gradedqualityCmaintenanceGives Claude Code read-only semantic search over an Obsidian vault by reading the embedding files the Smart Connections plugin already produces, so no API key or network is needed at query time and it works across the WSL/Windows boundary. Exposes tools for meaning-based note and block search with folder scoping, related-note discovery, note reading, and index health checks.103 npmMIT