Skip to main content
Glama
inkscape
by declanbx

inkscape-live-mcp

An MCP server that lets an AI agent (built for Claude Code) work in the Inkscape window you have open, at the same time as you. Its edits appear in your window as they happen and sit on Inkscape's normal undo stack, so Cmd+Z takes them back. Your edits are visible to the agent the next time it looks, because it reads the live document, not a saved file.

macOS, Inkscape 1.4.

Why

  • Shared, live editing. No export–edit–reimport loop. You drag a panel; the agent sees it moved. The agent aligns six headers; you watch them snap into place and can undo it.

  • Exact layout by numbers. Every coordinate is millimetres from the page's top-left corner, measured on the visual bounding box (stroke and glyph edges included) that Inkscape itself reports. "Align these to the left edge of that panel", "stack them 8 mm apart", "make this exactly 70 mm wide" are one call each, computed, not eyeballed; there is no render–nudge–render loop.

  • Safe to share a document. Every read returns a revision; an edit given that revision refuses to run if you have changed the drawing since. Every edit reports whether it actually landed.

Related MCP server: inkscape-mcp

Requirements

  • macOS (tested on macOS 27, Apple Silicon) and Inkscape 1.4 (tested on 1.4.4) in /Applications.

  • Homebrew for dbus (the installer runs brew install dbus if needed).

  • uv (brew install uv).

  • An MCP client. The installer registers the server with Claude Code; any client that runs stdio servers works.

Install

git clone https://github.com/declanbx/inkscape-live-mcp.git
cd inkscape-live-mcp
./install.sh

install.sh is idempotent (re-run it after pulling). It builds .venv with uv, links extension/ into Inkscape's user extensions folder, creates ~/Applications/Inkscape (Claude).app, and runs claude mcp add --scope user inkscape …. Flags: --no-register (skip the Claude Code registration), --no-app (skip the launcher). For another client, register <checkout>/.venv/bin/python <checkout>/run_server.py as a stdio server.

Optional, for Claude Code: cp -r skills/inkscape-live ~/.claude/skills/ adds a skill that teaches the agent the workflow (route by cost, lay out by numbers, share the window safely).

Quick start

  1. Open Inkscape (Claude).app, or drop an .svg on it. It is the same Inkscape with your usual preferences, started so that the agent can reach it. (A window opened the normal way is not reachable; see How it works.)

  2. In Claude Code, ask in plain words: "align the six section headers to the left edge of the Summary panel", "space column 2's boxes 8 mm apart", "what did I change?", "make these the same width as the map" (with objects selected), "replace the title".

  3. Save with Cmd+S as usual. The agent never saves over your file; it can write a copy (save_copy).

Tools (server name inkscape)

Reading

Layout: Inkscape's own commands, ~0.1–1 s, one undo step each

Content: one undo step per batch

Other

status outline find inspect selection render (with each object's box and id drawn on) window (screenshot of the Inkscape window) changes (what you moved, added, removed)

align distribute (equal or exact gaps) move (by or to mm) resize (exact visual width/height) style attributes structure (group, z-order, delete, duplicate, reparent) history (undo/redo)

text (set or find/replace, many at once) insert (.svg as live vector, .png/.jpg) add_svg (draw anything from SVG markup, in mm) python (arbitrary inkex code)

export (PDF, SVG, EPS, PS or PNG of the page, the drawing, chosen objects or a region; read back and checked) actions (any of Inkscape's ~1,070 actions, searchable; rejections reported) save_copy launch

Safety nets on every call

  • Revision guard. Every read and edit ends with revision <id>, a fingerprint of every object's id and visual box in paint order, read live from Inkscape. Every editing tool takes an optional expected_revision; if the document no longer matches it, the tool refuses, changes nothing, and lists what changed since (moved/added/removed objects). changes reports the current revision and makes it the new baseline; changes(since_revision=…) diffs against any revision read this session. history takes it too, so an undo never takes back something you did after the agent's edit.

  • Landed verification. After each edit the touched objects' live boxes are compared with what the edit planned: exact positions for moves and gaps, a shared edge for alignment, equal spacing for distribution, new ids present and deleted ids gone for inserts and deletes. The result says Landed: confirmed, NOT LANDED as planned: … (with what is where), or Landed: unconfirmed for an edit with no geometric trace, such as a colour.

  • No-op batches add no undo step. The bridge compares the document before and after a batch; if it is byte-identical it hands nothing back, so Inkscape adds no undo entry and your next Cmd+Z still undoes the last real change.

  • Exports are read back. export reopens the file it wrote and checks it against what was asked: the PDF page size in mm (read with macOS Quartz), the PNG's pixel size, the SVG's document size, and whether fonts are embedded or text was turned into outlines (text_to_path=True).

  • Raw actions say what Inkscape did. If Inkscape logs a rejection (transform-translate abc → "requires two comma separated numbers"), actions returns an error with that reason; otherwise it reports whether the geometry changed, because Inkscape ignores some malformed arguments without a word.

  • Uncertain completion is reported, never retried. If Inkscape does not answer in time (a dialog is open, a huge document), the call returns UNCERTAIN COMPLETION instead of an error or a silent retry: the edit may still apply. The next call waits for Inkscape to finish it before reading or editing.

Cost model

Measured with tests/bench.py on a synthetic A0 document of 50,082 objects (15 MB, one 8 MB embedded image), Apple Silicon laptop, through the MCP server:

Operation

Time

status

0.3 s

align / move / style (native)

0.6–0.7 s

distribute five panels to an exact gap

1.1 s

changes (geometry diff)

0.3 s

find / outline (tree cached)

0.25 s

first outline of an unedited file (read from disk)

1.6 s

first outline of an edited document (via the bridge)

1.7 s

render the whole page / one panel

0.8 s / 0.4 s

bridge edit (text, add_svg, insert, python)

3.0–3.2 s

bridge batch that changes nothing

2.2 s

undo of a bridge edit

1.2 s

export the page as PDF / one panel as PDF (via a headless copy)

3.4 s / 3.0 s

export the page as PNG at 150 dpi

1.1 s

On a 2,000-object figure everything is under 0.6 s. Bridge edits grow with document size, because Inkscape serialises the whole document to the extension and reloads its result: on a real 100,000-element poster with heavily clipped figures one bridge edit took about 16 s. So batch content edits: text takes a list, add_svg and python take many elements at once.

How it works

  • Native route. launch.sh starts a private D-Bus session bus (~/.inkscape-mcp/bus.sock) and opens Inkscape as the unique application org.inkscape.Inkscape.claude on it (--app-id-tag=claude). Inkscape then publishes its actions over org.gtk.Actions; inkscape_live_mcp/bus.py calls them and reads query output (query-all, select-list) from Inkscape's stdout, which launch.sh logs to a file.

  • Bridge route. For what no action can do (text content, inserting markup or files, reparenting, arbitrary edits), extension/claude_bridge.py is an effect extension the server triggers over the same bus. It reads a request file, applies the whole batch atomically (if any operation fails, nothing changes), and hands the document back as one undo step.

  • Reading. Boxes come from query-all on every call. The XML tree is cached and re-read only when the set of objects changes; when the window shows no unsaved changes it is read straight from the file.

  • Choosing an instance. INKSCAPE_MCP_TAG (default claude) names the instance and INKSCAPE_MCP_STATE (default ~/.inkscape-mcp) its state folder, so several instances can run side by side; the test suite uses this to run on its own instance.

Quirks, and how they are handled

  • Launch constraints silently disable Python extensions when Inkscape is exec'd from a shell, so no edit through the bridge would work. launch.sh therefore starts Inkscape through LaunchServices (open -n -a … --env … --args --app-id-tag=…).

  • GTK on macOS exports no /window/N objects on D-Bus, only the application and /document/N (confirmed on 1.4.4 by the test suite). Every operation uses application-level actions plus /document/N for undo, redo and extensions; window-level actions such as save and zoom are left to you.

  • Open-file limit. Inkscape 1.4.2 on macOS can crash the first time it runs an extension if it inherits a ~1M soft RLIMIT_NOFILE, common under IDEs and agent hosts (reported in aravindev/inkscape_mcp#33). Launched through LaunchServices, Inkscape does not inherit the caller's limit: from a shell with 1,048,576 the extension sees 2,560 (Inkscape 1.4.4). The test suite asserts it stays at or below 4,096.

  • Wrong boxes for groups that contain a nested <svg>. Inkscape 1.4 adds the nested viewBox size, in parent units, to the group's box (an inserted 70 mm legend is reported 105.8 mm wide). The server rebuilds those groups' boxes from their children and aligns them with computed moves rather than Inkscape's own Align.

  • Export settings persist inside a running Inkscape, and some cannot be switched back (measured on 1.4.4): once text-to-path is on, setting it off has no effect, and the page/drawing area mode can be changed but never unset. PNG exports therefore pass an explicit area and pixel size every time; PDF, SVG, EPS and PS exports write the live document to a temporary copy, crop the copy to the page, the drawing, the chosen objects or a region, and export it with a fresh headless Inkscape. Your window's export state is never touched, and a region works for PDF too (Inkscape's own vector export ignores one).

  • Inkscape's own "drawing" area is wrong for documents holding a nested <svg> (the bug below), so export(area="drawing") uses the corrected boxes.

  • Some action arguments are ignored silently. A bare or misspelled object-align does nothing and logs nothing; transform-translate abc logs a reason. See "Raw actions say what Inkscape did".

  • select-by-id adds to the selection: every command clears it first, then restores your selection.

  • export-area cannot be cleared once set, so every render passes an explicit area.

  • A saved copy keeps its source's internal document name, so the front window is matched by file name first.

  • Two sessions driving one Inkscape would interleave on the bridge files and the stdout log, so every client call holds a cross-process lock (~/.inkscape-mcp/client.lock).

Limitations

  • macOS only. On Linux the same D-Bus mechanism works natively; see Related work.

  • The revision sees geometry, not paint. A colour-only or attribute-only edit you make does not change the revision, and Landed cannot confirm such an edit. changes(deep=True) re-reads the document and reports text and style differences.

  • The guard is a check, not a lock: an edit you make in the milliseconds between the check and the agent's edit is not caught.

  • Bridge edits reload the whole document: seconds on large documents (see Cost model).

  • Tools act on the front window. With several documents open, the front one is identified from the window title; click the window you want if it is ambiguous.

  • Inkscape 1.4 only. Other versions are untested.

Security

  • No network access, no API keys, no telemetry. Everything runs on your machine.

  • python (and actions) run arbitrary code inside Inkscape's Python as you, by design, with the same trust as the agent's own shell access. Run the server only for agents you would give a shell to.

  • The bus is a private dbus-daemon whose socket lives in ~/.inkscape-mcp (mode 0700, as are its bridge files and logs).

Tests

.venv/bin/python tests/test_units.py   # no Inkscape needed
.venv/bin/python tests/e2e.py          # 62 checks over MCP on a private Inkscape instance
.venv/bin/python tests/bench.py        # the cost table above

e2e.py and bench.py launch their own Inkscape with a fresh tag, a private bus, a temporary state folder and a throwaway Inkscape profile, check before any edit that the server is driving that instance, and quit it afterwards (--keep leaves it open). They never touch a window you have open.

  • aravindev/inkscape_mcp is the closest prior art: the same mechanism, a running Inkscape driven over D-Bus plus inkex extensions, together with a headless CLI mode and a much broader tool surface, on Linux and Windows (macOS support is an open pull request). This project adds layout by Inkscape-measured visual boxes in millimetres (with the nested-<svg> box correction), exact align/distribute/resize tools, renders annotated with object ids, a change diff and revision guard for editing alongside a person, edit-landed verification, and macOS support today. The check for actions Inkscape rejected follows its CLI wrapper's stderr scan.

  • Shriinivas/inkmcp: live control of a running Inkscape over D-Bus with an extension and inkex code execution (Linux).

  • tspspi/mcpinkscape: an offline SVG backend plus an optional native C++ Inkscape extension for live editing with snapshots, polling and revision-conflict protection; the revision guard here follows its expected_revision idea.

  • P1oN/inkscape-mcp-server: a native Rust server with guarded, verified edits; the landed check, the "uncertain completion" result and the no-op-adds-no-undo rule here follow its design.

Files

launch.sh (bus + Inkscape) · install.sh · run_server.py (entry point) · inkscape_live_mcp/ (bus.py D-Bus and stdout log, client.py operations, revision guard and landed checks, svgtree.py tree and box model, server.py MCP tools) · extension/ (claude_bridge.inx/.py) · tests/ (_instance.py private test instance, e2e.py, bench.py, test_units.py, fixtures/) · skills/inkscape-live/SKILL.md (a Claude Code skill for using the tools well). State and logs: ~/.inkscape-mcp/ (inkscape.out.log, inkscape.err.log, bridge/bridge.log, renders).

Troubleshooting

  • Tools say "not running": open Inkscape (Claude).app (a normal Inkscape window is not reachable).

  • UNCERTAIN COMPLETION or a stuck call: call window to see Inkscape; a dialog may be open.

  • After updating, re-run install.sh and restart Inkscape (Claude), which reads extensions at startup.

License

MIT, see LICENSE.

Available Tools

24 tools
actionsA

Run any of Inkscape's ~1,070 actions, or search them. run: ["select-all", ["object-align", "left page"], ["transform-rotate", 90]] — returns what Inkscape printed, any complaint it logged (an action it rejected makes this an error), and whether the geometry changed (Inkscape ignores some malformed arguments silently). search: substring over action names and descriptions. Most act on the current selection; use selection(set_ids=…) first. To write files, prefer export.

Inputs: run, search, expected_revision. Returns: result

add_svgA

Draw anything by writing SVG markup: shapes, lines, arrows, text, groups, gradients. With x_mm/y_mm the markup's coordinates are units ('mm' or 'px') from that page point, so '' at x_mm=20 is a 40×10 mm bar at 20 mm. Without them the markup is written as-is in the parent's own user units. Colliding ids are renamed.

Inputs: markup (required), show, x_mm, y_mm, +4 more.

alignA

Align objects exactly. edges: 'left' | 'hcenter' | 'right' | 'top' | 'vcenter' | 'bottom', or one of each ('left top'). to: an object id to align to (it stays put), or first | last | biggest | smallest | page | drawing | selection. to_mm: align that edge to a page coordinate instead. as_group moves the ids as one block. expected_revision: refuse if the document changed since.

Inputs: ids (required), edges (required), to, show, +3 more.

attributesB

Set (or with null, remove) XML attributes: 'inkscape:label', 'width', 'rx', 'xlink:href', 'id'…

Inputs: ids (required), attrs (required), expected_revision. Returns: result

changesA

What the user changed since you last looked or edited (or since since_revision): objects added, removed, moved or resized (exact, cheap). Reports the current revision and makes it the new baseline. deep=True also re-reads the document to report text and style edits (slow on posters).

Inputs: deep, since_revision. Returns: result

distributeB

Space objects along x or y. Without gap_mm: Inkscape's equal distribution between the outermost objects (mode gap | centers | left | right | top | bottom). With gap_mm: stack them in their current order with exactly that gap, starting at the first object (or at start_mm).

Inputs: ids (required), axis, mode, show, +3 more.

exportA

Export the live document (unsaved changes included) to PDF, SVG, EPS, PS or PNG; the format comes from the file suffix (or format=). What to export: area='page' (default) or 'drawing' (everything drawn); or ids=[…] for those objects (only=False keeps whatever overlaps them); or region_mm=[x0, y0, x1, y1] (PNG only). dpi: PNG resolution, and the resolution filter effects are rasterised at in PDF/EPS/PS. text_to_path=True turns text into outlines (for printers that cannot take fonts). background: PNG background colour (default transparent). The file is read back afterwards: page size in mm, pixel size, fonts embedded.

Inputs: path (required), dpi, ids, area, +5 more. Returns: result

findA

Find objects by any combination of: text content (case-insensitive substring), inkscape label, id regex, tag (text, g, rect, image, path…), a style substring (e.g. 'fill:#4b80ad'), or a page region [x0, y0, x1, y1] in mm (within = fully inside, overlaps = touches). top_level_only drops matches nested inside another match.

Inputs: tag, text, label, style_has, +5 more. Returns: result

historyA

Undo or redo n steps in the live document (the same stack as the user's Cmd+Z). Pass expected_revision so an undo never takes back something the user did after your edit.

Inputs: n, op, expected_revision. Returns: result

insertC

Place a figure file into the document with its top-left at (x_mm, y_mm), scaled to width_mm or height_mm (aspect kept). .svg is inlined as editable vector (ids namespaced so nothing collides); .png/.jpg are embedded (embed=False links the file instead).

Inputs: path (required), id, show, x_mm, +7 more.

inspectB

Everything about specific objects: tag, label, where it sits in the tree, bounding box, every attribute (embedded images summarised), parsed style, full text, and its children. xml=True adds the raw XML (truncated to max_chars).

Inputs: ids (required), xml, max_chars. Returns: result

launchB

Start Inkscape (Claude) if needed and open path in it (an .svg; Inkscape also opens .pdf/.ai/.eps).

Inputs: path. Returns: result

moveB

Move by (dx_mm, dy_mm) and/or place so the anchor point ('top-left', 'center', 'bottom-right', 'top-center'…) of the ids' joint box lands at (x_mm, y_mm). each=True places every object separately.

Inputs: ids (required), each, show, x_mm, +5 more.

outlineA

The object tree with ids, labels, text snippets and live bounding boxes (mm).

root: start below this id (default: the whole page). depth: levels to expand; deeper content is summarised as counts. refresh: force re-reading the document (normally automatic).

Inputs: root, depth, refresh, max_lines. Returns: result

pythonA

Run Python (inkex) against the live document in one undoable step — anything the other tools cannot express. In scope: svg (root element), by_id(id), selected (ids), inkex, etree, Transform, frames (frames.content(el) maps an element's coordinates to page px), to_px('3mm'), qname('inkscape:label'), unique_id(base), parse_style/style_str. print() output and result = … come back. readonly=True skips the document reload (fast inspection). timeout_s: give up waiting after this long (the result is then UNCERTAIN COMPLETION: the code may still finish and apply).

Inputs: code (required), readonly, timeout_s, expected_revision. Returns: result

renderA

Render the LIVE document to an image you can see.

region_mm [x0, y0, x1, y1] or around=[ids] (+margin) choose the area; default is the whole page. only=True hides everything except the around objects. annotate draws each object's box and id on the image: pass a list of ids, 'children:' for that object's children, or 'top' for the main parts in view (the shallowest level showing several objects, wrappers skipped) — the fastest way to learn which id is which.

Inputs: only, around, annotate, width_px, +3 more.

resizeB

Resize to an exact visual width or height (or by a scale factor), keeping the anchor corner fixed. keep_aspect=False with both width_mm and height_mm stretches (goes through the slower extension).

Inputs: ids (required), each, show, scale, +5 more.

save_copyA

Write the live document (unsaved changes included) to path as Inkscape SVG. Does not change which file the window is editing; the user saves the real file with Cmd+S.

Inputs: path (required). Returns: result

selectionA

Read what the user currently has selected (ids + boxes), or replace the selection with set_ids (useful to show the user which objects you mean).

Inputs: set_ids. Returns: result

statusA

Is Inkscape (Claude) running; which documents are open; what is frontmost; what the user has selected.

Inputs: no named parameters. Returns: result

structureB

Rearrange objects. op: delete | duplicate | group | ungroup | pop_out (out of its group) | raise | lower | top | bottom (z-order) | clone | unclone | reparent (move into parent at index, keeping the visual position). group takes optional new_id and label.

Inputs: op (required), ids (required), index, label, +3 more. Returns: result

styleB

Set CSS style properties on objects, e.g. {"fill": "#4B80AD", "font-size": "11pt", "stroke-width": "0.5", "opacity": "0.8"}. A null value removes the property. Applies to groups too (children inherit unless they override — inspect them if a colour does not take).

Inputs: ids (required), props (required), show, expected_revision.

textC

Change text in ONE undoable batch. edits: [{"id": text id, "text": "new text; \n for new lines"}] keeps the first run's style. replace: [{"old": "…", "new": "…", "ids": optional scope}] swaps a substring inside existing runs, preserving mixed formatting (bold words, superscripts).

Inputs: show, edits, replace, expected_revision.

windowA

Screenshot of the Inkscape window itself (the user's view: zoom, selection handles, any open dialog). Use when something seems stuck or to see what the user is looking at.

Inputs: no named parameters.

Tool Schema Changelog

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

Loading tool schema changelog…

    TDQS

    A3.5/5.0

    Scored across 24 tools

    Disambiguation4/5

    Most tools have clearly distinct purposes (e.g., outline vs inspect, find vs selection), but a few pairs overlap: export vs save_copy (both write files), move/align/distribute (positioning), actions vs python (general escape hatches), and style vs attributes (both set properties). Detailed descriptions help disambiguate, making it mostly distinct with a few confusable pairs.

    Naming Consistency4/5

    All tool names are lowercase, using underscores only for multi-word names (save_copy, add_svg). The style is consistent, but the set mixes verbs (move, export) and nouns (selection, attributes, text), lacking a uniform verb_noun pattern.

    Tool Count4/5

    24 tools is slightly above the typical 3-15 range, but the domain (full Inkscape control) is complex and each tool addresses a distinct area. Specialized tools plus general escape hatches (actions, python) are justified, though the set feels heavy.

    Completeness4/5

    The surface covers state reading, object manipulation, text/style/attribute editing, export, and general escape hatches. Minor gaps exist (no direct overwrite save, document close/switch), but actions and python provide workarounds.

    Maintenance

    ActivitySlowing
    ResponsivenessOutdated data

    Related MCP Connectors

    Related MCP Servers