MCP server wrapping the nb CLI for LLM-friendly note-taking
MCP server wrapping the nb CLI for LLM-friendly note-taking.
Using nb directly via shell has two problems for LLM assistants:
Backtick escaping: Markdown content with backticks triggers shell command substitution, corrupting notes.
Notebook context: nb assumes a default notebook, making per-project use awkward.
This MCP server solves both by:
Install nb by following the official instructions:
nb installation guide.
From crates.io:
cargo install nb-mcp-server
See the changelog for release history and upgrade notes.
Or download a prebuilt binary from GitHub Releases.
cargo build --release
With default notebook from environment:
NB_MCP_NOTEBOOK=myproject ./target/release/nb-mcp
Or via CLI argument (takes precedence):
./target/release/nb-mcp --notebook myproject
Disable commit and tag signing in the notebook repository:
./target/release/nb-mcp --notebook myproject --no-commit-signing
Allow new notes at the notebook root instead of requiring a folder:
./target/release/nb-mcp --notebook myproject --allow-top-level-notes
Print the installed version:
./target/release/nb-mcp --version
Show the resolved notebook path and state directory:
./target/release/nb-mcp --show-paths
Add to your MCP client configuration (e.g., .mcp.json):
{
"mcpServers": {
"nb": {
"command": "/path/to/nb-mcp",
"args": ["--notebook", "myproject"]
}
}
}
The canonical access path is the multiplexed nb tool with a command
parameter, which reduces the token footprint of the MCP server.
The args field must be a JSON object. Stringified JSON payloads are rejected.
Unknown args fields are rejected instead of ignored; use the exact command
schema fields or documented aliases.
Returned identifiers such as coordination/mcp/1 or
myproject:coordination/mcp/1 are nb selectors, not filesystem paths in the
current repository. Notebook storage is managed by nb configuration.
The notebook argument must be a bare notebook name. Use folder for folder
paths and id / selector for note selectors. Existing-item commands accept
copied selectors such as myproject:coordination/mcp/1, but reject conflicts
with a separate notebook argument.
All commands are also available as direct first-class tools with typed
schemas: add, show, delete, move, list, search, todo,
do, undo, tasks, bookmark, folders, mkdir, import,
status, notebooks, plus the body-aware tools replace_note_body,
edit_note_substring, edit_note_lines, retitle_note,
edit_note_tags, and the line tools show_note_lines,
search_note_lines. These bypass the multiplexed command dispatch. The
multiplexed nb tool remains as the compact/backcompat compatibility
surface for the retained commands.
| Command | Description | Key Arguments |
|---|---|---|
nb.add | Create a note | title, content, tags[], folder required by default |
nb.show | Read a note (structured envelope) | id (alias: selector) |
nb.delete | Delete a note | id (alias: selector) |
nb.move | Move or rename a note | id (alias: selector), destination |
nb.list | List notes | folder, tags[], limit ([ ] / [x] indicate todo status; leading glyphs are item markers) |
nb.search | Full-text search | queries[] (required), mode (any default, all), tags[] |
| Command | Description | Key Arguments |
|---|---|---|
nb.todo | Create a todo | folder required by default, title, optional description (alias: content), optional tasks[], tags[] |
nb.do | Mark complete | id (alias: selector), optional task_number |
nb.undo | Reopen | id (alias: selector), optional task_number |
nb.tasks | List todos | optional status (open or closed), optional recursive (true default) |
| Command | Description | Key Arguments |
|---|---|---|
nb.bookmark | Save a URL | url, folder required by default, title, tags[], comment |
nb.import | Import file/URL | source, folder required by default, filename, convert |
nb.folders | List folders | parent |
nb.mkdir | Create folder | path |
nb.notebooks | List notebooks only | (none) |
nb.status | Notebook info | (none) |
Create a note with code:
{
"command": "nb.add",
"args": {
"title": "API Design Notes",
"content": "# API Design\n\nUse `GET /items` for listing.\n\n```python\nresponse = client.get('/items')\n```",
"tags": ["design", "api"],
"folder": "docs"
}
}
Search for notes:
{
"command": "nb.search",
"args": {
"queries": ["API", "design"],
"mode": "any",
"tags": ["design"]
}
}
For multi-LLM projects, consider using consistent tag prefixes (optional). Example categories and prefixes:
| Category | Pattern | Examples |
|---|---|---|
| Collaborator | llm-<name> | llm-claude, llm-gpt |
| Component | component-<name> | component-api, component-ui |
| Task type | task-<type> | task-bug, task-feature |
| Status | status-<state> | status-review, status-blocked |
The legacy nb.edit tool (and its overwrite/append/prepend modes)
was removed in the nb-api 0.3 cutover. The body-aware replacement tools
are direct-only (no multiplexed nb.* aliases) and are designed to
prevent the destructive whole-note-overwrite failure mode that the old
surface enabled:
replace_note_body — replace the entire note body with plain UTF-8
new_body. Requires the body fingerprint from a preceding show; a
stale fingerprint is rejected with re-read guidance so you cannot
overwrite a note you have not just read.edit_note_substring — replace one or more occurrences of a plain-text
pattern with replacement. expected_count must match the actual
match count; an optional fingerprint guards against stale edits.edit_note_lines — apply a batch of disjoint insert/delete/replace
edits verified against line anchors from one original snapshot;
content is bare text (no terminator bytes; the library appends the
document EOL, so single-line replaces never merge the next line).retitle_note — change the title without changing the path.edit_note_tags — add and/or remove tags in one atomic operation.These tools address notes by a flat id (alias selector) string exactly
like show/delete/move; notebook-relative filenames are not exposed,
and qualified selectors returned by show/show_note_lines/
search_note_lines round-trip directly as id. Since nb-api 0.4,
numeric .index ids are surfaced too: reads and mutation outcomes carry
selector plus numeric_id, and <folder>/<id> / <id> are accepted as
id (ids are folder-local, positional, and never reused; path stays
canonical). Body content (pattern, replacement, title,
new_body, line-edit content) and line/search text/title are native
UTF-8 strings with no base64 anywhere on the wire. Line-level reads
are available through show_note_lines (bounded, anchored windows with a
document-level eol declaration — lf/crlf plus has_final_eol —
instead of per-line terminators) and search_note_lines (anchored
matches, no EOL fields by design), which support search-to-edit
without reading the whole note. New notes get nb-faithful filenames
mangled from the title (Hello World Title → hello_world_title.md).
add creates notes only: todo-shaped content (checkbox lines or a
Tasks heading outside fenced code blocks) is rejected with a hint to
use the todo tool instead.
Invoking multiplexed nb.edit is rejected with recovery guidance naming
the replacement tools. The body-aware and line tools are direct-only:
invoking them through the multiplexed nb tool is rejected.
show returns a text-first slim structured envelope: selector, path,
kind, todo_state, title (normalized Option<String>), tags, body
(full UTF-8 text), body_contiguous, fingerprint, and numeric_id.
No base64 field is exposed. When the source is not valid UTF-8, show
returns a typed NonUtf8 error carrying the MIME hint plus guidance to
an external raw-retrieval facility outside MCP (the nb CLI); a
non-textual target returns UnsupportedShowTarget. Neither ever
returns base64 bytes.add, todo, bookmark, mkdir, delete, move,
do, undo, and the body-aware tools) return a structured
CommitOutcome: commit_created, revision_id, pre_revision, and
per-operation path/selector/numeric_id/noop/fingerprint. Idempotent no-op
mutations report commit_created: false.nb-api 0.4 introduces typed failures that the MCP layer translates into
actionable diagnostics on both the multiplexed nb.* surface and the
first-class tool surface. NonUtf8 and IndexLockTimeout (busy .index
lock; retry later, nothing mutated) join the existing set:
show on a non-text selector (folder, archive, image, ...): the
error names the selector and the actual non-text type, states
that show reads text notes only, and points the caller at
folders/list. The server never silently re-routes show to
another command.add with both a title and a content whose first nonblank
line is an H1 that duplicates the title: the error names the
title and the detected heading and tells the caller to remove
the duplicate H1 or omit the separate title.FingerprintMismatch, AnchorMismatch, and
OccurrenceMismatch tell the caller to re-read the note and retry
with fresh fingerprint/anchors/counts.FragmentedBody: a multi-fragment body (for example a bookmark with
multiple body fragments) refuses line/substring/body-replace
operations; metadata operations (retitle_note, edit_note_tags)
still apply.DirtyBaseline: a mutation refuses when the notebook worktree/index
is dirty; the diagnostic tells the caller to commit or clean it first.IndeterminateCommit / RecoveryRequired: commit completion is unknown
or recovery is needed; the diagnostic instructs the caller not to
auto-retry and to inspect HEAD/status before acting.GateTimeout: the notebook is busy; retry later.PathCollision / PathIgnored / UnsupportedStructure /
PlanValidation: surface the specific path/plan guidance.Priority order:
notebook argument (highest)--notebook flagNB_MCP_NOTEBOOK environment variableIf no notebook can be resolved, commands fail with a configuration error. The
server does not fall back to nb's default notebook.
If the resolved notebook does not exist, the server creates it automatically.
Use --no-create-notebook to disable automatic creation.
Logs are written to ~/.local/state/nb-mcp/{project}--{worktree}.log (XDG-compliant).
For Git worktrees, logs are named after both the master project and the worktree basename to avoid collisions between multiple MCP server instances.
Use --show-paths to print the resolved notebook path and state directory.
By default, note-creating commands require a folder argument so agents do not
accidentally litter project notebook roots. This applies to nb.add, nb.todo,
nb.bookmark, and nb.import. Use nb.mkdir to create new folders and
nb.folders to list existing folders.
Set NB_MCP_ALLOW_TOP_LEVEL_NOTES=true or pass --allow-top-level-notes to
permit root-level note creation.
Mutating commands warn after successful writes when the notebook argument
targets a notebook other than the project default. Cross-notebook writes remain
allowed for collaboration across teams, but the warning helps catch accidental
notebook/folder confusion.
The notebook argument accepts only bare notebook names, not selector syntax.
For example, use notebook: "other-team" with folder: "todos/mcp", not
notebook: "other-team:todos/mcp".
Control log level with RUST_LOG:
RUST_LOG=debug nb-mcp --notebook myproject
Use --no-commit-signing to disable commit and tag signing in the notebook
repository. The server updates the notebook repository's local Git config so
signing prompts do not block MCP tool calls.
nb CLI. Published on crates.io. This MCP server depends on nb-api for all note-taking primitives; the body-aware editing surface, typed errors, structured results, and sanitized empty listings all come from nb-api 0.4.See the contribution guide and code of conduct:
This listing does not have a supported local package template. Use the maintainer’s documentation for its hosted endpoint, authentication, and client-specific setup. No install command has been inferred.
https://github.com/emcd/nb-mcp-server/releases/download/v0.14.0/nb-mcp-server-0.14.0-aarch64-apple-darwin.mcpbothernb-mcp-server works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.
~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.~/.cursor/mcp.jsonRestart Cursor for changes to take effect..vscode/mcp.jsonReload VS Code window for changes to take effect.~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect..mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.