A Claude Code plugin for iterating on Markdown documents by pointing at them instead of describing them.
/md-review:review some-doc.md renders the file in a preview, you click a
paragraph or select a phrase and leave a comment, press Send to Claude, and
the comments arrive in the chat with exact line numbers and the verbatim source
text of each anchor. Claude applies them, the page reloads itself, and the next
round starts.
git clone <this-repo> ~/projects/md-reviewThen in Claude Code:
/plugin marketplace add ~/projects/md-review
/plugin install md-review@md-review
Requires Node 18+ (uses only the standard library — no dependencies, no build step) and Claude Code 2.1.212 or later for the backgrounding behaviour described below.
Two processes in one: an MCP server on stdio and a loopback-only HTTP server.
/md-review:review doc.md
│
├─ open_review(path) ────────► renders doc.md, returns http://localhost:7377
├─ preview_start(url) ───────► page opens in the Claude Code Browser pane
└─ await_comments() ─────────► blocks…
you comment, press Send
◄── { comments: [{ line_start, source, … }] }
Claude edits the file
├─ (page reloads itself, comments cleared)
└─ await_comments() ─────────► next round
The load-bearing detail is await_comments. It simply does not return until you
press Send. Claude Code moves any main-conversation MCP call past two minutes
into a background task and delivers the result as a task notification, so the
session is never frozen while you read — however long you take, the comments land
the moment you submit them. .mcp.json sets timeout: 3600000, which is both
the wall-clock ceiling for one round and a floor on the MCP idle timeout.
Between rounds the server polls the file's mtime; when Claude edits it, the open page reloads over SSE and clears the previous round's comments.
Comments sit in a right-hand margin aligned with the passage they belong to, the way Google Docs does it; when a card would overlap the one above it, it is pushed down. Below ~900px of available width there is no room for a margin, so cards fall back inline.
| Control | Does |
|---|---|
| click a block | Comment on it |
⌥click a block |
Edit its Markdown source, saved straight to the file |
| select text | Offers "Comment on selection", quoting that exact phrase |
↵ / ⇧↵ in a comment |
Save / newline |
esc |
Cancel the open comment or edit |
+ / - / 0 |
Zoom in, out, reset — bare keys, no modifier |
⌘↵ |
Save an open source edit, or send — where the host passes it through |
Modes are chosen with a modifier rather than hover buttons on purpose: a control
in the left margin sits outside the document's box, so moving the mouse toward it
fired mouseleave and took the button away before it could be clicked.
Saving leans on plain keys and buttons rather than chords. The desktop app claims
⌘-combinations for its own interface, and some embedded-browser input paths
deliver a chord with an empty key, so ⌘↵ is treated as a bonus: ↵ saves a
comment, and a source edit has a Save button.
Zoom lives in the page rather than the browser because the Claude Code desktop
app claims ⌘+ / ⌘- for its own interface, so the keystroke never reaches an
embedded page. The level persists across the reloads between review rounds.
✎ opens the block's Markdown source in a textarea and writes it back to the
file on save — no round-trip through Claude, for the tweaks that are faster to
just make. It edits the source, not the rendered HTML: an HTML-to-Markdown
conversion would quietly reformat the rest of the file, while splicing a line
range preserves every byte you did not touch.
Writes are guarded, because Claude and unrelated editors touch the same file. The
page sends the line range and the original text; the server writes only if that
text is still there, relocating it by a unique text match when the file has
shifted, and refusing outright when the passage is gone or ambiguous. Your edits
are reported to Claude in the next round as manual_edits so it does not redo
them. There is no undo — the file is the record, so keep it in version control.
Each comment is anchored twice — by line number and by verbatim text — so edits stay reliable even if the file moved underneath:
{
"kind": "rewrite",
"line_start": 42, "line_end": 44, "lines": "42-44",
"source": "The onboarding flow is far too slow.",
"selected_text": "far too slow",
"comment": "quantify this"
}kind is one of comment, rewrite, cut, expand, question, approve. A
line_start of 0 means the comment lost its anchor because the passage changed
after it was written; selected_text still holds the original.
Installing copies the plugin into a version-keyed cache pinned to a git commit
(~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/). Editing this repo
does not change an installed plugin — a new session keeps loading the cached
copy, at the version that was current when you installed it.
To ship a change:
# 1. bump "version" in BOTH plugins/md-review/.claude-plugin/plugin.json
# and .claude-plugin/marketplace.json, then commit
claude plugin marketplace update md-review
claude plugin update md-review@md-reviewThen restart Claude Code — an MCP server is only loaded at session start. Verify
with diff -r plugins/md-review ~/.claude/plugins/cache/md-review/md-review/<version>.
Old version directories are left behind in the cache; they are inert and safe to delete.
plugins/md-review/
├── .claude-plugin/plugin.json
├── .mcp.json # stdio server, 1h per-call timeout
├── server/
│ ├── index.mjs # MCP + HTTP, the blocking wait
│ ├── markdown.mjs # line-anchored Markdown → HTML
│ └── page.html # the review UI
└── skills/review/SKILL.md # /md-review:review
The renderer exists to make review anchors trustworthy, not to be a
CommonMark implementation. It covers ATX and setext headings, paragraphs, fenced
code, blockquotes, nested ordered/unordered lists, task lists, tables with
alignment, thematic breaks, YAML frontmatter (collapsed), and inline bold,
italic, strikethrough, code, links, images and [[wikilinks]]. Raw HTML blocks
are shown escaped rather than executed. Anything unrecognised still renders as a
paragraph and stays commentable, so no part of a document becomes unreachable.
Commentable units are block-level elements plus individual list items and table rows — each carries its own source line range.
| Variable | Default | Purpose |
|---|---|---|
MD_REVIEW_PORT |
7377 |
First port tried; scans upward if busy |
MD_REVIEW_KEEPALIVE_MS |
25000 |
SSE keepalive and MCP progress-ping interval |
The HTTP server binds 127.0.0.1 only.