Google Stitch (stitch.withgoogle.com) designs web and app screens. The CanAI Stitch add-on deploys a Stitch screen straight to a CanAI page: the site holds the owner's Stitch API key and calls Stitch's official API itself. You drive it with five MCP tools; you never fetch Stitch HTML yourself and never see the key once it is connected.
This skill is a companion to canai-mcp. Everything canai-mcp says about the transport
(CanAI MCP server only, no WP-CLI, no file edits) and about CanAI's conventions (menus, t(), templates, structure
comments, wpcanai-scan) applies here; this skill only adds the Stitch tools and the Stitch-specific workflow.
Requirements
- CanAI 1.98.0+ with its MCP server connected (canai-mcp's prerequisites).
- The CanAI Stitch add-on (
canai-stitch1.1.0+) active on the site. - No
wpcanai-stitch-*tools in your list? The add-on is not installed or not active. Ask the owner to install it (https://github.com/usewp/canai-stitch). If they can't, a screen's HTML exported from Stitch is just an HTML file: prepare it with canai-prepare and deploy it with canai-mcp like any other. - The key. The owner creates it in Stitch → Settings → API Keys and pastes it in CanAI → Tools → Import
from Stitch — preferred, the key stays out of the chat. Use
wpcanai-stitch-connect { "api_key" }only when the owner hands you the key for that purpose. Never write a key into a template, a file or a message. - Who can deploy: a deploy writes raw
_canai_html/_canai_css, so it followswpcanai-write-meta's rule — a request made with a CanAI API key, or a user withunfiltered_html(forbiddenotherwise).
Pick the mode first
A deployed page is a faithful copy of the design: its nav is hard-coded <a> links, its text is literal, it has
no structure comments. Bringing it up to CanAI conventions means editing it in CanAI — and an edited page is no
longer something a re-deploy can update without throwing those edits away. So decide with the owner which way round
it is, before cleaning anything:
| Mode | When | What you do |
|---|---|---|
| Stitch stays the source | The design is still changing; the owner iterates in Stitch | Deploy, review, re-deploy as often as the design changes. Don't edit the deployed page or layout in CanAI (every edit makes the next deploy stop). The copy stays a copy: fine for previews, landing pages, campaign pages. |
| Hand over to CanAI | The design is final | Deploy once, then run Cleanup below. From then on CanAI is the source: a later Stitch change either goes in by hand, or with overwrite: true (the cleanup is lost — the snapshot keeps it restorable). |
A new page for the same screen. A re-deploy always targets the page an earlier deploy made for that screen; only a page in the trash is ignored. To deploy the design to a fresh page while keeping the cleaned-up one, the owner duplicates the screen in Stitch and you deploy the duplicate (a new screen id, so a new page and layout).
Deploy
- Connected?
wpcanai-stitch-connectwith no arguments →{ connected, key_hint }. Not connected → the key step under Requirements. - Find the project. A Stitch account can hold dozens of projects; the site works with the ones its owner
chose (CanAI → Tools → Import from Stitch → Add projects).
wpcanai-stitch-list-projectswith no arguments lists exactly those. If the project the owner means is there, use it. If it is not (or nothing is chosen yet), look in the whole account —wpcanai-stitch-list-projects { "view": "owned" }, or"shared"for projects shared with the owner (each row hasadded) — ask the owner which project is for this site, thenwpcanai-stitch-set-projects { "add": ["<id>"] }. Never add projects the owner did not name.unavailablein the site list means a chosen project the key can no longer see (deleted, unshared, another account's key): tell the owner;wpcanai-stitch-set-projects { "remove": [...] }clears it. - Find the screen.
wpcanai-stitch-list-screens { "project_id" }→ the screen.deployed_page_id > 0means an earlier deploy made a page for it (re-deploy rules below).hiddencounts items that are not designs (no device, size or preview, such as Stitch's "Extracted text from…" notes): leave them alone — there is no HTML to deploy — and passinclude_other: trueonly to look at them. - Deploy.
wpcanai-stitch-deploy-screen { "project_id", "screen_id" }→ a draft page plus a layout of its own. Passstatus: "publish"only when the owner asked for it. - Read the result.
images.remote> 0 → the 25-second image budget ran out: deploy again (already-copied images are reused) to copy the rest.images.failed> 0 → read the warning. Stitch sometimes generates image URLs Google itself answers with 400 (their ids end in repeated patterns); those are broken in Stitch too and stay remote on every deploy — tell the owner, who can regenerate the image in Stitch or give you a replacement to sideload.site_changes→ whatprepare_siteturned on (Tailwind, the Tailwind plugins the screen uses). Mention it.
- Check it. Call
wpcanai-preview-link { "post_id": <page_id> }and open itsurlin your browser tool at phone and desktop widths. It shows the draft, without the admin bar, even during maintenance mode (canai-mcp: Check your work in a browser). Compare with the screen in Stitch. Afixed top-0bar at the very top is hidden behind WordPress's admin bar for a logged-in owner; visitors and the preview link see it.
What a deploy writes
- Layout
Stitch: <screen title>(template_typelayout, published): CanAI's document shell with{{ wp_head() }}, the screen's Google Fonts links (Material Symbols included), itstailwind.configin a<script id="tailwind-config">afterwp_head(), and its<html>/<body>classes and attributes. The screen's head<style>goes to the layout's_canai_css. Stitch's Tailwind CDN script is dropped: CanAI serves its own Tailwind v3. - Page:
_canai_htmlis the screen's<body>after a one-line{# Stitch: … #}note;_canai_layoutpoints at the layout. Any{{,{%or{#in the design (layout CSS included) is printed literally ({{ '{{' }}), so a design can never run Twig. Images that carried onlydata-altget a realalt. - Images are copied to the media library, attached to the page and named after the screen
(
<screen-slug>-1,-2, … with the image's own extension), and the markup points at the copies; a re-deploy reuses them. - One snapshot operation covers the page and the layout:
wpcanai-restore-operation { "operation_id", "confirm": true }undoes the deploy.
Re-deploy
Deploying the same screen again updates that page and its layout in place and keeps the page's title, slug,
status and parent; status changes only when you pass it.
If, since the last deploy, the page's _canai_html, the layout's code, or the page's layout choice was changed in
CanAI, the deploy stops with stitch_page_edited (409) and writes nothing. The error data names what changed:
edited ⊆ page, layout, layout_choice. Content a snapshot restore put back is not a change. Then:
- the owner wants the new design over those edits → deploy again with
overwrite: true(the snapshot keeps the edited version restorable); - the edits are your cleanup and the owner wants to keep them → don't overwrite; port the design change by hand
(read the new screen in Stitch, edit with
wpcanai-replace-in-meta).
Never pass overwrite: true on your own initiative.
Cleanup — hand over to CanAI
Run this only in Hand over mode, on the deployed page (page_id) and its layout (layout_id). It is canai-mcp's
"Implement HTML → CanAI" applied to a page that is already on the site; read that section and Menus there for the
detail of each rule.
- Look.
wpcanai-read-metaon the page and the layout, thenwpcanai-scan { "post_id": <page_id> }. Expecthardcoded_nav, missing structure comments and literal strings; clear them in the steps below. - Pages first. Stitch's nav links are mostly
href="#"placeholders. Ask the owner which page each nav item should open; create missing pages withwpcanai-create-page(or deploy their Stitch screens) before writing the menu. Never invent URLs. - Header and footer → templates (when they belong on other pages too). Create them with
wpcanai-create-template—typeheader/footer, a title that slugs to exactlyheader/footerunless the site already has those (then reuse or merge; checkwpcanai-list-templates). Move the screen's<header>/<footer>markup into them, then, withwpcanai-replace-in-meta, put{{ wpcanai_template('header') }}before and{{ wpcanai_template('footer') }}after{{ page_content }}in the Stitch layout, and remove the markup from the page. A screen-specific header can stay in the page. - Navigation → menus. Write each nav list once with
wpcanai-write-menu(primaryfor the header,footerfor the footer — never another location), then replace the list with theget_menu()loop, keeping the screen's classes on the<nav>and the<a>. A mobile drawer loopsget_menu('primary')again. - Internal links. Other links to site pages →
{{ turl(id_url(<id>)) }}(orslug_url()); in-page#sectionanchors stay. - Strings →
t(). Wrap user-facing copy one whole sentence pert(), inline tags inside, block elements outside, dynamic values as{placeholders}(canai-mcp Native string translation). - Structure comments. Label the sections with
{# Type / Short Label #}comments (canai-mcp's "Implement HTML" step 7 and STRUCTURE-NAVIGATION.md), replacing the HTML comments Stitch left. - Images (optional). The markup points at the copies by URL. To follow CanAI's helper rule, swap each for
{{ media_url(<id>, 'full') }}/{{ image_attrs(<id>, 'src,alt') }}— find the ids withwpcanai-list-media(searchby the screen slug). Ask the owner about images left on Google. - Icons. Material Symbols keep working (the layout loads the font). Convert to Lucide only when the site uses Lucide elsewhere and the owner wants one icon set.
- Scan again until clean, open the page, and tell the owner the page is now CanAI's: a re-deploy from Stitch
will stop with
stitch_page_editedand onlyoverwrite: truewould replace the cleanup.
Tools
wpcanai-stitch-connect
- Args:
{ "api_key"?: string, "disconnect"?: boolean }. Withapi_key, Stitch checks the key before it is stored; a key Stitch rejects is not stored and an existing one is kept.disconnect: trueremoves the stored key. Neither → the current status. - Returns:
{ "connected": bool, "key_hint": "…ab12", "projects"?: int }— never the key.wpcanai-get-optionrefuses the key's option.
wpcanai-stitch-list-projects
- Args:
{ "view"?: "owned"|"shared" }. Omitted → this site's projects (the ones its owner chose).owned/shared→ every project in the connected account, to choose from. - Returns:
{ "projects": [{ "id", "title", "updated", "device", "thumbnail", "added" }], "unavailable"?: string[] }—added: the project is on this site's list;unavailable(site list only): chosen ids the key cannot see.
wpcanai-stitch-set-projects
- Args:
{ "add"?: string[], "remove"?: string[] }— project ids. Neither → the ids chosen so far, without calling Stitch. - Adding checks every id against the account (owned or shared) first; if one is not found, nothing is added
(
stitch_project_not_found). Removing only edits the site's list: the project in Stitch and pages already deployed from it are untouched. Disconnecting the key keeps the list. - Returns:
{ "added": string[], "removed": string[], "projects": string[] }— the ids now chosen, in the order they were added. - The add-on does not stop
wpcanai-stitch-list-screens/-deploy-screenfor a project that is not on the list; put the project on the list first so the owner sees it in wp-admin.
wpcanai-stitch-list-screens
- Args:
{ "project_id": string, "include_other"?: bool (default false) }. - Returns:
{ "project_id", "screens": [{ "id", "title", "device", "width", "height", "thumbnail", "design": bool, "deployed_page_id", "deployed_at", "page_status", "edit_url" }], "hidden": int }. Items that are not designs are left out and counted inhidden; a screen with a deployed page is never left out.
wpcanai-stitch-deploy-screen
- Args:
{ "project_id": string, "screen_id": string, "status"?: "draft"|"publish"|"pending"|"private", "rehost_images"?: bool (default true), "prepare_site"?: bool (default true), "overwrite"?: bool (default false) }. - Returns:
{ "page_id", "layout_id", "created": bool, "status", "url", "edit_url", "images": { "rehosted", "reused", "remote", "failed" }, "site_changes": string[], "warnings": string[], "operation_id", "purged" }.
Errors (nothing is written)
| Code | Meaning / what to do |
|---|---|
stitch_not_connected |
Connect first (Requirements). |
stitch_invalid_key |
Not shaped like a Stitch key — have the owner copy it again. |
stitch_unauthorized |
Stitch rejected the key (deleted or revoked) — the owner makes a new one and connects it. |
stitch_not_found |
Wrong id, or a project of another account (try view: "shared"). |
stitch_project_not_found |
wpcanai-stitch-set-projects add: an id is not in the connected account (owned or shared); nothing was added. List the account with view and use an id from it. |
stitch_rate_limited |
Wait a minute, then retry. |
stitch_unavailable / stitch_bad_response |
Stitch is unreachable or answered oddly — retry later. |
stitch_link_expired |
The download link expired twice in a row — retry. |
stitch_html_too_large |
The screen's HTML is over 5 MB. |
stitch_parse_failed |
The screen's HTML could not be read; tell the owner which screen. |
stitch_page_edited |
See Re-deploy. |
forbidden |
The caller lacks unfiltered_html (and the request has no CanAI API key). |
invalid_input |
A missing or malformed id, or an unknown status. |