Skip to main content
Glama

svg-annotate-mcp

Annotate SVG paper figures in the browser — click an element (hovering live-highlights the semantic element under the cursor, Claude Design style) or lasso a region. The annotation (including the hit SVG element info) is sent back to Claude over MCP; Claude edits the source file (the SVG or the generating script), and the page watches for file changes and auto-refreshes — forming a closed "annotate → modify → refresh → re-annotate" loop.

  • Backend: Python + official mcp SDK (MCPServer, stdio), with an in-process stdlib HTTP thread (127.0.0.1 ephemeral port).

  • Frontend: single file src/svg_annotate_mcp/web/index.html. The SVG is fetched and injected via DOMParser into a Shadow DOM (bidirectionally isolated from the page's styles and IDs); annotations are drawn on a separate overlay, and hit testing happens browser-side at submission.

Installation and registration

cd ~/Projects/svg-annotate-mcp && uv sync
claude mcp add --scope user svg-annotate -- \
  uv run --directory /Users/boryant/Projects/svg-annotate-mcp svg-annotate-mcp

Optional: loosen the MCP client timeout so a single wait_for_annotations call can wait longer (it works without this too; the loop just runs more often): set MCP_TOOL_TIMEOUT=600000 in your shell profile or in the env block of the Claude Code settings.

Related MCP server: Browser Feedback MCP

Tool surface (4 tools)

Tool

Purpose

open_svg(svg_path, source_script="", title="")

Opens the SVG in the browser. Pass the figure's generating script path (e.g. matplotlib's fig_*.py) as source_script; every subsequent batch of annotations carries it back verbatim. If a page is already open, the tab is reused (a session event is pushed to swap the figure).

wait_for_annotations(timeout_s=120)

Blocks until the user clicks "Submit to Claude". A timeout returning status:"timeout" is not an error — just call it again immediately to keep waiting (loop protocol; also documented in the tool's description).

get_annotations()

Non-blocking fallback: retrieves the most recently submitted batch (to recover the scene after a broken timeout chain).

set_status(message)

Pushes a status to the page's top bar (e.g. "Editing fig_fig_xxx.py and rerunning…"). No need to call it after an edit; the page refreshes automatically.

Element-level click selection (v2)

Under the default "Select" tool, hovering immediately highlights the semantic element under the cursor (the g[id] groups from matplotlib such as text_N / line2d_N / legend_N, taking the minimum-area hit, with a small tag showing id + text). Clicking enters the element inspector (the side panel shows the id, verbatim text, and an ancestor breadcrumb — click the breadcrumb to reselect a parent group, e.g. text_54 → legend_1); an annotation is only produced when you type the "modification notes" (a bare click is just the inspector — no junk annotations). Esc or "Cancel selection" exits. Element annotations appear as dashed boxes + numbers on the figure.

Annotation payload structure (designed for Claude to locate)

Each annotation contains:

  • kind (element/rect/arrow/freehand/text), note (the user's modification instruction), number (the number on the canvas);

  • For kind:"element", a target field: the clicked element's tag/id/text (verbatim)/ancestors/d_prefix/bbox_svg — this is the strongest locator anchor; use it first (grep the id or text directly in the generating script or SVG);

  • geometry_norm (0-1) and geometry_svg (in viewBox coordinates, already converted server-side);

  • hits[]: the SVG elements the selection hit; each includes tag / id (semantic groups such as text_N/line2d_N in matplotlib) / text (element text, verbatim) / ancestors (ancestor id chain, e.g. ["figure_1","legend_1","text_54"]) / bbox_svg / coverage / d_prefix (first 30 chars of the path's d — a grep anchor when editing the SVG directly). Already denoised: background containers removed, semantic groups preferred (leaves inside groups not repeated), max 10 per annotation;

  • texts_in_region: all the text inside the selected region (reading order, deduplicated) — the first lead for grep-based locating in the generating script.

What gets changed is Claude's call: when an annotation carries source_script, prefer editing the script and rerunning (edits to the SVG artifact would be overwritten by the next rerun); for SVGs with no script, edit the SVG file directly. The page makes no assumption about the editing method — it only watches for file mtime changes (500 ms polling, double-tick stabilization, and a </svg> tail check to avoid acting on half-written files).

Annotation lifecycle

Unsubmitted drafts survive figure refreshes. After clicking "Submit to Claude", the batch stays on the figure at 35% transparency (numbers retained, for easy cross-reference); the next refresh triggered by Claude's file edit clears the translucent annotations.

Typical closed loop

用户: 帮我改 figure11,我来圈
Claude: open_svg("/path/figures/figure11.svg", source_script="/path/fig_tri_complement.py")
        wait_for_annotations()          # 挂起
用户: (浏览器里圈图例写「图例移到右上」,点提交)
Claude: 收到批注 → set_status("正在改 fig_tri_complement.py…")
        → 改脚本 → 重跑出图 → 页面自动刷新
        → wait_for_annotations()        # 等下一轮

Testing

uv run python tests/smoke_test.py     # 端到端:握手/HTTP/阻塞等待/坐标换算/SSE reload/复用 tab
uv run python tests/manual_driver.py <svg> [script]   # 起 server 供手动/浏览器自动化测试,批次落盘 tests/out/batches.jsonl

Page debug parameters: ?nosse=1 skips SSE (for headless screenshots); ?autotest=x,y,w,h auto-draws a rectangle annotation and submits it after load (0-1 normalized coordinates); ?autotest_click=x,y click-selects the element at that point and submits it as a real annotation (add &autotest_stage=pick to just do the click-highlight, for inspector screenshots).

Environment variables: SVG_ANNOTATE_NO_OPEN=1 prevents open_svg from auto-opening the browser (for testing).

Known limitations and (first version)

  • Single-session instance (one figure at a time; another open swaps the figure; multiple Claude sessions = independent server instances, no mutual interference);

  • No annotations.json export (annotations are only returned through memory and are lost on server restart — Claude's context is the fallback copy);

  • External resources in an SVG are not proxied (open_svg will warn; matplotlib output is all embedded, so this does not trigger);

  • Browser opens only via macOS open;

  • No auth on localhost.

Available Tools

4 tools
get_annotationsA

非阻塞地取最近一批已提交的批注(wait_for_annotations 的兜底)。

用于超时链中断后恢复现场;没有批注时返回 status="empty"。

Inputs: no named parameters.

open_svgA

在浏览器中打开一个 SVG 图,供用户圈选区域写批注。

打开后应调用 wait_for_annotations 等待用户提交批注。
svg_path 必须是绝对路径。source_script 可选:该 SVG 的生成脚本路径
(如 matplotlib 的 fig_*.py);之后每批批注都会原样带回该路径,
便于判断是改 SVG 文件还是改脚本重跑。title 可选,显示在页面顶栏。

Inputs: svg_path (required), title, source_script.

set_statusA

向批注页面顶栏推送一条状态文字,如「正在修改 fig_xxx.py 并重跑…」。

修改开始前调用一次,能让用户知道 Claude 正在干活;改完文件后
页面会因文件变化自动刷新,无需再调用本工具通知。

Inputs: message (required).

wait_for_annotationsA

阻塞等待用户在浏览器里提交批注,提交后立即返回批注内容。

重要协议:返回 status="timeout" 表示用户还没提交,这不是错误——
若仍在等待用户批注,应立即再次调用本工具继续等待,直到拿到
status="submitted" 或用户在对话里明确说不批注了。
返回的每条批注含:kind(rect/arrow/freehand/text)、note(用户文字)、
geometry_svg(viewBox 坐标)、hits(命中的 SVG 元素:id/文字/祖先链,
用于在 SVG 源文件或生成脚本中定位)、texts_in_region(选区内的文字)。

Inputs: timeout_s.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.1/5.0

    Scored across 4 tools

    Disambiguation5/5

    每个工具职责清晰唯一:打开、等待、获取、状态推送,互不重叠,边界明确。open_svg 负责启动,wait_for_annotations 阻塞等待,get_annotations 非阻塞兜底,set_status 用于反馈,无混淆可能。

    Naming Consistency5/5

    所有工具均采用 snake_case 的动词_名词结构(open_svg, wait_for_annotations, get_annotations, set_status),动词清晰且规律一致,无混用风格。

    Tool Count5/5

    4 个工具覆盖了 SVG 批注交互的完整核心流程,每个工具都有不可替代的作用,没有冗余或缺失,数量恰到好处。

    Completeness5/5

    工具集覆盖了打开、等待提交、非阻塞获取和状态通知,生命周期闭环。没有明显的缺失操作,且描述了超时续等和兜底恢复机制,足以支持用户与 agent 的完整交互。

    Maintenance

    ActivityStale
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables visual annotation on web pages for Claude Code, allowing element selection, comment addition, screenshot capture, and structured UI feedback for code fixes via an MCP server.
      MIT
    • A
      license
      A
      quality
      C
      maintenance
      Enables visual browser feedback collection directly into Claude Code. Users can point at elements in their browser and send annotated feedback that Claude can act on immediately.
      12
      1
      MIT
    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables drawing annotations directly on local web pages and sending screenshots, DOM data, and annotation context to your IDE agent for precise code edits.
      49 npm
      3
      MIT