tesserae-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@tesserae-mcp Build a dashboard with weather widget for NYC and show preview"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
tesserae-mcp (moved)
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-mcpIt 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
MCP servers (install & configure): https://docs.tesserae.ink/dev/mcp-servers/
Build dashboards with AI: https://docs.tesserae.ink/dev/mcp/
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 toolsadd_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
Scored across 18 tools
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.
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.
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.
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
Related MCP Connectors
Run your TRMNL ePaper (e-ink) displays from any AI agent: devices, playlists and new screens.
Display delivery platform for AI agents. Push HTML, dashboards and live data to screens.
Give your AI agent the power to cast to your home TV and control an interactive canvas. By ArdaBot.
Create, read and live-edit visual boards, Kanban plans, Gantt timelines and diagrams with AI agents.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables 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 npmMIT
- AlicenseNot gradedqualityDmaintenanceEnables 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 npm2MIT
- AlicenseNot gradedqualityBmaintenanceEnables AI agents to compose and edit dashboards through MCP tools, allowing them to manage tabs, widgets, layout, and data bindings via a unified control plane.20MIT
- FlicenseNot gradedqualityBmaintenanceEnables 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.-