Skip to main content

A FastMCP server exposing structured, hierarchical SVG authoring with a render-and-see feedback loop.

Project description

svg-mcp

svg-mcp

📖 Rendered documentation: docs.georgeharker.com/svg-mcp — with a gallery and cookbook.

A FastMCP server that exposes structured, hierarchical SVG authoring to an LLM — all primitives, gradients/patterns, paths, text-on-path, embedded raster, reusable styles — built around an inkex-backed document model the AI can navigate, query, and manipulate as groups, layers, transforms, and masks, with a fast construct → render → see → iterate loop.

The logo above was authored entirely through these tools — see Usage.

See DESIGN.md for the full architecture and the INKEX_PRIMITIVES.md catalog for the inkex → tool mapping.

Quickstart (Claude)

  1. Prerequisites — just Python ≥ 3.12. Rendering happens in-process (the resvg engine ships as the resvg-py dependency), so there's no separate renderer to install. Optionally, install the resvg CLI for a small per-render speedup — it's used automatically when on PATH:

    brew install resvg          # OPTIONAL (macOS; or: cargo install resvg)
    

    Linux build headers. On macOS the install is pure wheels. On Linux, two dependencies — lxml and PyGObject (the latter pulled in by inkex) — sometimes have no matching wheel for a recent interpreter and are compiled from source by pip, which needs C headers. Install them up front (see System packages (Linux) for the full per-library mapping and other distros). On Debian 13 (trixie) / Ubuntu:

    sudo apt-get install build-essential python3-dev pkg-config \
        libxml2-dev libxslt1-dev libglib2.0-dev \
        libgirepository-2.0-dev gir1.2-girepository-2.0 libcairo2-dev
    
  2. Install. You need uv — install it first if you don't have it (docs):

    curl -LsSf https://astral.sh/uv/install.sh | sh   # macOS / Linux
    # alternatives: brew install uv  ·  pipx install uv  ·  winget install astral-sh.uv (Windows)
    

    Easiest — no clone. uvx fetches svg-mcp and all its dependencies (including the in-process renderer) into a cached, isolated environment and runs it — nothing to manage:

    uvx svg-mcp --help                                                # once published to PyPI
    uvx --from "git+https://github.com/georgeharker/svg-mcp" svg-mcp  # before PyPI / track main
    

    Use that command as the MCP server command in step 3 (then skip the rest of this step).

    Alternative: install from a clone (for development)

    Pick one of two install layouts — this choice decides the path you point Claude at:

    a) Project-local venv (recommended, self-contained). Creates ./.venv inside the repo and installs svg-mcp editable into it — nothing touches your other environments:

    uv sync                             # → ./.venv with svg-mcp + deps (from pyproject/uv.lock)
    # equivalently, without uv.lock:  uv venv && uv pip install -e .
    # no uv at all:                   python -m venv .venv && .venv/bin/pip install -e .
    

    → entrypoint: ./.venv/bin/svg-mcp (absolute: $(pwd)/.venv/bin/svg-mcp).

    b) Into your own existing venv. Activate the venv you want first, then install editable:

    source /path/to/your/venv/bin/activate      # or otherwise have VIRTUAL_ENV set
    uv pip install -e .                          # or: pip install -e .
    

    → entrypoint: /path/to/your/venv/bin/svg-mcp.

    ⚠️ uv sync always targets the project's ./.venv, but uv pip install / pip install target whichever venv is currently active. If you already have one active, svg-mcp and its dependencies (fastmcp, inkex, fontTools, numpy, Pillow, …) get installed into it. Use option (a) — or an explicit fresh venv — unless you deliberately want it in an existing environment.

  3. Connect Claude — point it at the entrypoint path from step 2 (./.venv/bin/svg-mcp for option a, or /path/to/your/venv/bin/svg-mcp for option b; always use the absolute path in configs):

    • Claude Code — from the repo directory (option a shown):

      claude mcp add svg-mcp -- "$(pwd)/.venv/bin/svg-mcp"
      

      (Or just open the project — it ships a .mcp.json you can approve.)

    • Claude Desktop — add to claude_desktop_config.json and restart the app:

      { "mcpServers": { "svg-mcp": { "command": "/ABSOLUTE/PATH/TO/<venv>/bin/svg-mcp" } } }
      
  4. Try it — ask Claude:

    Use svg-mcp to make a 320×120 badge that says "hello" on a blue gradient, then show me the render.

    Claude creates a document, adds the shapes and text, and calls render_document to show you the image inline — then you iterate in plain language ("bigger text", "add a drop shadow", "outline the text to paths"). See Usage for the conventions and a worked example.

Status

The full inkex catalog is mapped through to 117 MCP tools (see INKEX_PRIMITIVES.md), with ruff/mypy clean and the test suite green.

  • Document model (inkex-backed, multi-document with an active-document default): shapes, paths (+ arc/star factories and path-data ops), text + tspan runs + text-on-path + flowed text, images (base64/file embed), groups (+ ungroup, z-order, duplicate, world-preserving reparent), layers (+ visible/locked/opacity), symbols/use, hyperlinks.
  • Text → paths (text_to_path): pure-Python glyph outlining (fontTools) — flattens tspans, honors bold/italic/per-run fill, and walks text along a curve (<textPath>). list_fonts enumerates installed families; measure_text returns a run's width/height from font metrics so you can fit and center text without a render round-trip.
  • Variable-width strokes (add_variable_width_path): SVG has no native variable stroke-width, so swelling/tapering lines — calligraphy, engraving, brushes, tapered arrows — are expanded into a filled ribbon, with butt/round caps and optional cubic (Catmull-Rom) smoothing of both the path and the width.
  • Squircles & smoothed shapes — a parametric family with iOS/Figma corner smoothing, each add_/edit_ with a stored spec for re-editing: add_squircle (Apple's continuous-corner app icon — rounded rect whose edges ease into the corner arc with cubic Béziers; smoothness 0–1), add_rounded_polygon (the same idea generalized to N sides — soft-cornered triangle/pentagon/ hexagon/…), add_pill (stadium with fully rounded ends), and add_superellipse (the Lamé curve |x/rx|^n+|y/ry|^n=1 — one continuous edge-less curve the exponent morphs from diamond → ellipse → squircle → rectangle).
  • Boolean ops (boolean — union/difference/intersection/exclusion): combine shapes with no new dependency, realized via native clip/mask/compound-path constructs (e.g. an even-width icon bezel is difference of an outer and inner squircle). Operands may be composite groups. Note: the result is a render-time construct, not a re-editable merged path — true geometry-level booleans await an engine (lib2geom).
  • Path offset (offset_path): parallel-curve / inset by a signed distance — concentric rings, even-width bezels, glow outlines, stroke-outlining. A squircle/pill/rounded-polygon is offset EXACTLY by regenerating its params (and stays a re-editable parametric shape); anything else uses an analytic cubic-Bézier offset (adaptive Tiller-Hanson + round/miter/bevel joins) into a new path — approximate, no self-intersection trimming (a large inward offset can fold over itself).
  • Bulk constructors (add_rects/add_circles/add_lines/add_paths/ add_variable_width_paths): add many shapes in one call (one round-trip) — for procedural art, hatching/engraving fields, and data viz.
  • Import existing SVG (import_svg, inline or from a file) into a session document to render, inspect, or edit.
  • Reusable resources: named styles (CSS classes + @name paint refs), linear/radial/mesh gradients, patterns, markers, clip + mask, and filters (blur, drop-shadow, color-matrix/overlay, blend, morphology, component-transfer, turbulence, displacement, plus a raw filter-graph builder).
  • Transforms as primitives: translate, rotate-about-center, scale-about-anchor, skew, raw.
  • Queries / context: current_context, describe_node, list_resources, outline, bbox, computed style, transform/CTM, unit conversion, selectors (find/get_subtree), image extraction; metadata (title/desc/RDF); guides & pages.
  • Render-and-see loop: serialize-then-rasterize via the resvg engine — in-process by default (bundled resvg-py), or the CLI when present; the image is handed back directly as base64 image content. Documents are also published as readable MCP resources (svg://documents, svg://{id}/svg, svg://{id}/render) with change notifications.
  • File export (export_render): faithful raster (png/jpeg/webp via resvg) and true vector (pdf/ps/eps via librsvg). cairo is intentionally avoided — it silently drops SVG filters (a drop shadow renders blank), so it is not faithful to the document.

Architecture

svg-mcp architecture

Authored entirely through the svg-mcp tools and rendered via the server's resvg backend (docs/architecture.svg is the live serialized source).

Three layers: an interface & contract tier (the FastMCP server, pydantic input schemas, and per-session document stores), a document-operations tier (inkex-facing construction/edit, read-only introspection, and the document model), and a rendering & output tier (pure-Python typesetting, the render/export backends, and SVG serialization). See DESIGN.md for the full layering rationale.

Install

pip install -e ".[dev]"

System packages (Linux)

macOS installs entirely from wheels — nothing extra. On Linux, pip falls back to building a couple of dependencies from source whenever no wheel matches your interpreter (common on Debian 13 “trixie” and other recent/edge distros), and those builds need C headers and build tooling. Which library needs what:

Python dependency Why Debian/Ubuntu packages
(all C-extension builds) compiler, headers, pkg-config build-essential · python3-dev · pkg-config
lxml the XML/XSLT C libs it binds libxml2-dev · libxslt1-dev
PyGObject + pycairo (both pulled in by inkex on Linux) GObject-introspection (girepository) and the cairo C lib (PyGObject depends on pycairo) libgirepository-2.0-dev · gir1.2-girepository-2.0 · libcairo2-dev (drag in libglib2.0-dev)
pangocffi / pangocairocffi (cairo extra only) the pango C lib (cairo is already covered above) libpango1.0-dev · libffi-dev

Core install (everything except the optional cairo extra) on Debian/Ubuntu — verified on Debian 13 (trixie):

sudo apt-get install build-essential python3-dev pkg-config \
    libxml2-dev libxslt1-dev \
    libglib2.0-dev libgirepository-2.0-dev gir1.2-girepository-2.0 \
    libcairo2-dev

On trixie the GObject-introspection dev package is the new libgirepository-2.0-dev (it pulls in libglib2.0-dev); on older releases it is libgirepository1.0-dev. Likewise the XSLT headers are libxslt1-dev (the bare libxslt-dev name is only a virtual alias).

If you also install the cairo extra (pip install -e ".[cairo]"), add pango (cairo is already in the core set above):

sudo apt-get install libpango1.0-dev libffi-dev

Equivalents on other distros (same libraries, distro-specific names):

# Fedora / RHEL
sudo dnf install gcc python3-devel pkgconf-pkg-config \
    libxml2-devel libxslt-devel \
    gobject-introspection-devel glib2-devel cairo-devel \
    pango-devel libffi-devel                    # last line = cairo extra
# Arch
sudo pacman -S base-devel libxml2 libxslt gobject-introspection cairo \
    pango libffi                                # pango/libffi = cairo extra

If pip instead finds wheels for every dependency on your platform/interpreter, none of the above is needed — the build-from-source fallback is the only thing that pulls these in.

inkex (the SVG DOM)

Depends on the released PyPI inkex>=1.4.1. Note its API shape (verified against the install): inkex.colors is a flat module, and there is no Image.embed_image() — raster embedding is done manually (base64 data URI). All element classes plus bounding_box, composed_transform, specified_style, and Transform @ composition are present.

resvg (the default renderer)

resvg is a deterministic, cross-platform renderer with native text-on-path and broad filter support and no system libs. It renders in-process via the bundled resvg-py binding (same engine — verified pixel-identical to the CLI), so a bare install is self-contained with no external binary.

If the resvg CLI is on PATH it's used automatically instead — marginally faster, entirely optional:

brew install resvg          # OPTIONAL (macOS; or: cargo install resvg)

Override the CLI path with SVG_MCP_RESVG_BINARY=/path/to/resvg, or force a backend with SVG_MCP_RENDERER=resvg-py (in-process) / resvg-cli. Vector export (pdf/ps/eps) still needs the librsvg rsvg-convert binary; raster output never does.

Optional backends

  • cairo extra — secondary vector/raster backend (PDF/PS/SVG out) via cairocffi + pango; needs the cairo + pango system libs (macOS: Homebrew cairo + pango; Linux: see System packages (Linux)). Stub today.
  • Headless Inkscape — reference renderer + heavy ops, driven via --shell (no D-Bus on macOS). Stub today.

Configuration

All settings are env vars prefixed SVG_MCP_ (or a .env file):

Var Default Meaning
SVG_MCP_RENDERER resvg Render backend: resvg (CLI if present, else in-process) · resvg-py · resvg-cli · cairo · inkscape
SVG_MCP_RESVG_BINARY auto Path to the resvg CLI
SVG_MCP_INKSCAPE_BINARY auto Path to the Inkscape CLI
SVG_MCP_FEEDBACK_MAX_EDGE unset Optional long-edge cap (px); unset = raw image handed back directly as base64
SVG_MCP_DEFAULT_BACKGROUND transparent Default render background (CSS color)
SVG_MCP_RENDER_TIMEOUT_S 30 Per-render subprocess timeout
SVG_MCP_TRANSPORT stdio Server transport: stdio · http · streamable-http · sse
SVG_MCP_HOST / SVG_MCP_PORT 127.0.0.1 / 8000 Bind address for the http transports
SVG_MCP_PREVIEW unset Auto-start the live preview web server on boot (1/true)
SVG_MCP_PREVIEW_HOST / SVG_MCP_PREVIEW_PORT 127.0.0.1 / 8808 Bind address for the live preview

Transport, host, and port can also be set with CLI flags (which take precedence over the env vars): svg-mcp --transport streamable-http --host 127.0.0.1 --port 7731.

Develop

pytest          # tests (resvg smoke test auto-skips if the binary is absent)
ruff check .    # lint
mypy src        # types (no Any / object — precise types only)

Run

svg-mcp                                          # FastMCP server over stdio (default)
svg-mcp --transport streamable-http --port 7731  # or streamable HTTP at 127.0.0.1:7731/mcp
# env vars work too: SVG_MCP_TRANSPORT=http SVG_MCP_PORT=7731 svg-mcp

A long-running HTTP server is handy for a shared/persistent endpoint a bridge can connect to (the server runs as a single process; each client connection gets its own isolated documents).

Live preview

Rendering each step back into the chat costs tokens and, in a terminal, the model sees the image but you don't. The live preview solves both: a tiny loopback web page that mirrors the active document and auto-refreshes on every edit over Server-Sent Events — so you watch the drawing build in a browser while the model keeps working, with no render bytes spent on the conversation.

svg-mcp live preview

  • Start it: ask the model to "show me" (or "open a preview") and it calls the start_preview tool, then hands you the URL. Or set SVG_MCP_PREVIEW=1 to auto-start on boot.
  • Watch it: open the URL once and leave it — it repaints on each change. Toggle PNG (faithful resvg output) vs SVG (crisp vector), zoom, drag to pan, and Save the current frame as PNG/SVG/WebP/JPEG/PDF straight from the page.
  • Per-chat: the URL carries a session token (/<token>/), so each chat gets its own isolated preview even though they share one server and port — one chat's edits never appear in another's.

The page just mirrors the read-only svg:// resources: GET /<token>/active/render is the same render as the svg://{id}/render resource, refreshed by the same change signal.

Experiment with an LLM

Quickest sanity check (no LLM): render a sample poster to PNG.

.venv/bin/python scripts/demo.py            # writes demo_output.png

Claude Code — this repo ships a project .mcp.json; open the project and approve the svg-mcp server, or add it explicitly:

claude mcp add svg-mcp -- /Users/geohar/Development/svg-mcp/.venv/bin/svg-mcp

Claude Desktop — add to claude_desktop_config.json:

{
  "mcpServers": {
    "svg-mcp": { "command": "/Users/geohar/Development/svg-mcp/.venv/bin/svg-mcp" }
  }
}

MCP Inspector — interactively call tools and view rendered images in a browser:

uv run fastmcp dev src/svg_mcp/server.py:mcp

The model's loop is: create_document → add nodes / resources → render_document to see the result inline → iterate → export_svg. The server's instructions describe the full workflow and conventions; each tool carries its own description.

Usage

The tools are called by an LLM over MCP. The core loop is create → build → render-and-see → iterate → export. A minimal session (arguments shown as the JSON each tool receives):

create_document(width=320, height=120)                 # → {document_id:"doc1", active:true}

# define a reusable gradient, then paint with it by name (@name) or url(#id)
define_linear_gradient(x1=0, y1=0, x2=1, y2=0,
    stops=[{offset:0, color:"#7dd3fc"}, {offset:1, color:"#1e3a8a"}], name="brand")
add_rect(x=0, y=0, width=320, height=120, rx=16, style={fill:"@brand"})

add_text(x=160, y=72, content="svg-mcp", name="title",
    style={font_family:"Helvetica", font_size:"40px", font_weight:"bold",
           text_anchor:"middle", fill:"#ffffff"})
apply_drop_shadow(target="title", dx=0, dy=2, blur=3, color="#000", opacity=0.4)

render_document(scale=2)        # returns the rendered PNG inline — look, then adjust
export_svg()                    # final SVG source string

Conventions

  • Active document. create_document returns a document_id and makes it active; you may omit document_id on later calls to target it. Pass it explicitly to switch, or use set_active_document. Call current_context() to re-anchor (active id, open docs, outline).
  • Targets by id or name. Every target/parent/content arg takes a node's returned id or the friendly name you gave it. Name things you'll revisit; reason via find(name=…) and outline. Names should be unique — a name matching several nodes is rejected (no silent guess); disambiguate with a hierarchy path ancestor/name (each segment an id or name) or the id. Each chat/connection has its own isolated set of documents (current_context reports the session_id).
  • Stacking. Later siblings paint on top. Restack relative to a sibling with reparent(target, above=<node>) / below=<node> instead of counting child indices.
  • Coordinates. User units, origin top-left, y increases downward.
  • Style. A structured object — fill, stroke, stroke_width, opacity, plus typography (font_family, font_size, font_weight, font_style, text_anchor). Colors accept hex / rgb() / CSS names / none, or a paint reference url(#id) / @name to a defined gradient or pattern.
  • Resources follow create → define → reference/apply: define_* returns an id you use as a fill (url(#id)/@name) or attach via apply_* (clip/mask/marker/filter). Clip/mask/ symbol/pattern definitions move the listed content nodes into the resource, so build those shapes first. list_resources() shows what's defined.
  • Transforms compose: translate_node, rotate_node (optional center), scale_node (optional anchor), skew_node, or apply_transform("rotate(45 100 100)").
  • Text. add_text + add_text_run for multi-line/styled spans; add_text_on_path to flow along a path. Judge text size with render_document (geometry queries are empty for live text). text_to_path outlines text to glyph paths — font-independent, flattens tspans, and walks <textPath> along its curve; list_fonts lists installable families.

Resources

Open documents are also exposed as readable MCP resources, so a host can surface live state: svg://documents (index + which is active), svg://{id}/svg (source), svg://{id}/render (PNG). Mutations emit resources/updated notifications.

Example: the logo

The header logo (logo.svg) was built with these tools — <mcp>svg</mcp> as a blue gradient wordmark, an orange→white starfield clipped into the glyphs, the inner svg word under a translucent white veil (so the <mcp> tags read as starry space and svg as frosted white), all under one drop shadow — then text_to_path outlined the glyphs so the final file is font-independent. scripts/demo.py shows a smaller end-to-end build you can run directly.

License

GPL-2.0-or-later. svg-mcp's document model is built on inkex (the Inkscape extensions library), which is GPL-2.0-or-later — a strong copyleft license that extends to works that import it. svg-mcp is therefore licensed under the GNU General Public License v2 or later; see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

svg_mcp-0.2.5.tar.gz (1.3 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

svg_mcp-0.2.5-py3-none-any.whl (132.4 kB view details)

Uploaded Python 3

File details

Details for the file svg_mcp-0.2.5.tar.gz.

File metadata

  • Download URL: svg_mcp-0.2.5.tar.gz
  • Upload date:
  • Size: 1.3 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for svg_mcp-0.2.5.tar.gz
Algorithm Hash digest
SHA256 2d1c4ef501b74d92c3658f21b82499e480b84882b09548494391f17cf1c19e6e
MD5 a69746dc8ec901fa23b6db03dfd1d8d7
BLAKE2b-256 5d40d12c1ae7ba0e55e0fce627ebd0d311d7607263482151dcbde6ea08415e7d

See more details on using hashes here.

Provenance

The following attestation bundles were made for svg_mcp-0.2.5.tar.gz:

Publisher: release.yml on georgeharker/svg-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file svg_mcp-0.2.5-py3-none-any.whl.

File metadata

  • Download URL: svg_mcp-0.2.5-py3-none-any.whl
  • Upload date:
  • Size: 132.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for svg_mcp-0.2.5-py3-none-any.whl
Algorithm Hash digest
SHA256 d95c9445356f8df5f15f9d7254f5ef1249b23f6008a7c944fc3a7351b4e7bca5
MD5 72dd2f76006212939adf5807af377d33
BLAKE2b-256 8b2c0f6fbcd37149a525ca9b12426d05cb5ad43ee0d57fde280dbe4907b7e317

See more details on using hashes here.

Provenance

The following attestation bundles were made for svg_mcp-0.2.5-py3-none-any.whl:

Publisher: release.yml on georgeharker/svg-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page