Skip to content

v0.9.0

Choose a tag to compare

@zekaizer zekaizer released this 29 Aug 15:53
· 14 commits to main since this release

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 by prefix to one directory or to a single note; confinement is matched per path component, so a sibling like dir-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 answers if-none-match with 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 requires if-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_notes gains base. Pass the path of an existing note to copy it verbatim — frontmatter and body — instead of sending content. Pure copy: base excludes content/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 .md only, 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_notes output 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_notes on 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_notes and move_notes remain the only doors.