caretline

The MCP server

caretline-mcp is a Model Context Protocol server that gives an agent a caretline editor to work in. The agent can attach to a live editor a person is typing in and edit alongside them, or start a headless engine on a file. Either way it reads a revision number with the text, and every write names that revision.

$ caretline notes.md --listen                     # the person, in one terminal
$ claude mcp add caretline -- caretline-mcp       # the agent, once

Why

Agents usually edit files with search and replace on text they read earlier. When the file changes in between (the person typed, a formatter ran, another agent got there first), the edit fails or, worse, lands in the wrong place. A text editor that is open on the file can’t see the agent at all.

caretline is an editing engine whose whole state is one value, with a revision that goes up with every change and a trace that replays exactly. Over the state protocol, caretline-mcp turns that into tools an agent can use safely:

Install

cargo install --locked --path crates/caretline-app   # the editor: `caretline`
cargo install --locked --path crates/caretline-mcp   # the server: `caretline-mcp`

caretline-mcp --help lists the flags; caretline-mcp --list-tools prints the tool definitions as JSON.

Set it up

Claude Code

claude mcp add caretline -- caretline-mcp

Add flags after the command, for example a read-only server: claude mcp add caretline-ro -- caretline-mcp --read-only.

Claude Desktop

In claude_desktop_config.json (on macOS in ~/Library/Application Support/Claude/):

{
  "mcpServers": {
    "caretline": { "command": "caretline-mcp", "args": [] }
  }
}

Use the binary’s full path if Claude Desktop doesn’t see your shell’s PATH (which caretline-mcp).

Cursor

In ~/.cursor/mcp.json, or .cursor/mcp.json in a project:

{
  "mcpServers": {
    "caretline": { "command": "caretline-mcp", "args": [] }
  }
}

Flags

FlagDoes
--read-onlyRefuses edit and save. The agent can list, open, read, watch and trace. Its view is opened read-only
--quietNo name: edited line N in the live editor’s status bar
--name NAMEThe name edits are announced under (default: the MCP client’s name, such as claude-code)
--allow-unguardedAccepts edit without if_rev. Not recommended
--list-toolsPrints the tool definitions and exits

The server speaks MCP over stdio, protocol versions 2024-11-05 through 2026-07-28, built on the official Rust SDK (rmcp).

How a session goes

sequenceDiagram
    participant P as Person (caretline --listen)
    participant E as Editor
    participant M as caretline-mcp
    participant A as Agent
    A->>M: list_editors, open
    M->>E: hello, subscribe
    A->>M: read
    M-->>A: text, rev 12
    A->>M: view_open
    P->>E: types on line 1 (rev 14)
    A->>M: edit if_rev 12: replace "teh" on line 3
    M->>E: state.get: text changed since 12
    M-->>A: stale, rev 14, changed_by person, diff
    A->>M: read, then edit if_rev 14
    M->>E: msgs [edit] if_rev 14, show_status
    E-->>P: the fix and "claude: edited line 3"
    A->>M: watch since 16
    P->>E: keeps typing
    M-->>A: person typed "…"

Tools

Positions are {"line": L, "col": C}, both 1-based, the column in characters. col one past the line’s last character is the end of the line. Every tool takes an optional session (the id open returned), which may be left out when only one session is open.

list_editors

The live editors on this machine, newest first: pid, file, socket, started_ms, and alive (its socket answers). They come from the discovery files listening editors write to $TMPDIR/caretline/. Also lists this server’s open sessions.

open

ArgumentOpens
(none)The newest live editor
pidThe live editor with that process id
socketThe editor or caretline serve --socket on that Unix socket
fileA headless engine on that file, in this process (created on save if missing)
textA headless engine on that text, with no file
outline, layoutHeadless: an outline document (with the outline layout)
width, heightHeadless: the viewport (80x24)

Returns session, live, file, pid, socket, rev, lines and dirty. Attaching changes nothing in the editor.

read

The text with rev, lines, dirty (unsaved), and carets: the person’s caret and selection, and the agent’s once it has a view. from_line and to_line read a range. The text comes as a second content block, numbered ( 3│ text) unless numbered: false; the structured result’s text is always raw.

With render: true (or {width, height}), it returns the screen instead: the agent’s view (or the person’s, with view: "person") drawn as text, as the editor would draw it, and the cursor cell.

edit

{"if_rev": 12, "ops": [{"kind": "replace", "search": "teh cat", "text": "the cat"}]}
OpFieldsDoes
replacesearch, textReplaces the one place search occurs. 0 matches is not_found, 2 or more is ambiguous with where they are: add surrounding text
replace_rangefrom, to, textReplaces [from, to). text: "" deletes
insertat, textInserts at a position
selectfrom, toSelects [from, to) in the agent’s view (to left out: a caret)
keyskeysPlays a key script through the editor’s keymap, in the agent’s view

The result has the new rev (use it as the next if_rev), previous_rev, dirty, a line diff of what changed, and the agent’s caret.

view_open / view_close

Opens the agent’s own view (width, height, default 80x24), starting at the person’s caret. Its selection, scroll and folds are its own; the text, marks and undo history are shared. Text edits go through it too, so the agent’s caret follows its edits.

watch

Waits for changes after since_rev and returns them grouped by who made them:

{"rev": 31, "since_rev": 18, "timed_out": false, "text_changed": true,
 "changes": [{"who": "person", "from_rev": 20, "to_rev": 31, "actions": ["typed \"Ship it\"", "delete_backward"]}],
 "diff": {"summary": "line(s) 4-4 changed", "first_line": 4, "old_lines": ["Ship"], "new_lines": ["Ship i"]}}

who is person (the keyboard), another client (another program or agent on the socket), this agent (left out unless include_own) or editor (startup, resizes). Clock ticks and resizes are left out unless include_noise. It returns at the first change, after settle_ms (400) more so a typed word arrives whole, or after timeout_ms (20000).

trace

The session’s trace as JSON Lines: by default the current segment, which starts with a state line and replays on its own; since_rev gives only the lines after that rev; all everything kept. path writes it to a file:

$ caretline --replay session.jsonl --dump-state -     # the same text, carets and history
$ caretline --replay session.jsonl --snapshot 80x24   # the same frame

save

Writes the document to its file, the way Ctrl-S would in the editor: the live editor performs the write_file effect, or the headless engine writes it. The tool description tells the model to call it only when the user asked.

close

Closes the agent’s view and detaches. The live editor keeps running.

Worked example: a typo fixed while you type

The person writes in one terminal:

printf 'Teh plan\n\nShip it on fryday.\n' > plan.md
caretline plan.md --listen

and asks the agent, “fix the typos in the plan I have open”. The agent:

  1. list_editors, then open with no arguments: session s1, the editor on plan.md.
  2. read: rev 6, 1│ Teh plan, 3│ Ship it on fryday.
  3. view_open, so its caret is its own.
  4. edit {"if_rev": 6, "ops": [{"kind": "replace", "search": "Teh", "text": "The"}, {"kind": "replace", "search": "fryday", "text": "Friday"}]}: both in one change, rev 9. The person sees the line change and claude-code: edited line 1.

Meanwhile the person has started typing a new line at the end. Had they typed before step 4, the edit would have come back stale with changed_by: ["person"] and the new line in the diff; the agent reads again and sends the same edit with the new rev. Typing never lands in the middle of an agent’s edit, and an agent’s edit never lands on text it hasn’t seen.

The person saves with Ctrl-S when they’re happy. The agent saves only if asked to.

The example client crates/caretline-mcp/examples/agent_session.rs runs this against a live editor, with a stale edit and a watch:

cargo run -p caretline-mcp --example agent_session

Safety

Read-only mode. --read-only refuses edit and save and opens the agent’s view read-only, so the agent can follow along (read, watch, trace) and change nothing.

Saves. Only the save tool performs a write_file effect. Edits and key scripts return effects without performing them, so <c-s> or <c-q> in a key script neither writes nor quits. trace with path writes the trace file, nothing else.

Threat model. The server runs as you, on your machine, and adds no network surface: MCP runs over its stdin and stdout, and it reaches editors only through local Unix sockets.

Limits

This page on GitHub: docs/caretline/mcp.md