Skip to main content
Glama
inksmcp

inksmcp

by bhanutpt

inksmcp

An MCP server that lets AI assistants create, edit and export vector graphics with Inkscape.

CI PyPI Python License: MIT

Describe a poster, a diagram, a chart or a comic page to your assistant, and it builds a real SVG document in Inkscape: measured, laid out and exported to PNG or PDF. It can also open drawings made elsewhere and change them.

Comic page

Periodic table poster

Swimlane flowchart

Floor plan

Datasheet with four plots

Tamil alphabet poster

Made by Claude through inksmcp during field tests, in 13 to 41 tool calls each. The SVG was never edited by hand. The floor plan and the periodic table used a short script to prepare their data.

Why inksmcp

Asking a model to write SVG by hand works for an icon, but it breaks down on real layouts. The model can't measure text, keeps redoing coordinate arithmetic, and can't see what it drew. inksmcp takes that work off the model:

  • Measured, not guessed. Bounding boxes come from Inkscape itself, text included, in the document's own units.

  • Relationships instead of coordinates. align and layout arrange things. place keeps a caption below a title's real glyphs. fit_to sizes a box around its content. connect draws arrows that stay attached when things move.

  • Data-driven. repeat stamps a template per data row (cards, timelines, legends, table rows, character poses). split divides a page into panels or cells. Large data comes from JSON or CSV files, so it doesn't pass through the conversation.

  • Works on existing files. It opens .svg/.svgz drawings from other tools. inspect find locates elements by colour, type, text or symbol. Symbols can be placed from icon libraries.

  • Safe and checkable. Every tool is all-or-nothing. Editing responses warn about overlapping text, and render_preview shows the agent what it drew.

  • Real output. PNG, PDF (optionally with text as paths), plain SVG, EPS, EMF and more, rendered by Inkscape.

Related MCP server: imagetosvg-mcp

Quick start

  1. Install Inkscape 1.x and uv.

  2. Add the server to your MCP client. For Claude Code:

    claude mcp add --scope user inkscape -- uvx inksmcp

    For Claude Desktop, Cursor and others:

    { "mcpServers": { "inkscape": { "command": "uvx", "args": ["inksmcp"] } } }
  3. Ask: "Using the inkscape tools, make an A5 flyer for a book swap on Saturday at 10:00 in the library garden, show me a preview, then export a PDF."

Details for each OS and client, and troubleshooting, are in getting started.

Tools

Area

Tools

Documents

document_create, document_open, document_save, inspect (outline, real bboxes, find), inkscape_info

Elements

add_elements (rect, circle, ellipse, line, polyline, polygon, path, text, group, arrow, image, use), update_elements, delete_elements, import_file, repeat

Arrangement

align, layout, split, connect, z_order, move_to_layer, page_fit, page_resize

Charts

grid (linear/log graph paper and axes), plot (data series in data values)

Paths

path_operation (union, difference, intersection, combine, stroke to path, ...), run_actions (guarded raw Inkscape actions)

Output

render_preview (PNG back to the agent), export

See the recipes for how they combine, and the tool reference for every parameter.

How it works

The document lives in the server as an lxml tree, which is the source of truth. Edits are applied there directly. Anything that needs Inkscape (measuring, boolean operations, connectors, rendering, export) goes to one persistent inkscape --shell process, so a call takes milliseconds instead of the ~1 s it takes to start Inkscape. The architecture notes have the details.

Status

inksmcp is beta (0.3). It has been through eleven field tests, in which agents used it for real tasks. The latest one edited files made in other tools. There are 160 tests, and they run against the real Inkscape.

Known limits:

  • It is developed on Windows with Inkscape 1.4.4. CI also runs on Linux and macOS.

  • The tool list costs about 9k tokens of context per session.

  • Gradients and patterns can't be created as style keys yet. They can be applied through raw style when they already exist in the document.

  • SVG 1.2 flowed text can be moved but not edited.

The roadmap lists what's next.

Contributing

Bug reports, field reports ("I asked for X, here's what the agent struggled with") and pull requests are welcome. See CONTRIBUTING.md. The project is built experiment-first: every Inkscape behaviour it relies on is proven by a script in experiments/ and guarded by a test. The development handbook explains the method.

License

MIT. Inkscape is a separate program under the GPL; inksmcp calls it as an external process.

Available Tools

24 tools
add_elementsA

Add one or more elements in a single call. Returns the new ids. Element spec keys — common: type, id, label, layer (name; created if missing), parent (group id), transform, style (css string or dict). Style shorthands: fill, stroke, stroke_width, opacity, fill_opacity, stroke_opacity, stroke_dasharray, stroke_linecap, stroke_linejoin, font_size, font_family, font_weight, font_style, text_anchor. Geometry per type: rect: x, y, width, height, rx, ry, fit_to, fit_padding, fit; circle: cx, cy, r; ellipse: cx, cy, rx, ry; line: x1, y1, x2, y2, marker_start, marker_end; polyline: points, marker_start, marker_end; polygon: points; path: d, marker_start, marker_end; text: x, y, text, line_height, vertical_anchor, width, halo, halo_width; group: -; arrow: x1, y1, x2, y2, shaft_width, head_width, head_length; image: x, y, width, height, href, object_fit, embed; use: x, y, width, height, href. points = [[x,y],...]. text supports '\n' for multiple lines; font_size is in user units. Coordinates are in the parent's system: inside a transformed layer or group (e.g. after layout moved it, or a scaled plan group) they are offset/scaled with it. use: href "symbol_id" places a symbol (or any element) of this document; "library.svg#symbol_id" copies that symbol from a file first (list a library's symbols: document_open it, inspect find {"type": "symbol"}); width/height scale a symbol that has a viewBox. clip: an element id (its current shape) or [x, y, w, h] — the element is cut to it and the clip then moves with the element; null removes it. text halo: '#ffffff' outlines the glyphs behind the fill so text reads over lines (halo_width default 0.3 x font size; 'none' removes). place: {"below": id, "gap": 1.5} (or above / left_of / right_of; "align": start|center|end on the other axis) puts the element beside the MEASURED box of another (real glyph extents, any script) and keeps it there when that element changes; inside repeat, ids are template names; null frees it. Responses warn about overlaps among the touched elements: text on text, text across a shape's edge, text crossed by a line (a text with a halo may cross lines). rect fit_to: [ids] sizes the rect around them after wrapping (fit_padding: n | [v, h] | [t, r, b, l]; fit: both | height | width, e.g. height keeps a card's width); it re-fits when those elements are edited (update_elements {"id": rect} re-fits after moves; fit_to: null frees it). defaults is merged into every element (only keys valid for its type), e.g. {"font_size": 2.2, "fill": "#2a8a4a", "layer": "Labels"}. elements_path: a JSON file (a list of specs, or {"elements": [...], "defaults": {...}}) instead of elements — for generated geometry, so it never passes through the conversation.

Inputs: doc_id, preview, defaults, elements, +1 more.

alignA

Align elements to the page, to each other, or inside another element — e.g. centre a label in a box: {"ids": ["label"], "to": "box", "horizontal": "center", "vertical": "middle"}. Operations run in order, each seeing the previous moves. Returns the moves [dx, dy] and new bboxes in user units.

Inputs: operations (required), doc_id, preview.

connectA

Draw arrows/lines between elements that stay attached when things move (align/layout/ update_elements/page_fit). Default: native Inkscape connectors — clipped to the real shape (circles etc.) and still live in the Inkscape GUI. With from_side/to_side/via you control the route (e.g. a loop diagram: {"from": "condenser", "to": "valve", "from_side": "left", "to_side": "top", "routing": "elbow"}); those attach at the middle of the chosen side of the bounding box, as do connectors with start_gap/end_gap. Returns the connector ids, their layers, label ids and warnings.

Inputs: connections (required), doc_id, preview.

delete_elementsA

Delete elements (and their children) by id. Connectors attached to them and their labels are deleted too; all removed ids are returned. Defs only they used (symbols of an imported library, gradients, clip paths) go too: defs_removed counts them.

Inputs: ids (required), doc_id.

document_createA

Create a new blank document and make it current. The viewBox matches width/height, so coordinates are in unit. background (e.g. '#ffffff') adds a full-page rect with id 'background'.

Inputs: width (required), height (required), unit, background.

document_openA

Open an existing SVG (.svg or .svgz) and make it current. Returns its outline, and notes when the file was normalised for editing without changing how it renders: a page whose viewBox doesn't match its width/height (or uses %), fill/stroke set on the root (new elements would inherit them), ids given to elements that had none.

Inputs: path (required).

document_saveA

Save the document as Inkscape SVG (to path, or where it was opened/last saved).

Inputs: path, doc_id.

exportA

Export via Inkscape. Format defaults to the file extension. What is exported: region [x, y, w, h] (user units, everything visible); or ids — only those objects cropped to them (only_ids=true, default) or the area around them with everything visible (only_ids=false); otherwise area 'page' or 'drawing'. For PNG set dpi or width/height (px); background e.g. '#ffffff' (default transparent).

Inputs: path (required), dpi, ids, area, +8 more.

gridA

Draw a grid / graph paper inside rect [x, y, w, h] — linear or logarithmic per axis — without computing any line positions. Axis specs: linear: {"scale": "linear", "major": 10, "medium": 5, "minor": 1, "label_start": 0, "label_step": 1} (spacings in user units, each a whole multiple of the finest; labels on major lines) log: {"scale": "log", "cycles": 3, "subdivisions": "standard" | "fine" | "integers", "start": 10} (decades major, 2..9 medium, subdivisions minor; "start" = value at the origin (default 1); "labels": "decades" (start, start*10, ...; default when start is given) | "paper" (1..9 per cycle)) "reverse": true flips an axis (default x left→right, y bottom→top); "lines": false keeps the axis (labels, plot mapping) but draws none of its gridlines, e.g. vertical-only lines for a bar chart. weights: {"major", "medium", "minor"} stroke widths (defaults 0.45/0.22/0.08 mm); border: stroke width of the frame (default 0.6 mm, 0 = none). labels: {"sides": ["left", "bottom"], "font_size", "gap", "color", "font_family", "bold_major", "x_title", "y_title", "title_font_size"} — placed outside the grid, centred on their lines (measured); major labels are bold unless "bold_major": false; titles go below / left (rotated) of the tick labels. Result: one path per weight class in layers '<layer_prefix> minor/medium/major', labels in '<layer_prefix> labels'. Use plot with grid=<id_prefix> to draw data on it.

Inputs: rect (required), x, y, color, +7 more.

import_fileA

Place a file into the current document. An SVG (e.g. geometry a script generated) becomes one group scaled to this document's units, its top-left at at (default 0,0); width or height scales it (both: stretch). Its layers become labelled groups, its defs join ours, clashing ids get "-" in front. An image (png/jpg/gif/webp/bmp) becomes an image element: natural size at 96 dpi unless width/height (one keeps the ratio), linked by default (embed: true stores the pixels in the SVG), object_fit contain|cover|fill for a given box. Relative paths start at the document's folder. For whole documents use document_open; for data rows see repeat rows_path.

Inputs: path (required), at, id, embed, +7 more.

inkscape_infoA

Inkscape version and location, server version, and open documents.

Inputs: no named parameters.

inspectA

Outline of the document: layers, groups and elements with ids, fill/stroke as rendered (style, attributes or inherited), text, use targets (href) and real visual bounding boxes [x, y, width, height] in user units (measured by Inkscape). Layers/groups with more than max_children children are summarised (counts per type, first/last ids, bbox). To list one of them, pass layer (layer name or group id) and a larger max_children. find: a flat list of matching elements instead of the tree, e.g. {"fill": "#99cc32"} (colours compared in any notation), {"type": "text", "text": "total"}, {"type": "use", "href": "Parking"}, {"type": "symbol"} (symbols in defs, with titles), {"id_prefix": "card-"}.

Inputs: bbox, find, layer, doc_id, +1 more.

layoutB

Arrange items in a row, column or grid with a gap — no coordinate maths needed. An item is an id or a list of ids that move together, e.g. ["box1", "box1_label"], or {"ids": [...], "anchor": "fig1-border"} to arrange by that member's box (plot frames line up even when their tick labels differ in width); the rest moves along. gap is a number or [horizontal, vertical]. align places items on the cross axis (grid: within cells). The block stays where the first item is, or its top-left goes to at [x, y], or it is aligned to to ('page' or an element id) using horizontal/vertical/margin. Connectors follow.

Inputs: items (required), at, to, gap, +8 more.

move_to_layerB

Move elements into a layer (by name; created on top if missing) or into a group/layer by id, at its top or bottom. They keep their relative order and stay visually where they were.

Inputs: ids (required), layer (required), doc_id, preview, +1 more.

page_fitB

Resize the page to fit the drawing (or only ids) plus margin (one number, [vertical, horizontal] or [top, right, bottom, left], user units). All content moves together so the page keeps its 0,0 top-left; full-page background rects are resized, connectors follow.

Inputs: ids, doc_id, margin, preview.

page_resizeB

Set the page size in user units (e.g. 210 x 297 for A4 in a mm document). anchor='center' keeps the drawing centred on the new page; 'top-left'/'none' leave content where it is. Backgrounds are resized.

Inputs: width (required), height (required), anchor, doc_id, +1 more.

path_operationC

Geometry operations performed by Inkscape on the given ids. For difference, the top-most (later in document order) object is subtracted from the bottom one. to_path converts shapes and text into paths. union/intersection/exclusion/difference keep the BOTTOM object's id, style and layer; combine keeps the TOP object's (E22). result lists the ids that hold the outcome.

Inputs: ids (required), operation (required), doc_id, preview.

plotA

Plot data on a grid made with the grid tool, in DATA values — no coordinate maths. grid: that grid's id_prefix. Each series: {"points": [[x, y], ...], "line": true, "stroke": "#1f77b4", "stroke_width", "stroke_dasharray", "marker": "circle"|"square"|"diamond"|"none", "marker_size", "marker_fill" (default white), "point_labels": ["", "COP 3.2", ...] (one per point, null/"" to skip), "label_offset": [dx, dy], "label_anchor": "start"|"middle"|"end", "label_halo": "#ffffff"|"none", "label_font_size", "label_color", "font_family", "id", "layer"}. Labels: label_offset is from the point to the label's anchor, and the label is vertically CENTRED on point + dy (dy = 0 → centred on the point). Anchor defaults to start for dx >= 0, else end. Without label_offset the label goes right of the point, on the side the line is not heading to. Labels carry a halo stroke (default white) so the line can cross them — set label_halo "none" (or the background colour) for labels on dark fills; recolouring a label later does not remove its halo (use stroke: "none"). Ids follow the series id: -line, -marker-, -label- (k = 1-based point index). Returns per series those ids and the points in user units (for annotations); warns about points outside the grid.

Inputs: series (required), grid, doc_id, preview.

render_previewA

Render to a PNG image you can look at (white background, longest side = max_size px). Zoom in with region [x, y, w, h] in user units, or with ids (the area around them, everything still visible). only_ids=true draws just those objects.

Inputs: ids, area, doc_id, region, +2 more.

repeatA

Stamp a block of elements once per data row — timelines, card grids, tables, legends, map symbols — in one call. template: element specs as for add_elements, drawn for the FIRST row. "{key}" in any string is replaced from the row ("{year}"; a value that is exactly "{w}" keeps the row's number), in geometry and style alike ("fill": "{colour}"); "{n}" is the row number (1-based) and "{i}" the index, so rows can't use the keys n and i. Ids are local names: "card" becomes card-1, card-2, ... A "parent" may name another template element (e.g. a group holding a card and its texts); fit_to and clip may name template elements of the same row. Each row goes into a group <id_prefix>- moved by n-1 steps: step [dx, dy], or a grid with columns (step = [column pitch, row pitch]), filled row by row or, with order "column", column by column. cell ["col", "row"]: each row is placed by its own 1-based column/row values (gaps and fractions allowed: periodic tables, calendars, timetables, lanes); the template is drawn for cell (1, 1) and step is the [column, row] pitch. Components (a character, a node, a symbol placed at data positions): a template group with "transform": "translate({x},{y}) scale({s})" and step [0, 0], parts drawn around a local origin, pose/shape parts as placeholders ("d": "{arms}"). mirror {"x": 148.5, "rows": "even"|"odd"|"all"} (or "y") mirrors those rows about the axis: shapes are reflected (pointers flip), texts and groups keep their reading direction and move as blocks — group a card with its texts so they cross together. Per element "mirror": "reflect"|"block"|"none" overrides. rows_path: a .json (list of row objects) or .csv file (header line = keys, numbers parsed) instead of rows, so data never passes through the conversation. Returns the row groups, ids per template name (runs shortened to "card-1..card-12"), wrapped_lines, fitted.

Inputs: step (required), template (required), cell, rows, +9 more.

run_actionsA

Escape hatch: run raw Inkscape actions (e.g. "object-align:left last", "transform-rotate:30") after selecting select ids. File, export, window and quit actions are blocked. Extensions (org.inkscape.*) always act on the whole document, so they can't be combined with select.

Inputs: actions (required), doc_id, select, preview.

splitA

Divide a region into named cells — comic panels, dashboard tiles, poster columns, table grids — without computing any positions. region: "page", an element id (its measured box) or [x, y, w, h]; margin insets it (one number, [vertical, horizontal] or [top, right, bottom, left]). rows: one entry per row, either a number of equal columns or a list of column ratios, e.g. [[2, 1], [1, 1], [1, 2]]; heights: row ratios (default equal); gutter: space between cells (number or [horizontal, vertical]). Creates one rect per cell, <id_prefix>-1, -2, ... row by row, in layer — invisible unless style gives e.g. {"fill": "#fff", "stroke": "#000", "stroke_width": 0.6, "rx": 2}. Use the ids as targets: clip ("clip": "cell-5"), align/layout ("to": "cell-2"), place, fit, connect. An element region is its measured box (stroke included). Tables: split the header strip into columns (e.g. rows [[3, 1, 1]]), then repeat the data rows with step [0, row pitch] and texts at the returned column edges (numbers: text_anchor end at the right edge). Returns the cells [x, y, w, h] and their ids per row.

Inputs: rows (required), layer, style, doc_id, +6 more.

update_elementsB

Change existing elements. Each update is {"id": ..., }; only the given keys change. Style shorthands merge into the existing style. Set transform to "" to clear it.

Inputs: updates (required), doc_id, preview.

z_orderA

Change stacking order (what is drawn on top). front/back: top/bottom within the element's own layer or group. forward/backward: one step past the next object it overlaps (visible change). above/below: directly above/below target, moving into target's layer/group if needed while keeping the visual position. Several ids keep their relative order. Returns each id's position.

Inputs: ids (required), operation (required), doc_id, target, +1 more.

Tool Schema Changelog

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

Loading tool schema changelog…

    TDQS

    A3.7/5.0

    Scored across 24 tools

    Disambiguation5/5

    Each tool has a clearly distinct role in the SVG/Inkscape editing workflow. Potential overlaps such as align vs layout, z_order vs move_to_layer, and render_preview vs export are well differentiated by their descriptions and parameters.

    Naming Consistency4/5

    All names are consistently snake_case and readable, with common verb_noun or resource_action patterns such as document_create, add_elements, and update_elements. However, single verbs and noun phrases like inspect, repeat, grid, and z_order mean the naming is not a strict uniform verb_noun pattern.

    Tool Count4/5

    At 24 tools, the surface is on the heavy side but each tool corresponds to a distinct capability area in a full vector-editing server. The count is justified by the domain's breadth, though it sits near the upper end of what is comfortable.

    Completeness5/5

    The surface covers document lifecycle, inspection, element creation/update/deletion, data-driven repetition, import, region splitting, connectors, layout, path operations, z-order, layers, alignment, page sizing, grid/plot generation, preview, and export. The run_actions escape hatch fills remaining raw Inkscape gaps, leaving no obvious dead ends.

    Maintenance

    ActivitySlowing
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers