Skip to main content
Glama

tesserae-mcp (moved)

IMPORTANT

This repository has moved into the main Tesserae repo. Development now happens atdmellok/tesserae → packages/tesserae-mcp. This repo is archived and read-only; it is kept only so existing installs and pinned tags keep resolving. It will not receive further updates.

Install

pip install tesserae-mcp

It is published to PyPI from the monorepo. To install from source:

pip install "git+https://github.com/dmellok/tesserae#subdirectory=packages/tesserae-mcp"

Related MCP server: Excalidraw MCP Server

What it is

The stdio MCP bridge that lets an AI agent (Claude Code / Desktop, Codex, Cursor, or any MCP client) build Tesserae canvas dashboards: it lists your widgets and devices, lays out a canvas, renders a preview to check its own work, and pushes to a panel. It is a thin client over a running Tesserae's /api/mcp HTTP surface, so the rendering, widgets, and devices come from your own instance.

Docs

Why it moved

The bridge is a thin client over Tesserae's /api/mcp surface, so keeping it in the same repo holds its tool list and doc-shape in lockstep with the server and tests both in one CI run. It still ships a thin wheel (stdlib plus the mcp SDK only), so an agent-machine install stays light.


Existing pip install git+https://github.com/dmellok/tesserae-mcp commands and the v* tags in this repo continue to work but are frozen. Switch to pip install tesserae-mcp to track new releases.

Available Tools

18 tools
add_elementA

Append ONE element to a canvas and save (each call is a separate save, so an open editor updates live as you build). 'element' is a single element object; returns {ok,id,rev,elements,element_id}. Use set_canvas to replace the whole layout at once.

A canvas document is JSON: { "w": int, "h": int, # artboard size in px (match the target panel) "theme": str, "style": str, # appearance ids from list_widgets().appearance "font": str, "bg": str, # optional font id and background colour override "els": [ , ... ] # painted in list order: first = back, last = front } Elements may sit partly off the panel (it clips at the edge). Each element has a unique "id" and a box "x","y","w","h" (px, top-left origin; x/y may be negative), plus optional "opacity" (0-100) and "rotate" (degrees). By "kind":

  • widget: {"kind":"widget","widget":"","fragment":"full","options":{...}} from list_widgets(); "fragment" from that widget's fragments (or "full"); "options" per get_widget_options().

  • text: {"kind":"text","text":"...","color":"<css or var(--accent-1)>","size":<px, 0=auto>,"align":"left|center|right"}

  • rect: {"kind":"rect","color":"...","fill":true,"stroke":,"radius":}

  • ellipse: {"kind":"ellipse","color":"...","fill":true,"stroke":}

  • line: {"kind":"line","color":"...","stroke":}

  • icon: {"kind":"icon","icon":"","color":"...","weight":"thin|light|regular|bold|fill|duotone"}

  • data: {"kind":"data","source":"","options":{...},"field":"", "display":"text|number|line|bar|sparkline","format":"","unit":"","precision":0, "label":"","color":"...","size":<px, 0=auto>,"align":"..."} Binds a widget's data field to a scalable value or graph. "source" is a widget key from list_widgets(); configure it via "options" (get_widget_options). Use probe_widget_data(source, options) to see the real data shape before choosing "field". "format" (text/number only) is a date pattern ("HH:mm","MMM d","ddd HH:mm"), "relative", or a number pattern ("0.0").

  • html: {"kind":"html","html":"…","css":"div{…}"} A mini widget from static HTML + CSS in a sandboxed iframe (no scripts, no network).

  • svg: {"kind":"svg","html":"<svg …>…","css":""} -- raw SVG, scaled to fill the box.

LIVE BINDINGS ("bind" on ANY element -- makes a SHAPE reflect data): Data elements auto-update, but shapes (rect/ellipse/icon/line/text) are static geometry. Add "bind": [ , ... ] to drive a shape's props from data each render (in lockstep with data elements, no polling). A binding is: {"source":"","options":{...},"field":"","transform":"","params":{...}} transforms: position -- scalar to a coordinate: {"axis":"x"|"y","in":[lo,hi],"out":[p0,p1],"center":} lo/hi may be numbers OR other field paths (e.g. "sun.riseMin"). length -- scalar to a size: {"dim":"w"|"h","in":[lo,hi],"out":[minPx,maxPx],"anchorMax":?} pick -- integer field indexes arrays: {"set":{"x":[...],"color":[...],...},"center":?} color -- scalar to a colour by ascending thresholds: {"stops":[[max,"#hex"],...],"else":"#hex"} gradient -- scalar interpolated smoothly along colour stops: {"stops":[[value,"#hex"],...]} (quantised to the panel palette on e-ink; a value-driven gradient, not animation) icon -- code/string to a Phosphor glyph: {"table":{"":"ph-name"},"default":"ph-name"} Several bindings combine (e.g. bind x by position AND colour by threshold). A binding that can't resolve its value is skipped, so the element keeps its authored props.

FIELD PATHS ("field" on data elements): dotted -- "current.temp" array index -- "hourly.0.temp" (or "hourly[0].temp") pluck (for charts) -- "series.*.total" (or "series[].total") maps .total over every item of the array "series", yielding an array of numbers. Charts (line/bar/sparkline) need a field that resolves to an array of numbers -- use pluck to get one from an array-of-objects. probe_widget_data() returns a "fields" list of the bindable paths (with sample values), so you don't have to reverse-engineer the shape.

EDITING WITHOUT RESENDING EVERYTHING: update_element / delete_element / patch_canvas change one thing without re-sending the whole document (cheaper, fewer errors on a big canvas). add_element appends one.

AVOID CLOBBERING A CONCURRENT EDIT: get_canvas() returns a "rev". Pass it as base_rev to a write; if the page changed since (someone edited it in the UI, or another agent), the write returns HTTP 409 with the current rev, so you re-read instead of overwriting.

LAY OUT BY INTENT, NOT PIXELS: arrange(box, layout, count) returns aligned child boxes (grid/row/column) to spread across your elements -- no hand-computed x/y. measure_text() tells you how wide text renders so a box fits its content (prevents clipping). render_report() reads back what actually rendered (per-element value, overflow flags, live-vs-sample, colours) so you verify without eyeballing the PNG.

MATCH THE HARDWARE: list_devices() reports each panel's colour capability (color_mode, the renderable palette as hex, and a "mono" flag). Design within that palette so colours don't quantise away on the panel.

Inputs: element (required), page_id (required), base_rev.

arrangeA

Compute "count" aligned child boxes inside "box" ({x,y,w,h}) for a "grid" / "row" / "column" layout, so you place cells by intent instead of hand-computing pixels. "gap" is the space between cells, "pad" the inset from the box edge, "cols" forces a grid column count (default ~sqrt). Returns {boxes:[{x,y,w,h}, ...]}; spread them across your elements' geometry (bake them in as normal elements — they stay individually editable).

Inputs: box (required), count (required), gap, pad, +2 more.

create_canvas_pageA

Create a new, empty canvas dashboard and return its id. Then set_canvas() to lay it out. Size it to your target panel (see list_devices).

Inputs: name (required), h, w.

delete_elementB

Remove ONE element from a canvas by id. Returns the compact ack.

Inputs: page_id (required), element_id (required), base_rev.

get_canvasA

Get the full canvas document (size, appearance, and every element) for a page, plus "rev" / "updated_at" / "updated_by". Keep the "rev" and pass it as base_rev on your next write to be warned (HTTP 409) if the page drifted.

Inputs: page_id (required).

get_widget_choicesA

Page through the choice rows for one of a widget's options (kept out of get_widget_options so a picker with hundreds of entries doesn't bloat the schema). "q" filters by case-insensitive substring on value/label.

Inputs: option (required), widget (required), q, limit, +1 more.

get_widget_optionsA

Get the configurable options for one widget, so you can fill an element's "options" correctly (e.g. a weather widget's location). Each option carries a "format" hint for its type. Big choice lists (HA entity pickers) are omitted by default (the option shows "choices_count" + a "choices_endpoint"); pass include_choices=True to inline them, or call get_widget_choices() to page.

Inputs: widget (required), include_choices.

list_devicesA

List registered display devices with panel dims AND colour capability: "color_mode" (e.g. "6-colour (Spectra 6)"), "colors" (the renderable palette as hex), "gamut", "orientation", and a "mono" flag. Match a canvas's w/h to the target panel, and design within its palette so colours don't quantise away on the hardware.

Inputs: no named parameters.

list_pagesA

List existing canvas (freeform) dashboards.

Inputs: no named parameters.

list_widgetsA

List every widget that can be placed on a canvas (with its fragments) and the available theme/style/font appearance options.

Inputs: no named parameters.

measure_textA

Measure how wide/tall text renders in a widget font, so a box fits its content (prevents clipping). "items" is a list of {text, font?, size?, weight?, max_width?}. Returns {items:[{text,width,height,fits}]} where "fits" is whether the text is within max_width. Font names come from list_widgets().appearance.

Inputs: items (required).

patch_canvasA

Change document-level fields (any of name, w, h, theme, style, font, bg, bg_image, bg_fit) without touching the elements. Use update_element / set_canvas for elements. Returns the ack.

Inputs: patch (required), page_id (required), base_rev.

probe_widget_dataA

Return a widget's data as JSON to pick "field" paths before binding a data primitive. Returns {data, data_source, reason, fields}: "data_source" is "live" (real fetch), "sample" (demo fallback because nothing was configured), or "error" (fetch failed) so you never mistake a placeholder for a real result; "fields" lists the bindable dot-paths with sample values (a wrong key simply isn't in the list; an empty payload has fields with null values).

Inputs: widget (required), options.

push_to_deviceA

Render the canvas and push it to the given devices (ids from list_devices). device_ids is required — pushing is always explicit.

Inputs: page_id (required), device_ids (required).

render_previewA

Render the canvas to a PNG at its authored size and return the image, so you can visually check the layout and iterate. This is your feedback loop: place → render_preview → adjust → set_canvas → render_preview again. For a machine-readable check (values, overflow, colours), use render_report().

Inputs: page_id (required).

render_reportA

Read back what a canvas actually rendered, as JSON (a companion to render_preview's image). Per element: the resolved box, the text that rendered, overflow/clip flags (overflow_x when content is wider than its box), "data_source" (live | sample | error | static), and computed colours; plus the board's resolved background / theme. Use it to verify a render — catch clipping, confirm live data, read the real colours — without parsing a PNG. (Widget cells render into shadow DOM, so their "text" may be empty; data primitives and decorations report their text.)

Inputs: page_id (required).

set_canvasA

Replace a canvas dashboard's document. Returns a compact {ok,id,rev,elements} ack (not the full document), or an error with field-level "details" (HTTP 422) if the document is invalid, so you can correct it and retry. Pass base_rev (the rev from get_canvas) to be warned with HTTP 409 if the page changed under you. For a one-field change prefer update_element / patch_canvas. After setting, call render_preview() (or render_report()) to check the result.

A canvas document is JSON: { "w": int, "h": int, # artboard size in px (match the target panel) "theme": str, "style": str, # appearance ids from list_widgets().appearance "font": str, "bg": str, # optional font id and background colour override "els": [ , ... ] # painted in list order: first = back, last = front } Elements may sit partly off the panel (it clips at the edge). Each element has a unique "id" and a box "x","y","w","h" (px, top-left origin; x/y may be negative), plus optional "opacity" (0-100) and "rotate" (degrees). By "kind":

  • widget: {"kind":"widget","widget":"","fragment":"full","options":{...}} from list_widgets(); "fragment" from that widget's fragments (or "full"); "options" per get_widget_options().

  • text: {"kind":"text","text":"...","color":"<css or var(--accent-1)>","size":<px, 0=auto>,"align":"left|center|right"}

  • rect: {"kind":"rect","color":"...","fill":true,"stroke":,"radius":}

  • ellipse: {"kind":"ellipse","color":"...","fill":true,"stroke":}

  • line: {"kind":"line","color":"...","stroke":}

  • icon: {"kind":"icon","icon":"","color":"...","weight":"thin|light|regular|bold|fill|duotone"}

  • data: {"kind":"data","source":"","options":{...},"field":"", "display":"text|number|line|bar|sparkline","format":"","unit":"","precision":0, "label":"","color":"...","size":<px, 0=auto>,"align":"..."} Binds a widget's data field to a scalable value or graph. "source" is a widget key from list_widgets(); configure it via "options" (get_widget_options). Use probe_widget_data(source, options) to see the real data shape before choosing "field". "format" (text/number only) is a date pattern ("HH:mm","MMM d","ddd HH:mm"), "relative", or a number pattern ("0.0").

  • html: {"kind":"html","html":"…","css":"div{…}"} A mini widget from static HTML + CSS in a sandboxed iframe (no scripts, no network).

  • svg: {"kind":"svg","html":"<svg …>…","css":""} -- raw SVG, scaled to fill the box.

LIVE BINDINGS ("bind" on ANY element -- makes a SHAPE reflect data): Data elements auto-update, but shapes (rect/ellipse/icon/line/text) are static geometry. Add "bind": [ , ... ] to drive a shape's props from data each render (in lockstep with data elements, no polling). A binding is: {"source":"","options":{...},"field":"","transform":"","params":{...}} transforms: position -- scalar to a coordinate: {"axis":"x"|"y","in":[lo,hi],"out":[p0,p1],"center":} lo/hi may be numbers OR other field paths (e.g. "sun.riseMin"). length -- scalar to a size: {"dim":"w"|"h","in":[lo,hi],"out":[minPx,maxPx],"anchorMax":?} pick -- integer field indexes arrays: {"set":{"x":[...],"color":[...],...},"center":?} color -- scalar to a colour by ascending thresholds: {"stops":[[max,"#hex"],...],"else":"#hex"} gradient -- scalar interpolated smoothly along colour stops: {"stops":[[value,"#hex"],...]} (quantised to the panel palette on e-ink; a value-driven gradient, not animation) icon -- code/string to a Phosphor glyph: {"table":{"":"ph-name"},"default":"ph-name"} Several bindings combine (e.g. bind x by position AND colour by threshold). A binding that can't resolve its value is skipped, so the element keeps its authored props.

FIELD PATHS ("field" on data elements): dotted -- "current.temp" array index -- "hourly.0.temp" (or "hourly[0].temp") pluck (for charts) -- "series.*.total" (or "series[].total") maps .total over every item of the array "series", yielding an array of numbers. Charts (line/bar/sparkline) need a field that resolves to an array of numbers -- use pluck to get one from an array-of-objects. probe_widget_data() returns a "fields" list of the bindable paths (with sample values), so you don't have to reverse-engineer the shape.

EDITING WITHOUT RESENDING EVERYTHING: update_element / delete_element / patch_canvas change one thing without re-sending the whole document (cheaper, fewer errors on a big canvas). add_element appends one.

AVOID CLOBBERING A CONCURRENT EDIT: get_canvas() returns a "rev". Pass it as base_rev to a write; if the page changed since (someone edited it in the UI, or another agent), the write returns HTTP 409 with the current rev, so you re-read instead of overwriting.

LAY OUT BY INTENT, NOT PIXELS: arrange(box, layout, count) returns aligned child boxes (grid/row/column) to spread across your elements -- no hand-computed x/y. measure_text() tells you how wide text renders so a box fits its content (prevents clipping). render_report() reads back what actually rendered (per-element value, overflow flags, live-vs-sample, colours) so you verify without eyeballing the PNG.

MATCH THE HARDWARE: list_devices() reports each panel's colour capability (color_mode, the renderable palette as hex, and a "mono" flag). Design within that palette so colours don't quantise away on the panel.

Inputs: canvas (required), page_id (required), base_rev.

update_elementA

Change ONE element in place without re-sending the whole document. "patch" is a partial element ({field: value, ...}) merged over the existing one (a provided "options"/"parts" replaces wholesale). The cheap edit path for a big canvas: change a precision, a colour, a location, one box. Returns the ack.

Inputs: patch (required), page_id (required), element_id (required), base_rev.

Tool Schema Changelog

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

Loading tool schema changelog…

    TDQS

    A4.2/5.0

    Scored across 18 tools

    Disambiguation5/5

    Each tool targets a distinct purpose: listing resources, measuring, rendering, editing elements vs documents, and querying widget metadata. The close cousins (render_preview vs render_report, set_canvas vs patch_canvas vs update_element) are clearly separated by their input/output and scope, leaving no real ambiguity.

    Naming Consistency5/5

    Tool names follow a consistent verb_noun snake_case pattern (list_widgets, get_canvas, update_element, push_to_device). The only outlier is 'arrange', a single verb, but it is still a clear imperative and fits the overall style.

    Tool Count5/5

    18 tools is well-scoped for a canvas dashboard editor covering widget discovery, layout, rendering, device management, and document editing. Each tool addresses a distinct need without superfluous additions, and the count is appropriate for the complexity of the domain.

    Completeness4/5

    The set covers the full lifecycle for canvases and elements: create, read, update, delete, render, and push. The only notable gap is the lack of a delete_page tool (or explicit page deletion/archival), which is a minor oversight given list_pages and create_canvas_page cover the rest of page lifecycle.

    Maintenance

    ActivityInactive
    ResponsivenessOutdated data

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables AI agents to programmatically control a live Excalidraw canvas through element-level CRUD operations and real-time synchronization. It allows agents to iteratively build, inspect, and refine diagrams while providing visual feedback via screenshots and scene descriptions.
      5,646 npm
      MIT
    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables AI agents to programmatically control a live Excalidraw canvas with element-level CRUD operations and real-time synchronization. It supports iterative diagramming through scene descriptions, screenshots, and advanced layout tools for collaborative AI-human workflows.
      5,646 npm
      2
      MIT
    • F
      license
      Not graded
      quality
      B
      maintenance
      Enables AI agents to create and manage customizable dashboards with text, list, stats, progress, and chart widgets, accessible via a read-only web viewer and JSON API.
      -