geml

geml mcp — document editing over MCP

Let Claude change one block of a document instead of rewriting the whole file; a bad edit is caught before it reaches disk, and a wrong one can be undone a single block at a time.

When --root also holds a code graph, this same server adds the three read-only call-graph tools — one client entry, one process, instead of two. This replaced the separate geml codemap mcp server, which has been removed; if you registered it, switch to geml mcp --root <dir>.

Install

npm install -g @geml/geml
claude mcp add geml -- geml mcp --root /abs/path/to/your/docs

Or in claude_desktop_config.json:

{
  "mcpServers": {
    "geml": {
      "command": "geml",
      "args": ["mcp", "--root", "/abs/path/to/your/docs"]
    }
  }
}

--root is required and is the only directory the server will read or write. A client cannot override or widen it: every path a tool receives is resolved through it, with symlinks followed before the check, so neither ../../etc/passwd nor a symlink planted inside the workspace escapes.

Serving the code graph too

Point --root at the repository and the server picks up <root>/.geml-code-graph automatically, adding resolve_name, open_symbol and get_backlinks to the same tool list:

geml codemap build --root /abs/path/to/repo      # once, to create the graph
claude mcp add geml -- geml mcp --root /abs/path/to/repo

Use --graph <dir> for a graph kept somewhere else inside the root. With no graph the three tools are not listed at all, so a client never sees a tool it cannot use.

The graph tools are read-only, but they now share a process that writes — so a client-supplied graph_dir is confined to --root like every other path, rather than being free to name any directory as it was on the old standalone server. $GEML_GRAPH_DIR is ignored here for the same reason: the operator who chose --root decides what the server can reach, not the environment it inherits.

The tools

Tool What it does
geml_list_ids Every addressable block: #id, kind, heading text
geml_read_block One block by id — not the whole file
geml_check Diagnostics with stable codes (Appendix A)
geml_history_log Recorded revisions, newest first
geml_write_block Replace one block (whole / head / body)
geml_add_block Insert blocks or prose (append / before / after)
geml_delete_block Remove blocks by id
geml_rename_id Rename an id and every reference to it
geml_revert_block Undo one block — its last change, or a named revision

With a code graph under --root, five more (read-only):

Tool What it does
search_symbols Find symbols by partial name — start here when you don’t know the exact one
resolve_name Exact name → the document and block id to open
open_symbol Open one symbol’s block: its callees, confidence notes, called-by pointer
get_backlinks Who calls this symbol, with file:line sites
trace_calls Several hops of the chain as a tree — callees downstream, callers for the impact path

search_symbols and trace_calls are the two that keep an agent off the one-call-per-hop treadmill: the first because resolve_name needs a name you may not have yet, the second because a three-level chain would otherwise be three round trips carrying three full symbol blocks. Every line trace_calls prints is a complete doc.geml#id, so it can be fed straight back to open_symbol without working out which document it belongs to.

Building and refreshing a graph stay on the CLI (geml codemap build / refresh): both run indexers or recorded shell steps, and refresh is behind a trust gate for that reason — not something a model should be able to trigger. geml codemap serve renders HTML for a person, which a model cannot consume.

What makes it different from editing the file directly

A write is validated before it reaches disk. Every mutation is first produced without touching the file; the result is parsed; the file is overwritten only if the result is clean. A broken edit is refused with the diagnostics that refused it:

{ "ok": false,
  "diagnostics": [
    { "severity": "error", "code": "unresolved-reference",
      "message": "unresolved reference `#ghost`", "line": 12 }
  ],
  "hint": "… The write was refused; the file on disk is unchanged." }

The hint is there for the model: without being told the file is unchanged, a model reads “error” and carries on as though its edit landed.

Every write is preceded by a history commit, so geml_revert_block always has a revision to undo to. Pass --no-history to turn that off; the default is on, because without it the strongest tool in the set has nothing to revert to.

geml_revert_block undoes one block. After a bad edit you recover that block while every other byte of the document — including good edits made since — stays exactly as it was. General file-editing tools can restore a whole file from a snapshot; none of them can put back a single block.

Called without rev, it undoes that block’s last change, however many other blocks were edited in between. This matters more than it sounds: history records a snapshot of the whole document per write, so a -N offset is a document cursor, and which N holds a given block’s previous content depends on how many unrelated writes followed — a number the caller has no way to know. Pass rev only when you want one specific revision:

geml_revert_block(file, id)                 # undo my last edit to this block
geml_revert_block(file, id, rev="-2")       # go to a specific document snapshot
geml_revert_block(file, id, rev="c9d5f1cc") # go to a specific revision id

Repeating the call does not walk further back — it alternates between the last two versions of the block. Revert once, look at the result, and use an explicit rev from geml_history_log if you need to go deeper.

Two behaviours worth knowing

Deleting a referenced block is allowed. References left dangling are reported in diagnostics, but they do not block the deletion — removing something on purpose is a legitimate act, and the caller decides whether to repair or restore.

A document that already has errors is locked until they are repaired. The pre-write check refuses any result containing an error, including one the document already had, so editing an unrelated block in a broken document is refused too. The refusal says so explicitly — that the errors predate the edit and which ones they are — so the fix is to repair those first. Repairing them is never blocked.

Try it

> list the blocks in spec.geml
> read #budget
> rewrite #budget's body to mention Q3
> that was wrong — revert #budget

Troubleshooting

Symptom Cause
--root <dir> is required The server refuses to start rather than serve the whole filesystem.
path escapes the server root The path resolved outside --root. Use a path relative to that root.
graph_dir escapes the server root Same rule for the code-graph tools: the directory must sit inside --root.
The code-graph tools are missing No graph was found. Run geml codemap build --root <dir>, or pass --graph <dir>.
A write keeps being refused with the same error The error predates your edit. Run geml_check and repair it first.
no .gemlhistory sidecar yet Nothing has been written through the server yet; the first write creates it.