Kybase
Kybase’s MCP server lets agents read, search, and maintain a self-hosted Markdown knowledge base.
Search notes with hybrid keyword/semantic/text search, including places inside one note.
Read notes by ID or title, paged or by section, with optional wikilink resolution.
Create, update, append to, and exact-text replace note content.
Soft-delete, restore, and list trashed notes.
Organize notes with folders and tags; create, rename, move, and delete folders.
Explore
[[wikilinks]]via backlinks, neighbors, and graph views.Check semantic indexing status and the active embedding model.
List notes by recency, folder, tag, or date to see what is new or recently changed.
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., "@Kybase search my notes about meeting notes"
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.
Kybase gives Claude, ChatGPT, Cursor and any other MCP-speaking agent a long-term memory you own: one Markdown knowledge base, running on your own machine, that every agent can search, read and update — and that you can open in a browser and edit by hand.
Self-hosted. Private by default. Plain Markdown. Yours.
The problem · Quick start · Connect your agent · What you get · Settings · Sharing · Backups · Upgrading
The problem
You tell your agent how the staging deploy works. It helps, and the session ends.
Tomorrow you open a new one:
You: what did we decide about the staging database?
Agent: I don't have that context — could you tell me again?
So you explain it again. Then you switch editors, and explain it there too.
With Kybase, the knowledge lives outside the agent:
You: what did we decide about the staging database?
Agent: (searches Kybase) You moved staging to its own Postgres instance so migrations could be tested against real data first — that's in "Staging environment", under "Database".
New session, same memory. Different tool, same memory.
Related MCP server: Cairn MCP Server
Quick start
One local agent
Nothing to install, no Docker, no database to run:
{
"mcpServers": {
"kybase": {
"command": "npx",
"args": ["-y", "kybase-mcp"]
}
}
}Put that in your client's MCP config and restart it. Your notes live under
~/.kybase, and kybase-mcp export vault.zip gets them back out as plain
Markdown at any time.
The full app
For the web UI, the graph view, share links, and several agents against one knowledge base:
macOS and Linux
git clone https://github.com/Kyrzin/kybase.git
cd kybase
cp .env.example .env
sed -i.bak "s/^KYBASE_SECRET=$/KYBASE_SECRET=$(openssl rand -hex 32)/" .env
sed -i.bak "s/^POSTGRES_PASSWORD=$/POSTGRES_PASSWORD=$(openssl rand -hex 16)/" .env
rm -f .env.bak
docker compose pull && docker compose up -dWindows (PowerShell) — no openssl or sed there, so the secrets are
generated by .NET instead:
git clone https://github.com/Kyrzin/kybase.git
cd kybase
Copy-Item .env.example .env
$rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
$b = New-Object byte[] 32; $rng.GetBytes($b)
$secret = ($b | ForEach-Object { $_.ToString('x2') }) -join ''
$b = New-Object byte[] 16; $rng.GetBytes($b)
$pass = ($b | ForEach-Object { $_.ToString('x2') }) -join ''
(Get-Content .env) -replace '^KYBASE_SECRET=$', "KYBASE_SECRET=$secret" -replace '^POSTGRES_PASSWORD=$', "POSTGRES_PASSWORD=$pass" | Set-Content .env
docker compose pull
docker compose up -dEither block generates the only two secrets you need and starts the stack.
Open http://localhost:3000, log in with your KYBASE_SECRET — grep KYBASE_SECRET .env shows it, or Select-String KYBASE_SECRET .env on
Windows — then connect an agent.
That installs the app and its database — around 1 GB. Notes and text search work right away.
Semantic search needs an embedding provider. Open Settings and pick one:
Google or OpenAI — paste an API key and you are done.
Ollama, on your own machine — start it once, then pick a model in Settings:
docker compose --profile ollama up -dIt is not in the default install because its image carries NVIDIA and AMD GPU runtimes whatever your hardware is — about 4 GB that someone using a cloud provider would never run. Starting it later touches nothing else: the app keeps running and no note is affected.
Connect your agent
Kybase speaks MCP over Streamable HTTP at /api/mcp. A client that supports
OAuth signs in once in your browser and gets its own token — listed under
Settings → Access and revocable on its own — so your KYBASE_SECRET
never goes into its config.
Claude Code — add it once for all your projects:
claude mcp add --transport http --scope user kybase https://your-domain/api/mcpThen run /mcp in Claude Code, pick kybase → Authenticate, and enter
your KYBASE_SECRET on the page that opens.
Moving from a secret in the config to this sign-in? Remove the old kybase
entry from the project's .mcp.json first: Claude Code prefers it to the
one above and never uses the sign-in. If that file was committed with the
secret in it, change the secret (see SECURITY.md).
That's it — the agent can now search your notes, read them, write new ones, update existing ones and link them together.
Claude Desktop and claude.ai — Customize → Connectors → Add custom
connector, same URL, then Connect and sign in. Claude connects from
Anthropic's servers, so the instance has to be reachable over HTTPS from the
internet. In Claude Desktop, remote servers go here, not in
claude_desktop_config.json.
ChatGPT — Settings → Plugins → Add → Add MCP server, type Streamable HTTP, same URL. ChatGPT then opens the Kybase sign-in page. It connects from its own servers too, so the same HTTPS requirement applies.
Cursor — ~/.cursor/mcp.json (global) or .cursor/mcp.json (project):
{
"mcpServers": {
"kybase": {
"url": "https://your-domain/api/mcp"
}
}
}Then connect kybase under Customize → MCPs and sign in.
Devin Desktop (formerly Windsurf) — ~/.config/devin/mcp_config.json
(%APPDATA%\devin\mcp_config.json on Windows), with serverUrl instead of
url:
{
"mcpServers": {
"kybase": {
"serverUrl": "https://your-domain/api/mcp",
"headers": {
"Authorization": "Bearer <KYBASE_SECRET>"
}
}
}
}That header carries your KYBASE_SECRET, which is full access to the whole
instance: use it only for a client without OAuth, and keep it in your own
config — never in a file you commit.
Clients that only speak stdio use the npx -y kybase-mcp block from
Quick start, in the same file.
What you get
Markdown, not an opaque memory blob — every note is plain Markdown that you read and edit in the browser and your agents read and edit through MCP
One knowledge base, every agent — Claude, ChatGPT, Cursor and the web UI all look at the same notes
Hybrid search — full-text and meaning-based search fused into one ranked result, so an agent finds the note whether it knows the exact wording or not
Section-level reads and writes — an agent reads or edits the part of a note it needs, not the whole file
Backlinks and a knowledge graph —
[[wikilinks]]connect related notes automaticallyYours to take — Settings → Export .zip gives plain Markdown with folders as directories, readable by any editor, Obsidian included. Import .zip merges one back.
Obsidian and Notion are built for a person reading and writing. Kybase is built for the loop between you ↔ your knowledge ↔ your agent: you edit in the browser, an agent searches and updates over MCP, and both are looking at the same Markdown. If the agent remembered something wrong, you open the note and fix the sentence.
Private by default
Kybase runs on your infrastructure. No SaaS account, no external memory service, no cloud database — your own Postgres, local embeddings through Ollama, and a secret you generate.
Cloud embedding providers are optional. Choosing one sends your notes' full text to that provider to compute embeddings; Ollama keeps everything on your machine.
How agents use it
search_notes("deployment steps for staging")
→ hit includes section: "Rollback"
→ get_note(section: "Rollback")
→ agent reads just that section, not the whole note
→ append_to_note(section: "Rollback", text: "...")Search says which section of a note matched, not just which note. On a long note that can mean reading a fraction of the content — cheaper for every step after the first search, and it is how the agent writes back too.
The server ships with instructions that teach the agent to search before
writing and to add [[wikilinks]] to related notes, so the graph grows as
the agent works instead of filling up with orphans.
Settings
Everything else goes in .env, copied from .env.example, which documents
each option. Only KYBASE_SECRET and POSTGRES_PASSWORD are required;
KYBASE_PORT changes the host port, KYBASE_BIND=0.0.0.0 opens it to other
devices on your network (by default only this machine can connect),
KYBASE_PUBLIC_URL sets the address behind your HTTPS proxy, and
KYBASE_TAG pins a version (e.g. 1.4) instead of tracking latest.
Embedding provider. Open Settings in the web UI, pick a provider (Ollama, Google or OpenAI), choose a model, add an API key if it needs one, and click Save & Apply. Switching re-embeds every note, and the database adapts to the new model's vector size on its own.
Ollama keeps everything on your machine. Google and OpenAI are convenience options: picking either sends your notes' full text to that provider.
Reranker (optional). A cross-encoder can re-order the top search
results. It is not in the default install: it needs about 800 MB of RAM
and adds a few seconds to each search on a CPU. Start it with
docker compose --profile rerank up -d; Kybase picks it up on its own,
and a switch in Settings turns it off. Without it, search works as before.
Sharing notes
The Share button on a note creates a public read-only link — rendered Markdown, no login, wikilinks shown as plain text so nothing else in your vault is reachable. The link is the access: revoke links you no longer need under Settings → Active share links.
Backups
Everything lives in one Postgres volume, so a nightly pg_dump is one line.
Full recipe including cron and restore: docs/backup.md.
Upgrading
# prebuilt image
docker compose pull && docker compose up -d
# or rebuild from source
git pull && docker compose up -d --buildMigrations apply automatically on startup. Details: docs/upgrading.md.
More documentation
SECURITY.md (threat model) · CONTRIBUTING.md (running it locally, opening a PR) · docs/backup.md · docs/upgrading.md · packages/kybase-mcp (the standalone stdio package)
License
AGPL-3.0 — free to use, modify, and self-host. If you run a modified version as a network service, you must make its source available to your users under the same license.
For a commercial license (e.g. embedding Kybase in a closed-source product or service), contact the author.
Copyright © Denis Kurzin (https://github.com/Kyrzin)
Available Tools
18 toolsappend_to_noteA
Add text to a note without resending the rest — prefer it over update_note for journals, logs and running lists. A blank line separates your text from what was there. The note is locked for the read-modify-write, so two sessions appending at the same moment keep both additions instead of the later one overwriting the earlier. Re-embeds in the background like any content change.
Inputs: content (required), at, id, title, +1 more.
create_folderB
Create a new folder. Optionally nested under a parent.
Inputs: name (required), parent_id.
create_noteA
Create a new note. Embedding is generated automatically in the background. The server instructions' wikilink and tag rules apply: search_notes for the topic first and link the related notes it finds, and call list_tags before coining a new tag.
Inputs: title (required), tags, content, folder_id, +1 more.
delete_folderA
Delete a folder and its full subtree of child folders (cascade). Every note inside — including notes in nested subfolders — is soft-deleted into the trash along with it (see delete_note), recoverable via restore_note within the retention window. To preserve organization instead, move notes/subfolders out first.
Inputs: id (required).
delete_noteA
Soft-delete a note by id — it disappears from list_notes/search/get_note/the graph, but is recoverable with restore_note for 30 days before being purged for good. Use list_notes with trashed:true to see what's currently in the trash.
Inputs: id (required).
get_backlinksA
Notes that link to this one via [[Title]] wikilinks, by id or title. Each comes back as id/title/folder_path plus a snippet around the link; include_content:true returns their full text instead, which is expensive when many notes link here — prefer get_note on the ids you actually want. offset and limit count notes, not characters.
Inputs: id, limit, title, offset, +1 more.
get_graphA
Many notes at once and the edges between them — the heavy graph tool, and the only one that reaches for the whole vault. For one note's surroundings use get_neighbors; for who links to it, get_backlinks.
Nodes are {t} (t = title). Edges reference nodes BY POSITION in the nodes array: edges[0] = [2, 5] means nodes[2] links to nodes[5]. semantic_edges are undirected and come from embedding similarity rather than written links, with the cosine as a third number. unresolved_links are [[wikilink]] targets matching no note title — dangling, so they have no node index.
Unscoped this reaches for the ENTIRE vault and stops at max_nodes: truncated: true means you hold a recency-ordered prefix, NOT the shape of the vault. Scope with folder_id or root_title+depth rather than raising the cap.
Inputs: depth, folder_id, max_nodes, min_score, +3 more.
get_neighborsA
What is around ONE note in the [[wikilink]] graph, out to depth hops: a flat list of titles, no node indices to decode and no whole-vault payload. For the shape of that neighbourhood — which of them link to each other — use get_graph with root_title instead.
Traversal is undirected: a note linking HERE counts as much as one linked FROM here. links_out/links_in describe the direct relation and appear only at depth 1 — two hops out, "which way does the arrow point" has no answer. Each note appears once, at the shortest depth that reaches it.
These are links people wrote, not similarity: a note on the same subject that nobody linked is not here, and an empty result is a fact about the writing rather than about the topic. Rows carry the title, which is what get_note, get_backlinks and this tool all take.
Inputs: id, depth, limit, title.
get_noteA
Read one note, by id or title.
Long notes come back windowed — 20000 chars by default: check
content_truncatedand passnext_offsetback asoffsetfor the rest. A windowed reply carriesheadings— the H1–H3 outline with character offsets — so name one insectionto get that heading and its body alone instead of paging through the note.resolve_links:truealso returns the notes this one links to, id/title only unless you add include_content:true, whose text is capped at 4000 chars — call get_note on an id for the whole of one. Targets that match no note are listed as unresolved.Pass the
updated_atyou read back asexpected_updated_atwhen you write.content_updated_atis the one that moves only on a real edit to this note: a rename elsewhere rewriting a [[link]] inside it touchesupdated_atbut not that.
Inputs: id, limit, title, offset, +3 more.
indexing_statusA
Semantic index progress: total/indexed/pending, complete=true when pending is 0. Pending notes are still found by text search; ones never embedded stay out of semantic results until the background pass reaches them.
Also names the active embedding model and the configured similarity cutoff (semantic_min_similarity, null when there is none — the default, meaning nothing is refused for being too dissimilar). Cosines are not comparable between models, which is why the model is named rather than left to be guessed from a score.
Inputs: no named parameters.
list_foldersA
List folders with the full path already resolved — no need to walk parent_id yourself. Pass a folder's own id as parent_id to create_folder/update_folder to nest under it. Sorted by path, so a page is a contiguous slice of the tree read top to bottom; the reply is {folders, has_more, next_offset?}.
Inputs: limit, offset.
list_notesA
List notes newest first, filtered by folder, tag or date. This is the tool for "what is new" and "what changed lately": search_notes ranks by relevance and never by recency.
created_* is when a note was made, updated_* when its own text last changed — a rename elsewhere rewriting a [[link]] inside it does not count as an edit here. Each row carries content_length, so you can tell a long note from a short one before spending a get_note call. With trashed:true the other filters are ignored.
Inputs: tag, sort, limit, offset, +7 more.
list_tagsA
Tags in use with the number of notes carrying each, most-used first. The default limit cuts off the one-off tail — a tag used once is not one worth reusing — so raise it to see those.
Inputs: limit.
replace_in_noteA
Replace exact text in a note without resending the rest. Either find/replace or old_string/new_string — the same pair, either spelling.
For several replacements pass edits rather than the singular fields (one lock and one re-embed for the batch, not one per call); the two forms cannot be combined in one call. Edits apply in order, and each find is matched against the note as the edits before it already changed it — an earlier edit can create the text a later one needs, or destroy it, so sequence them. If any step's count is wrong the whole batch is refused and the note is left untouched, and the error names the failing index.
Inputs: id, find, edits, title, +5 more.
restore_noteA
Undo delete_note: brings a soft-deleted note back. Errors if the note isn't in the trash (never deleted, already restored, or purged past the retention window), or if a live note has since taken the same title (rename one of them first, then retry).
Inputs: id (required).
search_notesA
Search notes: hybrid (keyword + meaning) by default. Returns ranked excerpts, not whole notes.
Use type="text" for an exact identifier, filename, code symbol or quoted phrase; "hybrid" for questions and topics.
READ THE SECTION, NOT THE NOTE. A hit's
sectionis the heading its excerpt came from — pass that string to get_note'ssectionto get that part alone. If a long hit has nosection, get_note with limit:1000 returns itsheadingsoutline; pick one and re-read withsection. For prose without headings, a hit'sexcerpt_offsetgoes to get_note asoffset.A hit is a candidate, not an answer.
relevanceis relative to the best hit in this response, so the top result always reads 1.0 — an ordering, not a verdict;best_scoreis that hit's own raw similarity.matched_bysays which arms found it: semantic_score alone means "about something similar", never that it confirms your question. Quote the excerpt or open the note — a score is never evidence.exact:truemeans the query occurs verbatim in that note;text_tierof "or" or "substring" means the strict words missed and a looser pass filled in — recall, not confirmation.A long note such as a book comes back as one hit with one excerpt. To find more places in it, search again with its
note_id: results are then places inside that note, best first.Dates here filter, they do not rank. For "what changed lately" use list_notes, which sorts by recency; it is also how you browse by folder or tag, since a query is required here.
has_morewithnext_offsetpages the results.
Inputs: query (required), tag, type, limit, +10 more.
update_folderA
Rename a folder and/or move it under a different parent (set parent_id to null for top level). Provide at least one of name/parent_id. The response includes the resolved path so a rename or move can be confirmed without a follow-up list_folders call.
Inputs: id (required), name, parent_id.
update_noteA
Update note fields. Re-embeds if title or content changed. Updates wikilinks if title changed. The server instructions' wikilink and tag rules apply when substantially rewriting — in particular, call list_tags before coining a new tag. Pass expected_updated_at (the updated_at you read) to be refused instead of overwriting a change made in between.
Inputs: id (required), tags, title, content, +2 more.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 18 tools
Each tool targets a distinct verb+resource, and the descriptions actively route between the close pairs (list_notes vs search_notes, append_to_note vs update_note vs replace_in_note, get_backlinks vs get_neighbors vs get_graph). No two tools appear to do the same thing, and the explicit 'use X instead of Y' guidance removes the residual overlap.
Nearly everything is a clean verb_noun pattern (create_note, list_folders, get_backlinks, restore_note), with consistent pluralization for collections. The one outlier is indexing_status, which is a noun phrase with no verb prefix and breaks the pattern.
18 tools is on the heavier side but each maps to a real operation in a notes+folders+graph domain; nothing feels like filler. A slightly tighter surface (e.g. folding indexing_status into a broader status call) would make it ideal.
Note lifecycle is fully covered (create/read/list/update/append/replace/soft-delete/restore, search, backlinks, graph), and folders have full CRUD with cascade delete. Minor gaps remain: no tag management beyond list_tags (no rename/merge/delete of tags), no permanent purge, and no dedicated trash-listing tool (deferred to list_notes trashed:true).
Maintenance
Related MCP Connectors
Persistent memory, knowledge graph, task management and more for AI agents via MCP.
Self-hosted AI workspace: semantic RAG document search, long-term memory, and AI chat via MCP.
Persistent memory and knowledge management for AI agents with semantic search and 50+ tools.
A second brain for your AI agent. Shared memory for your team's AI agents, plus public and private knowledge bases — works with ChatGPT, Claude, Cursor and any MCP client.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceA self-hosted Markdown knowledge base and Agent Harness with an MCP server that enables AI agents to read and write notes, providing persistent memory and a shared workspace for multi-agent collaboration.2MIT
- AlicenseNot gradedqualityFmaintenanceA self-hosted persistent memory platform for AI agents and humans offering tools for memory storage, search, beliefs, work management, and code intelligence via MCP.7GPL 3.0
- AlicenseNot gradedqualityDmaintenanceSelf-hosted personal knowledge base with semantic search, enabling AI agents to capture, search, and manage thoughts using PostgreSQL with pgvector.12 npmISC
- AlicenseNot gradedqualityCmaintenanceSelf-hosted, local-first knowledge graph and memory server for AI agents. Enables agents to persist, recall, and organize knowledge through MCP with automatic distillation, deduplication, and cross-linking.MIT