MCP tools

One MCP entrypoint serves the whole corpus. The surface is the published MCP surface v1 contract: tool names and output shapes ARE the contract — adding a tool is a minor version, changing any existing name or shape is a major.

Transports

transporthowauth
stdio (default)curator mcp serve — any MCP client gets the whole corpus with one config entryprocess-local; none needed
streamable HTTPcurator mcp serve --http (or [mcp].transport = "http"), bound to [mcp].http_bindbearer token, required — env-injected via [mcp].bearer_token_env; the server refuses to start without it

Client-by-client status and config snippets: Tested MCP clients.

The six tools

toolargsreturns
kp_search{query, k?=10, mode?="hybrid"|"vector"|"fts"}ranked notes: id, title, path, snippet, score
kp_get_note{id} (any namespace)full note content + frontmatter + index metadata
kp_related{id, k?=10}embedding-nearest notes
kp_recent{days?=7, kind?}recently ingested/changed notes
kp_propose{title, rationale, files:[{path, content}]}proposal id (writes via proposals/v1 ONLY)
kp_digest_latest{}latest librarian digest note

Output shapes (normative)

Every tool returns its result as MCP structured content with exactly these JSON shapes (? marks nullable fields; timestamps are RFC 3339 UTC strings). The server also advertises them as tool output schemas.

mode echoes the mode that served the query; hits are best-first and score is comparable only within one response:

{"mode": "hybrid",
 "results": [{"id": "…", "title": "…", "path": "…", "snippet": "…", "score": 0.9}]}

kp_get_note

{"id": "…", "title": "…", "path": "…", "content": "…",
 "frontmatter": {"tags": [], "source": "…?", "created": "…?",
                 "updated": "…?", "checksum": "…?"},
 "index": {"ingested_at": "…", "links": [{"to": "…", "kind": "…"}]}}

Same hit shape as kp_search, anchor excluded:

{"id": "…", "results": [{"id": "…", "title": "…", "path": "…", "snippet": "…", "score": 0.9}]}

kp_recent

kind is an identity namespace (curio | zotero | kp | path); newest first by index write time, at most 50 rows:

{"days": 7, "kind": null,
 "notes": [{"id": "…", "title": "…", "path": "…", "tags": [],
            "source": "…?", "updated": "…?", "ingested_at": "…"}]}

kp_propose

dir is the vault-relative proposal directory:

{"id": "<ULID>", "status": "open", "dir": ".kp/proposals/<ULID>", "files": ["…"]}

kp_digest_latest

digest is null when no digest exists yet:

{"digest": {"id": "…", "title": "…", "path": "…", "content": "…",
            "created": "…?", "ingested_at": "…"}}

Errors

Tool-level failures (unknown id, invalid proposal, missing index, …) surface as MCP tool errors (isError: true) with a human-readable message — never as protocol failures.

Binding rules

  1. Tool names and shapes ARE the contract. Adding a tool is a minor version; changing any existing name or shape is a major version.
  2. kp_propose is the only write verb. There is no tool that writes canonical content directly — every write rides proposals/v1 and waits for human application. No exception exists anywhere in the surface.
  3. id arguments accept every kp-note/v1 identity namespace (curio: | zotero: | kp: | path:).
  4. When transport = "http", the bearer token (env-injected, see Configuration) is required — there is no unauthenticated network mode.