Skip to main content
Glama

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 -d

Windows (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 -d

Either 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 -d

    It 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/mcp

Then 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 automatically

  • Yours 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.

IMPORTANT

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 --build

Migrations 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 tools
append_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_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_truncated and pass next_offset back as offset for the rest. A windowed reply carries headings — the H1–H3 outline with character offsets — so name one in section to get that heading and its body alone instead of paging through the note.

  • resolve_links:true also 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_at you read back as expected_updated_at when you write. content_updated_at is the one that moves only on a real edit to this note: a rename elsewhere rewriting a [[link]] inside it touches updated_at but 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 section is the heading its excerpt came from — pass that string to get_note's section to get that part alone. If a long hit has no section, get_note with limit:1000 returns its headings outline; pick one and re-read with section. For prose without headings, a hit's excerpt_offset goes to get_note as offset.

  • A hit is a candidate, not an answer. relevance is relative to the best hit in this response, so the top result always reads 1.0 — an ordering, not a verdict; best_score is that hit's own raw similarity. matched_by says 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:true means the query occurs verbatim in that note; text_tier of "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_more with next_offset pages 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

    A3.9/5.0

    Scored across 18 tools

    Disambiguation5/5

    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.

    Naming Consistency4/5

    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.

    Tool Count4/5

    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.

    Completeness4/5

    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

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      B
      maintenance
      A 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.
      2
      MIT
    • A
      license
      Not graded
      quality
      F
      maintenance
      A self-hosted persistent memory platform for AI agents and humans offering tools for memory storage, search, beliefs, work management, and code intelligence via MCP.
      7
      GPL 3.0
    • A
      license
      Not graded
      quality
      D
      maintenance
      Self-hosted personal knowledge base with semantic search, enabling AI agents to capture, search, and manage thoughts using PostgreSQL with pgvector.
      12 npm
      ISC
    • A
      license
      Not graded
      quality
      C
      maintenance
      Self-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