Skip to main content

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

Set format_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:
  1. Explicit brand_kit_id parameter (highest priority)
  2. Session preference (set_session_brand_kit, or the showcase iframe’s “Use for this session” button)
  3. Team default brand kit
  4. None (only when skip_brand_kit=true is set)
Don’t restate brand colors / fonts / logos in the prompt when a kit applies — it fights the kit. The brand kit — not the prompt — selects rebrand vs content-swap. On 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_urlregister_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?} — carries rolesource / reference / asset / import that changes agent behavior
  • URL form (legacy, public URLs only): {url, name?, type?} — drops role metadata
Roles change behavior:
  • 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 with canvas_id, conversation_id, or template_canvas_id, needs the file-id form (not the URL form), allows at most one import attachment, and requires format_category to be slides or omitted. Violating any of those is a 422 invalid_attachment_role, not a silent fallback.
Passing a logo as 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 returned upload_urlregister_uploaded_file(storage_key, …) — three steps, when the file is local with no public URL.
The 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 on start_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 completed start_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.
All workspace-scoped tools (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-button becomes <Button>. Unnamed elements use visual heuristics.
  • Multi-page canvases: call list_moda_canvas_pages(url=…) first to plan per-page fetches; omitting page_number concatenates 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.
Full guides: docs.moda.app/mcp/design-to-code and docs.moda.app/mcp/naming-layers.