Repository navigation
v0.9.0
A note too large to pull through model context, and a bulk job too repetitive for per-note tool calls, now have their own road: a byte-level HTTP transfer surface (ADR-0028). The MCP tools stay the normal way to work; this surface is the accelerator beside them.
Features
- New tool
provision_transfer. Returns a one-time ticket (300 s, single redeem) that trades for a scoped bearer token (1800 s, lapses instead of renewing — a lapsed token answers 401, like a wrong one), plus a ready-to-run curl recipe with every value already substituted. A grant can be read-only or read-write, and confined byprefixto one directory or to a single note; confinement is matched per path component, so a sibling likedir-evil/never leaks in. Transfer credentials are refused on the tool surface — they never widen back into full access. - The HTTP surface itself. An index (one JSON object per note:
path,etag,size— no content) that answersif-none-matchwith 304 for cheap polling; one-note GET/PUT where the ETag is blake3 over the exact bytes served, frontmatter and line endings included. Replacing an existing note requiresif-match(428 without it, 412 when stale,if-match: *to overwrite knowingly); creating takes no header. A subtree can be pulled in one round trip as tar, and a push can be dry-run checked before it is made. Every PUT rides the vault's per-write auto-commit, the same as a tool write. create_notesgainsbase. Pass the path of an existing note to copy it verbatim — frontmatter and body — instead of sendingcontent. Pure copy:baseexcludescontent/frontmatter(CONFLICT together, NOT_FOUND for a missing base), and edits belong to the other tools afterwards. Batch order is honored, so an item may copy a note an earlier item just created.- The vault owns its path rules. Notes are
.mdonly, names are stored in one Unicode spelling (NFC), and every way in — tools and transfer alike — obeys the same rules.
Breaking
- A note is UTF-8 text, enforced at the door. Bytes that are not valid UTF-8 are refused on write; a non-UTF-8 note synced in from outside reads as
ENCODING. - One note per transfer request. The bulk surface is the one-note primitives in a loop (the recipe ships the loop); the earlier aggregate endpoints are gone.
- A transfer token lapses instead of renewing. Renewal-by-presentation is withdrawn; provision a fresh ticket.
Fixes
- Transfer confinement hardening from adversarial review: prefix comparison composes a real path prefix, a note-confined grant no longer acts as a directory grant, protected directories are refused as prefixes, recipe URLs are percent-encoded (spaces,
#,&, CJK names), and the fetch loop is POSIX sh — no bashisms, NUL-delimited, parallel. - Recipe polish from field testing: a one-note grant drops the many-notes loop it cannot use; the PUT line warns that this surface carries frontmatter and body whole (putting back
read_notesoutput would erase the frontmatter it strips); TTL and the 401 answer of a lapsed token are stated where the token is collected. - OAuth token collection writes exactly the token — no trailing newline, so
$(cat token)is safe in a header. read_noteson a directory path reports the note as absent instead of erroring; the e2e gate expectation caught up.
Notes
- The deployed server announces its version in the MCP
instructions(md-mcp v0.9.0) after restart. - Deleting and moving are deliberately not part of the transfer surface:
delete_notesandmove_notesremain the only doors.