Skip to content

Releases

v0.11.0

Choose a tag to compare

@zekaizer zekaizer released this 18 Oct 19:03

A vault can now be served read-only: agents read and search it, nothing they call can change it, and the server keeps it current as a pull mirror (ADR-0030). Multi-vault serving stays one instance per vault — a read-only vault is a second instance with its own env file, hostname and connector name.

Added

  • MD_READ_ONLY=1 (--read-only 1). The instance registers only the read and search tools, so every tool a client sees is readOnlyHint: true apart from an explicit pull. The server instructions say the vault is read-only.
  • Pull-only sync_vault. With git sync on, a read-only vault fetches and fast-forwards only — it never commits or pushes. It pulls once in the background at startup, every MD_GIT_SYNC_INTERVAL_SECS, and whenever an agent calls sync_vault. A branch that cannot fast-forward (diverged, or local edits in the way) fails the sync and changes nothing.
  • READ_ONLY error code. The core refuses every write on a read-only vault — a backstop the advertised surface never reaches.

Behaviour on a read-only instance

  • The transfer API has no PUT: it answers 405 Method Not Allowed. provision_transfer still grants reads, and refuses write: true.
  • A pending transaction journal refuses startup; start the instance once read-write to recover it.
  • MD_GIT_AUTO_COMMIT or MD_GIT_AUTO_PUSH_SECS together with MD_READ_ONLY is a startup error.

Notes

  • Serving one directory from both a read-write and a read-only instance is unsupported.
  • No change for read-write instances.

v0.10.0

Choose a tag to compare

@zekaizer zekaizer released this 06 Sep 18:10

A note created as … I/O ….md landed as folder … I and note O ….md, and nothing could refuse it: with a string path, a/b.md and a title holding / are the same bytes. Every path the tools carry is now an array of segments (ADR-0029), so a separator inside a name is an error at the write that caused it, not a surprise at a later read — and the contract no longer states a separator character at all.

Breaking

  • A path is ["dir", "note.md"], root to leaf; [] is the vault root. Requests, responses and error details alike, on every tool. A string path is refused at the schema. A segment that is empty, ., .., or holds /, \ or NUL is rejected as SEGMENT, naming the segment; a title such as I/O needs I∕O (U+2215) or IO.
  • move_notes names its target with exactly one of dest or into. dest is the full destination path including the new basename; into is a directory to move into keeping the basename ([] for the root). The trailing-/ convention that carried this distinction is gone; both or neither rejects the item as MISSING_CONTENT.
  • list_notes items carry kind (note | dir). Directory paths no longer end in /; kind is how a directory is told apart. A directory item's path is exactly what move_notes.into takes.
  • A transfer PUT never creates a directory. A URL cannot tell a separator from a / in a title, so PUT /api/notes/{path} under a missing parent answers 409 Conflict (RFC 4918 §9.7) with the way out in the body: make the directory through the note tools (create_notes with a path under it), then put again. The recipe says so on its PUT line.

Notes

  • Markdown links inside note bodies and the transfer API's URL paths stay strings — each has a path syntax of its own. On disk, in the event journal and in git commit subjects nothing changed.
  • The server instructions state the array form up front, and name the intro note as the array read_notes takes.
  • A mistyped directory segment still creates that directory silently: this release catches separators, not typos. A create_dirs guard stays a compatible follow-up if that failure shows up.

v0.9.0

Choose a tag to compare

@zekaizer zekaizer released this 29 Aug 15:53

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.

v0.8.2

Choose a tag to compare

@zekaizer zekaizer released this 13 Aug 06:59

A pause in the conversation no longer breaks the next tool call.

Fixes

  • An idle HTTP session survives an hour instead of five minutes. rmcp reaps a session after SessionConfig::keep_alive of inactivity, and its default is 300 seconds — a threshold a conversational client crosses constantly. The session worker quit on IdleTimeout, the manager dropped the entry, and the client's next tool call, still carrying the session ID it had been issued, got the spec-mandated 404 Not Found. That 404 is a transport-layer response with no JSON-RPC envelope, so the caller could not attribute it to an expired session: it surfaced as a failed tool and read as a bug in whichever tool happened to be called first after the gap.

Notes

  • The timeout remains as the safety net it is meant to be — a session whose HTTP connection dropped silently is still collected, just an hour later.
  • Sessions carry no vault state of their own (each is a cheap clone over the shared vault and the one commit lock), so a longer-lived session costs only its worker task.

v0.8.1

Choose a tag to compare

@zekaizer zekaizer released this 12 Aug 04:48

Editing a section no longer restyles the note around it.

Fixes

  • edit_sections replace keeps the blank lines that framed the replaced span. A section's content span begins right after the heading line and runs to the next heading, so the blank line under the heading and the separator ahead of the following heading were inside what replace overwrote. Content that did not carry its own leading newline silently produced ## A immediately followed by body text and body text immediately followed by the next heading — spacing every Markdown formatter, and markdownlint MD022, expects a note to keep. The replacement now restores the span's own leading and trailing blank-line runs.

Notes

  • Only ever restores what was there: a document written without blank lines around its headings stays that way, and nothing is added at a seam the note did not have.
  • Idempotent for round-trips. Content echoed back from read_sections already begins and ends with those separators, so they are not applied twice — a note does not grow a blank line per edit.
  • A span that is nothing but separator (a heading with an empty body) contributes one blank line to each side rather than duplicating the whole run, so filling in an empty section lands correctly spaced.
  • append, insert_before/insert_after, and move are unchanged — their placement and seam rules were already settled in ADR-0025. replace now follows the same principle against an existing span.
  • Spans, content_hash, overlap footprints, and the wire schema are untouched; only the bytes replace writes change.

v0.8.0

Choose a tag to compare

@zekaizer zekaizer released this 11 Aug 06:38

A one-character typo fix no longer costs a full read-and-rewrite of the surrounding section (ADR-0027).

Features

  • New tool replace_text. One item = one (note, find) literal substitution inside a note body; frontmatter is never searched (that stays edit_properties' domain). Matching is byte-exact — no regex, no case folding, no CJK spacing/markup folding — so the replaced span is never in question. By default find must occur exactly once: 0 matches reject with NOT_FOUND, 2+ with AMBIGUOUS; replace_all: true or expected_count: <n> takes every match. Optional heading_path (plus scope and expected_hash) narrows the search to one section, and dry_run: true reports the matches without writing.
  • Batches stay order-independent. Every item resolves its match spans against the unedited note and intersecting spans reject the batch (OVERLAP), so no item ever sees another item's output. All-or-nothing, like the other destructive tools.
  • Responses report positions, not prose. Per item: the replacement count plus the first few changed lines, numbered in the resulting note — never the note body, which is precisely the payload the tool exists to avoid.
  • New error code COUNT_MISMATCH for a violated expected_count.

Fixes

  • make check is green again. The stdio end-to-end suite still read read_outlines as a heading array, which v0.7.0 replaced with a text block, and four files no longer matched the rustfmt of the pinned toolchain — both gate stages had been failing.
  • The tool-annotation test now asserts that every registered tool is classified, which its comment claimed but the check did not do.

Notes

  • Measured on a real vault (259 notes): median note 12.5 KB, median leaf section 425 B (p90 1.4 KB). The old typo path — read_outlines for the address, read_sections for the bytes, edit_sections to send them back — moved roughly a kilobyte in each direction for a few bytes of actual change.
  • Structural change (adding, deleting, moving, renaming sections) stays with edit_sections: replace_text edits body text and does not validate heading levels.
  • search_notes still NFC-normalizes and folds CJK gaps, so a hit it finds may not match find byte-for-byte (전역지침 finds 전역 지침; replacing it does not). This asymmetry is deliberate — search may guess, a destructive edit may not — and the tool description says so.

v0.7.0

Choose a tag to compare

@zekaizer zekaizer released this 17 Jul 08:04

Outlines are re-encoded as text rows addressed by minimal unique suffixes (ADR-0026), cutting the vault-wide outline payload to ~22% of the previous encoding.

Breaking changes

  • read_outlines returns a text block, not a heading array. NoteOutline.headings is replaced by NoteOutline.outline: one <line> <#×level> <title> row per heading in document order, following the read_notes content-string envelope pattern. The grammar is unambiguous by construction — titles cannot contain newlines, heading rows start with a digit, marker rows with ↳. The response budget now measures rendered rows.
  • Heading addresses are minimal unique suffixes. Each heading carries the shortest ancestor-chain suffix that resolve_heading maps back to it, emitted as a ↳ heading_path: [...] row only under headings whose bare title does not resolve. occurrence becomes optional and is set only when even the full chain is duplicated; the ambiguous field is dropped — a multi-element address is the signal, and it answers the question agents actually have: does the bare title suffice?

Fixes

  • search_notes matches CJK terms across spacing and inline style. Korean spaces words optionally, so 전역지침 and 전역 지침 are one term, but the query was split on whitespace and matched as literal substrings — the two directions disagreed, and **전역** 지침 never matched 전역지침. A second scan now runs over a shadow copy with CJK-adjacent gaps folded, unioned with the raw scan; snippets and match_count still report original bytes.
  • The Windows build compiles again. The OAuth token store reached for unix-only file-permission APIs, so x86_64-pc-windows-msvc failed to compile and no Windows binary was attached; closes #10.
  • Ambiguity errors point at the outline's ↳ address rows instead of a heading flag the outline no longer emits.

Notes

  • Measured on a real vault (156 notes / 3107 headings): 99.2% of headings resolve by bare title, and every emitted address round-trips through resolve_heading.
  • The read_outlines description now names the ~10 KB threshold (see size_bytes in list_notes) above which outline-first reads pay off, and states the small-note fast path explicitly.
  • The OAuth store stays owner-only (0600) on unix; Windows has no mode bits, so it inherits the state directory's ACL — keep MD_STATE_DIR under a per-user location.
  • The Windows release asset remains best-effort and does not block the Linux binary.

v0.6.2

Choose a tag to compare

@zekaizer zekaizer released this 14 Jul 06:04

Patch release: insertion placement now follows authoring intent (ADR-0025).

Fixes

  • append continues the section's text. It used to insert at the raw span end — after the blank lines separating the section from the next heading, gluing the new text to that heading (lead\n\nmore\n## B). It now backs up over the trailing whitespace-only lines to sit directly after the last non-blank line, leaving the separator in place (lead\nmore\n\n## B). A span ending at EOF keeps the literal end (matching append_notes); no separator is ever added or removed.
  • insert_before/insert_after keep blank-line seams. Inserted sibling blocks were glued to both neighbours; they now get the same both-seams blank-line rule move already applied (only ever adds, never removes), via one shared implementation.

Notes

  • Spans, content_hash, overlap footprints, and the wire schema are unchanged — only insertion offsets and seam padding moved. Byte-level output changes for callers that pinned the old placement.
  • The edit_sections description now spells out placement, including the scope:"section" trap: plain text appended at a subtree's end structurally joins the last subsection — use scope:"body" for the section's own prose.

v0.6.1

Choose a tag to compare

@zekaizer zekaizer released this 14 Jul 03:48

Patch release: MCP structured-output schema conformance and gitfile-aware exclude hardening.

Fixes

  • Output schemas no longer require condensed-away fields. Fields dropped from the wire when empty/false (omitted, errors, conflicts, pruned, dry_run, …) were still listed as required in every tool's outputSchema, so clients that validate structuredContent against it (e.g. recent Claude Code) rejected virtually every successful call. All 17 such fields across 7 response types are now marked optional; per-module regression tests pin the invariant.

Features

  • .git gitfile support for exclude hardening. A vault whose .git is a file (git worktree, submodule, --separate-git-dir) now gets .md-mcp/ and .*/ written into the real repository's info/exclude — following commondir for linked worktrees — instead of being skipped, so the auto-commit sweep no longer pushes server state or hidden dot-dirs from such vaults. Git sync itself already worked in these layouts.

v0.6.0

Choose a tag to compare

@zekaizer zekaizer released this 10 Jul 14:38

Unified move primitive: move_notes replaces rename_notes and relocate_notes (ADR-0024).

Breaking changes

  • rename_notes and relocate_notes are removed. Migrate to move_notes; each item is { source, dest }:
    • dest ending in / — move into that directory, keeping the source basename (/ alone is the vault root). Covers the old relocate_notes.
    • dest without a trailing / — the full destination path including the new basename (a note keeps .md). Covers the old rename_notes, and move + rename in one atomic step (previously two calls).
  • The tool surface drops from 12 to 11 (plus opt-in sync_vault).

Features

  • Nested destinations (final-tree semantics) — an item may land inside another item's moved directory in the same batch (e.g. move a directory and file more notes under its new location, atomically). Ancestor destinations apply first; closes #7.
  • Move + rename in one call — the core restructure action is now one atomic, all-or-nothing batch item; closes #1.

Notes

  • All existing validation carries forward: vault jail, collision/overlap/swap rejection, all-or-nothing batches, dry_run, overwrite, prune_empty, update_links (vault-wide standard-Markdown link rewriting), and the Unicode same-file respelling exception.
  • A destination inside a subtree the batch vacates is still rejected (BATCH_COLLISION); cyclic swaps remain rejected.
  • The README is rewritten for end users: tool overview, safety properties, and MCP client quick start.