Skip to main content

Developer docs

Koru's built-in MCP server

Koru ships an MCP server, koru-mcp, next to the app on Windows, macOS and Linux. An MCP client such as Claude starts it as a local process and reads and edits your Koru documents through it. There is no plugin to install and no API key: the server talks only to Koru on your own computer.

Claude Code in a terminal, asked to write 20 dad jokes in a Koru workspace, reports the new page it created. A second agent in the next pane sorts the jokes by rank, and the Koru window in front shows the finished 20 Dad Jokes document, ranked 1 to 20.
Claude Code writes 20 dad jokes into a Koru workspace through the MCP server, a second agent sorts them by rank, and the result is an ordinary Koru document.

How it works

The server is a small executable, koru-mcp (koru-mcp.exe on Windows), installed beside Koru. It speaks MCP over standard input and output: your MCP client launches koru-mcp serve --stdio as its own child process and stops it when it disconnects. While it serves, stdout carries only MCP messages and diagnostics go to stderr and a log file.

The bridge does not open your files itself. It forwards each tool call to Koru on the same computer, which answers from your local database, applies edits through the same model the app uses, and only reports success once the change is saved.

Koru is native, not Electron, and open source under MPL-2.0, so you can read every line of this server on GitHub.

Claude and your notes, no plugin, no API key

Koru puts the exact command for your installation on its Welcome page. You paste it into Claude, and that is the whole setup. Nothing is installed inside Koru, no key is generated, and no certificate has to be trusted.

In Claude Code it is one command. Claude Desktop has no command for local servers, so there you add one entry to Claude's own config file.

Copy the command from Koru

  1. Install Koru and open it: 0.1.3 or later on Windows, 0.1.4 or later on a Mac. On a Mac, move Koru.app to Applications first instead of running it from the DMG, and open Koru once before you connect Claude. On Linux, extract the tar.gz into a folder of its own.
  2. On the Welcome page, find the row "Manage documents over MCP" and click Copy (clicking the command itself copies it too). The row is in Koru for Windows from 0.1.3 and for Mac from 0.1.4; on an older Koru, use the paths below.

The command is the full path to koru-mcp beside your Koru app, followed by serve --stdio, for example "C:\path\to\Koru\koru-mcp.exe" serve --stdio. A path containing a space is wrapped in quotes. On macOS the bridge is inside the app bundle, at /Applications/Koru.app/Contents/MacOS/koru-mcp. On Linux it is koru-mcp in the folder you extracted Koru into, for example /home/you/koru/koru-mcp.

Claude Code

  1. In a terminal, type claude mcp add koru -- and paste the command after it.
  2. Run claude mcp list (or /mcp inside a Claude Code session) and check that koru shows as connected.
  3. Start claude and ask it to use Koru, for example: "Use Koru's get_documentation tool for koru://docs/model, then list my Koru workspaces and documents."
Terminal, Windows
claude mcp add koru -- "C:\path\to\Koru\koru-mcp.exe" serve --stdio
Terminal, macOS
claude mcp add koru -- /Applications/Koru.app/Contents/MacOS/koru-mcp serve --stdio
Terminal, Linux
claude mcp add koru -- /home/you/koru/koru-mcp serve --stdio

The -- separates Claude Code's own options from the command that starts the server. claude mcp add stores the server for the current project by default; add --scope user before koru to have it in every project.

The /mcp panel in Claude Code, showing the tools for koru with each tool's name and whether it is read-only or destructive, starting with add_image, compare_document_versions, create_document and edit_document.
Run /mcp in a Claude Code session and open koru to see every tool the server publishes, marked read-only or destructive.

Claude Desktop

  1. Open Claude Desktop's config file, claude_desktop_config.json. On Windows it is %APPDATA%\Claude\claude_desktop_config.json; on macOS it is ~/Library/Application Support/Claude/claude_desktop_config.json.
  2. Add a koru entry under mcpServers. Put the path from the copied command in command, without the surrounding quotes and with every backslash doubled (on macOS, /Applications/Koru.app/Contents/MacOS/koru-mcp as it is), and serve and --stdio in args. Merge it with any servers already there; JSON allows no trailing commas or comments.
  3. Quit Claude Desktop completely, including its icon in the notification area or menu bar, and open it again. Claude starts and stops the bridge itself.
  4. Click the + button under the chat box and choose Connectors to check that Koru and its tools are listed, then ask Claude to read or edit a Koru document.
claude_desktop_config.json, Windows
{
  "mcpServers": {
    "koru": {
      "command": "C:\\path\\to\\Koru\\koru-mcp.exe",
      "args": ["serve", "--stdio"]
    }
  }
}
claude_desktop_config.json, macOS
{
  "mcpServers": {
    "koru": {
      "command": "/Applications/Koru.app/Contents/MacOS/koru-mcp",
      "args": ["serve", "--stdio"]
    }
  }
}

On Windows you can check the file is valid JSON before restarting Claude with Get-Content "$env:APPDATA\Claude\claude_desktop_config.json" -Raw | ConvertFrom-Json in PowerShell.

Compared with a typical Obsidian setup

Obsidian has no MCP server of its own. The common routes go through the Local REST API community plugin, which now includes an MCP endpoint, or through a separate server such as mcp-obsidian that calls that plugin. This is what their own READMEs ask for, next to what Koru asks for.

TopicKoruObsidian, Local REST API pluginObsidian, mcp-obsidian
Inside the notes appNothing to install: the server ships with KoruInstall and enable the Local REST API community pluginThe same plugin, plus the mcp-obsidian server
CredentialNone. The server only reaches Koru on your computerAPI key from the plugin settings, sent as a bearer headerAPI key from the plugin settings, in the server's env
Transportstdio, started by the clientStreamable HTTP at https://127.0.0.1:27124/mcp/ with a locally generated certificate to trust, or a plain HTTP port you enablestdio, calling the plugin's REST API
Claude CodeOne command: claude mcp add koru -- <command>One command with --transport http and an --header carrying the keyThe README documents a JSON config entry
Claude DesktopOne entry in claude_desktop_config.jsonOne entry that runs mcp-remote through npx (needs Node.js) with the key headerOne entry that runs uvx (needs Python 3.11+ and uv) with the key in env
Notes app must be openNo. The bridge starts Koru's background service if it is not runningYes, the server runs inside ObsidianYes, it calls the plugin running inside Obsidian

To be fair to Obsidian: the Local REST API plugin exposes things Koru's server does not, such as running Obsidian commands, deleting, moving and copying files, event streams and uploads through signed URLs.

Checked on October 30, 2026 against:

Other MCP clients

Any MCP client that can launch a local stdio server can use the same command. Koru's repository documents these:

Codex CLI
codex mcp add koru -- "C:\path\to\Koru\koru-mcp.exe" serve --stdio
Codex, ~/.codex/config.toml
[mcp_servers.koru]
command = "C:\\path\\to\\Koru\\koru-mcp.exe"
args = ["serve", "--stdio"]

ChatGPT desktop: Settings, MCP servers, Add server, choose STDIO, then enter the path as the command and serve --stdio as the arguments, and restart ChatGPT.

The bridge also takes --tool-pages on|off. Off (the default) answers tools/list in one reply; on splits it into pages under 7,000 bytes for clients that follow MCP pagination.

Tools

Every tool publishes a title, a description, an input schema and MCP safety hints. Read-only tools publish readOnlyHint: true; every tool publishes openWorldHint: false. No tool publishes an output schema, but every result is a JSON object in structuredContent, and a failure is { code, message, details? } with isError set. Request objects reject unknown fields.

IDs are UUID strings. A field marked ? is optional.

Read tools

get_documentationRead only

Returns the full model and editing contract, koru://docs/model, for clients that cannot read MCP resources. Read it before the first change.

Inputs
uri (only koru://docs/model)
Result
{ uri, mime_type, text }
Hints
readOnlyHint

list_workspacesRead only

Lists the Koru workspaces this connection can reach.

Inputs
none
Result
{ workspaces: [{ id, owner, name, created_at, last_opened_at?, icon?, color?, pinned, archived, deleted_at?, position }] }
Hints
readOnlyHint

list_documentsRead only

Lists a workspace's documents without opening their content.

Inputs
workspace_id
Result
{ documents: [{ id, parent?, ordinal, title, kind, trashed_at?, trashed_by? }] }
Hints
readOnlyHint

search_documentsRead only

Searches titles, blocks, attribute values and breadcrumbs across one, several or all workspaces, with tag and typed attribute filters.

Inputs
query, workspace_ids? (empty means all), kinds?, lifecycle? (live | trash | all, default live), sort? (relevance | observed_modified | last_used_workspace), page_size? (1 to 200), offset?, tags? (names), attributes? ([{ key, op, values?, numeric_values?, kinds }], op is is | one_of | before | after | between | exists | missing)
Result
{ hits: [{ workspace, workspace_name, document, node?, document_kind, kind, field, title, breadcrumb, snippet, spans, lifecycle, score, ... }], next_offset?, updating_workspaces, corrected_query? }
Hints
readOnlyHint

read_documentRead only

Reads a document as a complete, ordered tree of typed nodes, plus the revision token that edits need.

Inputs
workspace_id, document_id
Result
{ document_id, root_node_id, revision, schema_version, nodes }
Hints
readOnlyHint

list_filesRead only

Lists files in a workspace with stable IDs, names, MIME types, sizes, hashes and note IDs.

Inputs
workspace_id
Result
{ files: [{ file_id, name, mime, size?, hash, root_node_id, note_id? }] }
Hints
readOnlyHint

search_filesRead only

Finds workspace files by a case-insensitive filename fragment.

Inputs
workspace_id, query, limit? (at most 200, default 50)
Result
{ files: [file] }
Hints
readOnlyHint

get_fileRead only

Returns one file's metadata without reading its bytes.

Inputs
workspace_id, file_id
Result
{ file_id, name, mime, size?, hash, root_node_id, note_id? }
Hints
readOnlyHint

read_fileRead only

Reads up to 1 MiB of a file's bytes as base64 from an offset; continue until eof.

Inputs
workspace_id, file_id, offset? (default 0), length? (at most 1 MiB)
Result
{ file, offset, length, total_bytes, eof, base64 }
Hints
readOnlyHint

list_document_versionsRead only

Lists a document's saved history markers, newest first.

Inputs
workspace_id, document_id
Result
{ entries: [entry], next_before_at?, next_before_id? }
Hints
readOnlyHint

compare_document_versionsRead only

Compares two history entries using Koru's typed node model, not Markdown: added, removed, moved, reordered and modified nodes.

Inputs
workspace_id, left_entry_id, right_entry_id
Result
{ unchanged_nodes, changes }
Hints
readOnlyHint

Write tools

create_documentWrites

Creates a document, optionally with its whole body, in one atomic call. It only becomes visible once complete, and returns what read_document would.

Inputs
workspace_id, title, kind? (page), parent_document_id? (nests it; absent means the workspace root), idempotency_key? (UUID, replays for seven days), root_node_id? (required with initial_operations), initial_operations? (insert, insert_inline, set_done, set_language only)
Result
{ document_id, root_node_id, revision, schema_version, nodes }
Hints
destructiveHint false, idempotentHint false

edit_documentWrites

Applies a batch of edit operations, in order and atomically, at the revision from the latest read_document.

Inputs
workspace_id, document_id, base_revision, operations (see Edit operations)
Result
{ revision, history: entry }
Hints
destructiveHint true, idempotentHint true

ensure_file_noteWrites

Returns a file's note, creating it when the file has none. The note is an ordinary document for read_document and edit_document.

Inputs
workspace_id, file_id
Result
{ note_id, created }
Hints
destructiveHint false, idempotentHint true

extract_to_documentWrites

Moves whole blocks into a new or existing document and leaves one /document link where they were.

Inputs
workspace_id, source_document_id, base_revision, source_node_id, extra_source_node_ids?, expected, and exactly one of new_document_title or target_document_id, with under_node_id? for an existing target
Result
{ document_id, document_revision, source_revision, source_link_node_id, history: { document, source } }
Hints
destructiveHint true, idempotentHint false

save_document_versionWrites

Saves a pinned version of the document as it is now.

Inputs
workspace_id, document_id, label?, message?
Result
entry: { id, document, version, reason, recorded_at, label?, message?, boundary?, pinned }
Hints
destructiveHint false, idempotentHint false

restore_document_versionWrites

Restores a saved version by writing new, forward-moving changes, with history markers before and after.

Inputs
workspace_id, document_id, entry_id, base_revision
Result
{ revision, history: entry }
Hints
destructiveHint true, idempotentHint true

The server publishes 21 tools, 15 that read and 6 that write. A few more tools than are listed here are not covered by this page yet.

Edit operations

edit_document takes a list of operations, each an object with an op. Every operation on an existing node carries expected, the exact text read_document returned for it, so a change never lands on content the client did not read. Positions are given by node IDs and after_node_id (null means first); numeric positions are not accepted. at_utf8 is a UTF-8 byte offset into expected.

opFieldsWhat it does
replace_textnode_id, expected, textReplaces one node's text.
insertnode_id (new UUID), parent_id, after_node_id?, kind, textInserts a block. kind is paragraph, quote, listblock, table, code, contact, divider or task; body blocks go under the document-title node.
insert_inlinenode_id, expected, at_utf8, inlineInserts a /document link ({ type: document, document_id, node_id?, label? }) or a file attachment ({ type: attachment, file_id, label? }) into a text node.
movenode_id, expected, parent_id, after_node_id?Moves a block and its children within the document. The title node cannot be moved.
removenode_id, expectedRemoves a block and everything under it. The title node cannot be removed.
set_donenode_id, expected, doneChecks or unchecks a checkbox block.
set_languagenode_id, expected, language (null for auto-detect)Sets a code block's language; refused on any other kind.
set_attributenode_id, expected, key, valueSets a typed attribute (koru.due, koru.priority, koru.color or a key the workspace defines). value is date, datetime_zoned, datetime_floating, text, number, bool or url.
clear_attributenode_id, expected, keyRemoves every value under one key.
add_tagnode_id, expected, tag or tag_idTags a block by name, creating the tag (and its parents) if needed.
remove_tagnode_id, expected, tag or tag_idRemoves one tag. A name that matches nothing changes nothing.

The server never accepts Markdown or raw CRDT operations; it translates these semantic operations through Koru's own document model. The whole batch is validated on a copy first, so it applies completely or not at all.

Resources and prompts

The server publishes one MCP resource, koru://docs/model (text/markdown): the document model and editing contract, the same text get_documentation returns. Some clients and proxies do not forward resources, so the tool is the portable way to read it.

The server publishes no prompts. Its capabilities are tools and resources, and its instructions ask the client to read koru://docs/model before its first change.

What Claude can and cannot see in your notes

  • Every workspace, read and write. The built-in connection acts as you on this computer, with read and edit access to every workspace Koru has here. There is no per-workspace selection and no read-only mode for it today. Reach covers documents in the trash (search with lifecycle: trash), file bytes through read_file, and version history.
  • Only what the client asks for. Nothing is pushed to the client. Claude sees what its tool calls return, and only while it is connected.
  • Nothing leaves your computer through the server. The bridge talks to Koru over a local connection: Koru's service listens on 127.0.0.1 only. The one way your notes leave the machine here is what the MCP client itself does with tool results, such as Claude sending them to Anthropic as part of the conversation under Claude's own terms.
  • Changes are ordinary edits. An agent's changes are saved to your local database like your own, with history markers before and after each one, so you can compare or restore them. If you sync between your devices, they travel the same way.
  • Local logs, without your queries. The bridge writes a rotating log, koru-mcp.log, in Koru's local logs folder: tool names, timings and error codes. Search text is not logged.
  • What it cannot do. There are no tools to delete or rename a whole document, move a document to another place in the workspace, add files, or send raw CRDT operations.
  • Moving blocks inside a document. The move operation keeps the block's node ID, carries expected so a concurrent split cannot move the wrong half, and refuses to move the title node.
  • Moving blocks to another document. extract_to_document writes the destination first and only touches the source once the destination is saved, then leaves a /document link where the blocks were. If the source step fails, the error names the new document and the source is unchanged: content can be duplicated, never lost. It is the same machinery as the app's own Move to document.
  • Links survive moves. A /document link stores the target's stable document ID (and optionally a block ID), not its title or location, and is resolved when it is opened, so moving the target later does not break it. A block ID that no longer resolves falls back to opening the document.
  • Renames. There is no rename tool. A document's title is the text of its title node.
  • Conflicts. Every change to an existing block must include that block's exact current text, or the call fails with precondition_failed (stale_node if the block is gone) and the client reads again. Each batch is checked as a whole before any of it is applied, and base_revision must come from the document's own history. The server never guesses which change should win.

When the Koru window is closed

The server is not hosted by the Koru window. Your MCP client starts koru-mcp as its own process, and the bridge looks for Koru's background service each time it needs it. If none is running, the bridge starts Koru's background agent installed beside it and waits for it, so the Koru window does not have to be open.

Claude can use Koru even when the app is closed; it starts Koru's background service on its own, on Windows, macOS and Linux. On a Mac, open Koru once from Applications before you connect Claude.

Errors and troubleshooting

  • storage_failed Koru answered, but saving failed.
  • stale_node, precondition_failed A block's text no longer matches what the edit expected, or the block is gone. Read the document again and retry.
  • revision_conflict The base_revision is not in this document's history. Read the document again for a fresh one.
  • idempotency_conflict A create_document key was reused with different input.
  • unknown_attribute, attribute_rejected The key is not defined in that workspace, or the value has the wrong shape.
  • invalid_request, permission_denied A malformed or unknown field, or a workspace outside this connection.
  • documentation_missing The installed copy of the model contract is missing; reinstall Koru.

koru-mcp doctor prints a JSON status for the connection to Koru, and koru-mcp --version prints the bridge version. If your client shows old tool schemas after an update, quit it completely so its old bridge process exits, and open it again.

Platforms

  • Windows. koru-mcp.exe ships with Koru. The Welcome page shows and copies the command from 0.1.3.
  • macOS. koru-mcp ships inside the app bundle, at /Applications/Koru.app/Contents/MacOS/koru-mcp. Move Koru.app to Applications before connecting a client, rather than running it from the DMG. The Welcome page shows and copies the command from 0.1.4.
  • Linux. koru-mcp ships in Koru for Linux (x86_64), beside koru in the folder you extracted it into. Use its full path in your client, for example /home/you/koru/koru-mcp serve --stdio.

More

Last updated

Privacy choices

Choose which optional tools Koru may use on this site. Your choice is saved in a cookie in this browser for 12 months. Privacy policy

Privacy choices

Keep you signed in, protect forms and remember your language and this choice.

Always on

PostHog and Google Analytics 4 measure page views and actions such as downloads, using cookies or local storage.

PostHog records how pages are used, with form inputs masked and private pages excluded. Needs Analytics.

Ahrefs Web Analytics measures visits and where they come from, and Google Ads measures whether our ads led to a visit or download.