MCP server reference
cdno-mcp is a Model Context Protocol server that exposes your
vault to AI clients (Claude Desktop, Claude Code, Kiro, Gemini CLI, …). It runs the same domain
engine as the CLI, so anything the assistant does goes through the same rules and lands in the same
Markdown files. To wire it up, see Connect to Claude.
Transport and vault selection
Two binaries serve the same tool catalogue:
cdno-mcpspeaks JSON-RPC over stdio — the client launches it as a subprocess. It opens the vault named byCUADERNO_VAULT_PATH, or, if that’s unset, discovers one from its working directory (the same rule as the CLI).cdno-mcp-serverspeaks MCP Streamable HTTP for remote clients — see The HTTP server for its flags and security model.
The tool surface
The server advertises 66 tools. This reference groups them by purpose:
| Group | Page | What’s in it |
|---|---|---|
| Context-gathering reads | Context-gathering tools | Orientation, project/portfolio/weekly context, search, reads, lint, triage list |
| Writes | Write tools | Log, capture, file evidence, project/action/milestone/waiting edits, note revision and renaming, commitments, tracking, daily/weekly sections |
| Creation & lifecycle | Creation and lifecycle tools | Create projects/portfolios/questions/stewardships, link portfolios, park/activate/complete/drop, status transitions |
Every tool returns typed JSON; the shapes mirror the CLI’s --json output, so a
client gets the same structures whichever surface it uses.
Focus
Four of the write tools (pause_action, switch_action, switch_unplanned_action, resume_action)
move the one “what I am on” slot that current_focus reads, and eighteen write results carry a
focus field so the focus rides along without a second call. The server never refuses a write for
being outside the focus; it refuses a second start (or a switch to the focused action, or a resume_action over a
different focus) with a coded focus_open rejection, and pause_action or resume_action with
nothing to act on with no_focus. See Write tools and
Focus.
Conventions
- Slugs, not paths. Tools take slugs (
surrogate-model), matching the CLI. - Substring matching for completing actions/milestones and resolving waiting-on items, exactly as on the CLI.
- The same rules apply. The five-project cap, append-only notes, auto-logged project-state history, and commitments aggregation all hold — the MCP server is not a back door around them.
When a call is rejected
Cuaderno splits failures along the line MCP itself draws, and the distinction is worth knowing because the two arrive in different places in the response.
A rejection you can act on comes back as a tool result with isError: true — not as a JSON-RPC
error. The response carries no error member at all, and the payload is the single text content
item, JSON-encoded exactly as a successful result’s payload is:
{ "isError": true, "content": [{ "type": "text", "text": "…" }] }
Decoding that text gives the rejection itself. This is real output from complete_action with a
query matching two bullets:
{
"code": "ambiguous_action",
"details": {
"candidates": [
"Draft the methods section (deep)",
"Revise the methods appendix (light)"
],
"query": "methods",
"slug": "ablation-study"
},
"message": "ambiguous action match for 'methods' on project 'ablation-study': [\"Draft the methods section (deep)\", \"Revise the methods appendix (light)\"]"
}
Three fields, and each has a job:
| Field | For |
|---|---|
code | A stable snake_case discriminant to branch on — ambiguous_action, state_too_long, project_cap_reached, field_not_settable, … |
message | The human-readable sentence, for showing a person |
details | The structured fields needed to recover — candidates to pick from, chars/max to condense against, active_projects to park one of |
details is the part that matters for automation. An ambiguous query hands back its candidates as
an array, so a client picks from a list rather than parsing them out of a sentence.
Five project codes carry details a client is likely to branch on:
| Code | details | When |
|---|---|---|
project_not_active | slug, status (parked, completed or dropped), and closed when the map carries a date | A parked status means a focus verb (start_action, resume_action, switch_action, start_unplanned_action, switch_unplanned_action) or park_project was refused: starting or picking up work needs an active map, so activate it first only if the user wants to work on it now. Every other project-body verb (add_action, update_project_state, add_milestone, …) works on a parked map. A closed status means a body verb on a completed or dropped project, or a close repeating the outcome it already has: the project is finished, unless the user wants it back. |
project_cap_reached | current, max, active_projects | A project would begin to count with the counted projects at the cap (activate_project, or taking back an active project with hand_off_project). current and active_projects are the projects that count, active and held by no one else, so each listed slug is one whose parking, completion or dropping frees a slot: offer those, or leave the project as it is. |
ambiguous_project | slug, candidates: [{path, note_type}] | The slug names maps in more than one of projects/, projects/_parked/ and projects/_done/<year>/. Nothing can pick for you: keep one of the files by hand. |
project_in_focus | slug, action | A project was to be handed off while action, one of its actions, is in focus: a project is not handed off while you are in the middle of its work. Pause the action first, if the person wants to hand it off now. |
empty_field (for held_by) | field: "held_by", value, remedy: "take_back" | The holder given names the person or nobody (me, tbd, someone, …, the vault’s [projects] holder_sentinels). If they cannot name who has the project, it is theirs: take it back rather than hand it off. Any other empty_field carries field alone. |
The test is simply whether a different argument would change the outcome. A slug that matches no
note is a rejection (not_found), and its message carries the list of slugs that do exist. A note
missing the section a tool writes into is a rejection. So is a name that is already taken.
A mechanical failure stays a JSON-RPC error (-32603): a disk that would not write, permission
denied, a contended write lock, an index that would not answer, a transaction that rolled back.
Nothing the caller passed can fix those, so there is nothing to branch on. Malformed calls — an
unknown tool, unparseable arguments — are protocol errors too (-32602), per the spec.
The practical upshot for a client: read isError on the result, not just the presence of error.
A rejection is a normal, expected answer to a call that was itself well-formed.
Slug rejections
An explicit slug on a create tool (Creation and lifecycle) is
refused with one of four codes, each leaving the vault untouched. They are the same objects the CLI
prints under --json.
| Code | details | When |
|---|---|---|
invalid_slug | slug, why | The slug breaks the shape rule (lowercase a-z, 0-9, single -, at most 50 characters, not YAML-typed); why names the part. Fix the shape, or ask the person for another name. |
slug_taken | slug, holder, kind (stem|folder|alias) | Another note already holds the name. If holder is the note you meant, use it; otherwise ask the person for another name. alias means the name is a former name another note still lists. Do not suffix it yourself. |
portfolio_slug_mismatch | question_slug, slug | create_portfolio with question set to a question’s slug and a slug that is not that slug. Drop slug or pass the question’s. |
slug_given_twice | field | create_custom_note with slug and a slug entry in fields. Pass one of them. |
Building skills on top
Multi-step rituals (a morning orientation, a guided weekly review) are best wrapped as Claude skills that call these tools in sequence. See Using with Claude skills.