Gotchas
The MCP surface has several behaviors that won’t show up in tool signatures. This page is the one-stop reference for the silent-fail / surprising-behavior set.Format and dimensions
Setformat_category explicitly on start_design_task. If you omit it, a fresh canvas (which has no dimensions to infer a format from) is created as a generic other canvas at 1080×1080 with no format skill — so a deck, post, or document gets a generic layout. Pass one of:
This is the full category enum — the same 10 values as
moda_mcp/server.py’s start_design_task format_category docstring.
Slide count goes in the prompt text, not a parameter. Say “10-slide deck” in the prompt. The REST API has number_of_slides; the MCP server does not expose it.
Carousel page count clamps to your plan tier, the same limit slides use (free=10, paid=30, ultra/enterprise=100) — there’s no separate, lower carousel-specific cap. Pass carousel_page_count explicitly or omit it for a default target.
model_tier="pro_max" is silently coerced to pro. Pass pro directly.
Task lifecycle
start_design_task defaults to wait=False — returns a task handle (task_id, canvas_id, canvas_url, conversation_id, status: "queued") in milliseconds. Poll get_task_status(task_id) at retry_after_seconds (~3s) until is_terminal == true. Call export_canvas only when can_export == true.
remix_design defaults to wait=True — opposite of start_design_task. With a prompt, it blocks until the agent finishes. For bulk fan-out, pass wait=False explicitly or each call serializes.
Terminal statuses: completed, failed, cancelled, dead_letter, canvas_deleted (source canvas was deleted mid-run), insufficient_credits (billing block). Don’t pattern-match this list yourself — just check the is_terminal field on the task/response; it’s derived from the full terminal set and stays correct even if this set grows.
Cancellation: cancel_task(task_id). No-op on already-terminal tasks. Cancelling a blocking MCP call publishes the cancel signal internally — billing stops with the job.
Failed tasks are deterministic. Don’t silently retry on {status: "failed"} — surface the error to the user. The exception is upstream model errors, which are sometimes transient (retry once).
Conversations vs canvases vs remixes
Mutual-exclusion matrix on
start_design_task:
The asymmetry is intentional:
conversation_id always wins over canvas_id (older quirk, predates template_canvas_id), but template_canvas_id is strict because it changes the operation shape (source-copy-then-run vs in-place edit / resume).
Templates and themes
Discovery filters on
list_my_canvases / search_canvases: template_type (‘template’ or ‘theme’) filters to only that kind; exclude_templates=true returns only regular canvases (mutually exclusive with template_type); category narrows further by format. template_type='theme' plus a non-slides category is a valid, knowably-empty query, not an error. On list_my_canvases, query is optional — omit it (or pass an empty one) and template_type alone browses the team’s curated set by recency. An empty result means the team curated none — say so, don’t present an ordinary canvas as a template. Any readable canvas still works as start_design_task’s template_canvas_id; the filters only narrow discovery, not what’s eligible to remix. See ../recipes/find-a-template.md.
list_my_canvases when the user should see the canvases; search_canvases when only you need the id. Both take the same query + filters and return the same rows, but only list_my_canvases renders the gallery iframe — call it (optionally pre-filtered with query) when the user asked to see, browse, or pick a canvas, and search_canvases when you’re resolving a canvas_id / template_canvas_id for your own next call. Same split as list_brand_kits vs find_brand_kits below.
The guidance field: a row carries {"agent_instructions": "…", "agent_instructions_truncated": true, "pinned_note_count": 4} when the canvas’s author pinned agent instructions or locked pages — emitted for any canvas with authored instructions, not just templates. Truncated at 500 chars.
The three-rule contract, aware-don’t-restate: a template_canvas_id remix’s author-pinned instructions and locks travel onto the copy and reach the design agent automatically. So (1) never copy them into prompt — a paraphrase there creates a weaker, drifting duplicate; (2) never write a prompt that asks for something they forbid; (3) on a genuine conflict, ask the user rather than picking a side — locked nodes/pages are a hard constraint, but everything else is author-intent vs user-request with no defined runtime winner.
Brand kits
find_brand_kits for lookups; list_brand_kits only when the user asked to see them. Both return the same data, but list_brand_kits renders a visual showcase iframe on every call (per the MCP Apps spec, iframe rendering is decided at tool-listing time and can’t be suppressed per-call). When you’re just trying to pick a brand_kit_id for another tool, use find_brand_kits — it’s JSON-only and doesn’t steal screen real estate.
Resolution order for start_design_task / remix_design:
- Explicit
brand_kit_idparameter (highest priority) - Session preference (
set_session_brand_kit, or the showcase iframe’s “Use for this session” button) - Team default brand kit
- None (only when
skip_brand_kit=trueis set)
start_design_task(template_canvas_id=…), a brand_kit_id that differs from the
source’s makes the server force-bundle the full-rebrand skill; a matching (or omitted)
kit makes it content-only. The prompt does not need to ask for the rebrand and should
not — write it as a content brief.
set_context clears the session brand kit. Kits are team-scoped; a kit pinned on Team A is meaningless on Team B.
set_session_brand_kit is session-only. It does NOT touch the team default. To change the team default, use set_default_brand_kit — destructive, only call on explicit user request.
Ask the user when the team has multiple kits and get_moda_bootstrap shows no session preference. Silently falling back to the default isn’t always what they want.
import_brand_kit_from_fig is async and NOT task-registry-backed. Unlike start_design_task/remix_design, it does not appear in get_task_status or list_tasks — it returns a job_id immediately and you poll the companion get_fig_import_status(job_id) tool instead (same Redis-backed job the web app’s progress UI reads, so a job started here is visible there and vice versa). The user must have exported the file from Figma via File → Save local copy… first, then uploaded it via create_upload_url → register_uploaded_file (.fig is an allowed upload MIME type) to get the file_id. Omitting brand_kit_id creates a new kit; passing one merges additively (re-importing the same file adds nothing — colors/fonts are deduped against what’s already there). There is no feature flag to check — the tool is always available even though the .fig option is gated in the web UI.
First kit per team becomes default automatically. To promote a different kit later as the team default, use set_default_brand_kit(brand_kit_id).
Attachments
Two shapes:- File-id form (preferred):
{file_id, role, label?}— carriesrole∈source/reference/asset/importthat changes agent behavior - URL form (legacy, public URLs only):
{url, name?, type?}— drops role metadata
source— extract content from this file (use its text/data — e.g. a brief PDF, meeting notes)reference— emulate this file’s style (don’t copy content — e.g. a screenshot of a design you like)asset— drop the file in verbatim (e.g. a logo, hero image) — currently a hint rather than a hard guarantee (behavior still evolving per ENG-2549)import— PPTX-only: converts a PowerPoint attachment into editable canvas slides. It always creates a new slides canvas, so it can’t be combined withcanvas_id,conversation_id, ortemplate_canvas_id, needs the file-id form (not the URL form), allows at most oneimportattachment, and requiresformat_categoryto beslidesor omitted. Violating any of those is a422 invalid_attachment_role, not a silent fallback.
reference makes the agent emulate its style instead of placing it. Passing a brief as reference makes the agent mimic its formatting instead of using its content.
Don’t confuse this with the brand-kit-image role enum. add_brand_kit_image(role=…) uses a different (overlapping but distinct) set: logo / reference / asset. logo belongs to brand-kit images only; source belongs to design-task attachments only.
Two upload paths:
upload_file(source_url=…)— one call, when the file is already at a public URL. Content-hash deduplicated.create_upload_url→ PUT bytes to the returnedupload_url→register_uploaded_file(storage_key, …)— three steps, when the file is local with no public URL.
create_upload_url PUT goes to mcp.moda.app. The upload_url it returns is on the Moda MCP host itself (https://mcp.moda.app/uploads/proxy?token=…) — not a Google Cloud Storage URL. That’s deliberate: it’s the same host the MCP connector already talks to, so sandboxed clients with a network egress allow-list don’t need a separate rule. If a hardened client does block the out-of-band PUT, the one host to allow is mcp.moda.app — never all of storage.googleapis.com.
For existing Moda canvases as inspiration, don’t upload a screenshot — pass reference_canvas_ids=[cvs_…] directly. The agent sees the structure natively.
Concurrency caps (bulk fan-out)
Per-organization in-flight cap onstart_design_task + remix_design (shared across every team in the org — a two-team org on paid has one pool of 10 slots between both teams, not 10 each):
Exceeding the cap surfaces as a tool error on the call that puts you over. Use a windowed launch — keep at most
cap tasks in flight; slot in the next as each terminates. Default the window to 3 when the plan is unknown. See ../recipes/bulk-variants.md for the pattern.
Exports
A finished design task already includes its export — don’t re-export. A completedstart_design_task / remix_design carries result.export ({url, format, status, page_count}) — the design rendered to a file in the canvas’s category-default format. Read that. Calling export_canvas for the same just-finished canvas re-does work the task already did; only call it for a different format or page, or for a canvas that wasn’t just produced by a task.
export_canvas returns {status: "not_ready", reason, retry_after_seconds} while a design task is still running on the canvas. Not an error — retry after retry_after_seconds. Reasons: active_design_job (most common), job_status_unavailable (transient).
Large multi-page PDFs/PPTX can exceed the ~20s sync wait budget and return {status: "in_progress", task_id}. Poll get_export_status(task_id) for the URL. Export task records are kept ~1 hour.
Signed export URLs expire after 7 days.
Image vs document defaults: PDF / PPTX default to all pages. PNG / JPEG with page_number omitted on a multi-page canvas bundle all pages into a .zip (not just page 1) — pass page_number explicitly to get a single raw image.
Session context (multi-org users)
Session context (org + team) is sticky for 24h across reconnections.- Single-org user: nothing to do; primary workspace applies.
- Multi-org user: call
get_context()at session start; if unset, ask which org/team.set_context(org_name, team_name)takes names (case-insensitive), not UUIDs.
list_brand_kits, start_design_task, remix_design, upload_file) read from session context. Per-call overrides: pass org_id / team_id for one-call overrides without changing the session default.
Design-to-code (separate workflow)
get_moda_canvas(url=…) returns semantic pseudo-HTML (<Card>, <Button>, <Heading>, etc.) that maps directly to React / Vue / HTML / SwiftUI. get_moda_canvas_tokens(url=…) returns colors / fonts / radii / variables as JSON for theme config.
- Layer names drive tag quality. A rectangle named
cta-buttonbecomes<Button>. Unnamed elements use visual heuristics. - Multi-page canvases: call
list_moda_canvas_pages(url=…)first to plan per-page fetches; omittingpage_numberconcatenates everything into one (expensive) response. - Pair with
export_canvas(format="png")for pixel-perfect or complex-gradient cases that pseudo-HTML can’t fully capture.
docs.moda.app/mcp/design-to-code and docs.moda.app/mcp/naming-layers.