MCP Tools
Handoff exposes a Model Context Protocol server at https://demo.handoff.com/api/mcp. Connect Cursor, Claude, or Windsurf to read design tokens, search components, look up icons, and generate components from design artifacts.
Setup — Cursor & Claude
Connect AI assistants to this Handoff deployment for reference materials, components, sync, design library, and design-to-component generation. MCP uses the same OAuth token as the CLI — run handoff-app login first (step 1).
MCP endpoint on this deployment: https://demo.handoff.com/api/mcp
Get your access token
After handoff-app login, run handoff-app mcp-token to print your token, and paste it in place of PASTE_ACCESS_TOKEN_FROM_CLI_AUTH below. (The token also lives in .handoff/cli-auth.json — keep it out of git.)
handoff-app mcp-token
- Create or edit
.cursor/mcp.jsonin your design repo (project-only), or~/.cursor/mcp.jsonfor all workspaces. - Paste the config below, replace the token, then restart Cursor or open Cursor Settings → MCP and enable the handoff server.
- In chat, ask the agent to call
handoff_get_project_contextfirst, then use reference, sync, or design tools. Pair with Figma MCP for design-to-code.
{
"mcpServers": {
"handoff": {
"url": "https://demo.handoff.com/api/mcp",
"headers": {
"Authorization": "Bearer PASTE_ACCESS_TOKEN_FROM_CLI_AUTH"
}
}
}
}Common MCP tools
handoff_get_project_context— stack profile, paths, workspace summary (call first)handoff_get_stack_guide— Handlebars/React authoring ruleshandoff_get_reference— catalog, tokens, icons, property-patterns (maintained in Admin → Reference; regenerate after catalog changes)handoff_get_design_guidelines/handoff_get_brand_voice— team settings from Design → Settingshandoff_get_component_reference— buttons / inputs / iconography reference imageshandoff_sync_pull/handoff_sync_push— team sync (patches applied in your repo)handoff_start_component_from_design— design library → component job
Full tool list and scopes: see docs/HANDOFF-MCP-RFC.md in the handoff-app package.
Context
handoff_get_brand_voiceFormatted brand voice / copy guidelines from design workspace.
handoff_get_design_guidelinesTeam Design.MD guidelines from design workspace settings.
handoff_get_project_contextProject hydration: stack profile, paths, Figma key, translation hints.
handoff_get_stack_guideMarkdown authoring rules for the active stack (bootstrap-handlebars, react-tailwind, react-scss).
handoff_update_brand_voiceUpdate the team brand voice in design workspace settings (admin + sync:write). Accepts any subset of fields and MERGES over the stored value — omitted fields are left alone, and a field set to "" is cleared. This is the standing copy guidance every future generation inherits (design images, page copy, component previews, DESIGN.md exports), not a per-request override — it changes output for everyone until it is changed again. The response echoes a per-field before/after so the overwritten text is recoverable.
handoff_update_design_guidelinesReplace the team Design.MD guidelines in design workspace settings (admin + sync:write). This REPLACES the whole document — there is no merge, so send the complete text (read the current value with handoff_get_design_guidelines first). These are the standing design instructions every future generation inherits, not a per-request override. The response returns the previous content alongside the new one so the overwritten version is recoverable.
Components
handoff_browse_componentsOpen an INTERACTIVE inline gallery of the design system's components — a searchable card grid. The user can Select a component to hand it back to you, or Open it in Handoff. Use when the user wants to browse or pick a component; use handoff_search_components for raw catalog data.
handoff_get_componentComponent implementation data by id — code/html/sass/css, properties, variants, and usage guidance. Slimmed for context use: excludes the compiled sharedStyles CSS (~97% of the raw row), validationResults, and Figma sync metadata. Each preview may carry a `purpose` — structural (placeholder copy showing the form) / example (real content) / builder (seed for a new block) / edge (stresses the contract) — which is what tells you which one to read or copy; previews with no `purpose` are unclassified, not "generic".
handoff_get_component_referenceComponent style reference image for a slot: buttons | inputs | iconography.
handoff_get_referenceFetch generated reference material by id: catalog | tokens | icons | property-patterns. (May also be passed as "type".)
handoff_search_componentsSearch the component catalog. Matches every word of the query against a component's id, title, group, tags and description, so word order does not matter. Returns a `use` line saying what each block is for — read it before choosing, because several blocks hold copy and they are not interchangeable.
Tokens
handoff_browse_tokensOpen an INTERACTIVE inline palette of the design system's foundation tokens (color swatches, type specimens, spacing scale). The user can click a token to hand it back to you. Use when the user wants to SEE and pick tokens; use handoff_get_tokens for raw token data.
handoff_export_design_mdExport a compact DESIGN.md framing brief for this design system — system identity, token brief (colors/type/spacing/radius/grid), component vocabulary, brand voice, and design guidelines. Commit it to a project and reference it from CLAUDE.md so an agent has design-system context without a live MCP call. For a multi-axis system, pass brand/scheme to frame the brief around that resolved theme.
handoff_get_tokensFoundation design tokens (colors, typography, effects, and any spacing/radius/grid when extracted). Slimmed for context use — excludes icon/logo SVGs, per-component token usage, and the SCSS $map. Use handoff_get_icon_catalog/handoff_get_logo_set/handoff_get_component for those. Multi-axis (brand × scheme): the response advertises available `axes`; pass brand/scheme to get axis-resolved tokens under `axisTokens`.
Icons & Logos
handoff_get_icon_catalogReturn the full icon catalog as defined in the design system. Optionally filter by category. Each entry includes id, name, description, category, tags, usage guidance, and source (SVG content or iconify/fa-pro reference).
handoff_get_logo_setReturn all logo variants for the design system, including SVG content, usage guidance, and variant metadata (light/dark/color/mono, primary/alternate/wordmark/icon-only). Optionally filter by variant or form.
handoff_search_iconsSearch the icon catalog by name, tag, or description substring. Returns matching IconCatalogEntry objects including SVG content where available.
Assets
handoff_generate_imageGenerate an image for a block slot and put it in the asset library — THIS is how you get a picture. Never draw one yourself (an HTML canvas, an inline SVG, a data URL) and never invent a path: an image slot's `src` must be a URL the registry serves. Returns `{ url, assetId, width, height }`; put `url` straight into the slot's `src` with handoff_compose_page / handoff_create_page / handoff_update_page. Pass the slot's declared `width`/`height` so the aspect ratio matches the block (a 16:9 hero does not want a square photo). Generated lettering renders as gibberish, so the prompt is sent with a no-text rule — do not ask for words in the image. This RUNS the generation (25s–4min) and returns when the image is stored; it is not a queue handle. Requires server AI to be configured. If the user already gave you an image, use handoff_upload_asset instead — do not regenerate what you were handed.
handoff_get_assetGet full details for a single asset including component usages and size info.
handoff_list_asset_collectionsList all asset collections (Figma sections or manually created groups).
handoff_search_assetsSearch the asset library — logos, icons and images. Matches every word of the query across title, alt text, description and tags; if nothing matches all of them, falls back to any of them and says so. Returns a summary per asset: id, title, type, storageUrl, alt text, description, tags and native dimensions. Use `handoff_get_asset` for full detail including SVG content, file size and component usages.
handoff_upload_assetAdd an image (or video) to the asset library from an https URL or inline base64 bytes, and get back the URL to reference it by. Pass exactly one of `url` (fetched server-side; https only, public hosts only) or `data_base64` (+ `filename`). The type is taken from the bytes, so `mime_type` is only needed when they are ambiguous. Returns the same summary shape `handoff_search_assets` gives, plus `rawUrl`. Put `storageUrl` into an image slot's `src` with handoff_create_page / handoff_update_page — do NOT invent image paths. Re-uploading identical bytes returns the existing asset with `deduplicated: true` (ids are content-addressed) rather than making a copy.
Design Artifacts
handoff_create_design_artifactCreate design artifact with base64 image (design:write).
handoff_generate_component_from_designFetch a design artifact's spec and extracted assets to generate a component locally. If no spec exists yet, queues server-side spec generation. Returns the full spec, markdown, image URLs, and stack guide context for you to implement the component in the local codebase.
handoff_get_component_specGet the component specification (structured spec + editable markdown) for a saved DESIGN ARTIFACT — takes an `artifactId`, not a component id. Returns the full ComponentSpec JSON and the rendered markdown for local component generation. **For an existing component's contract (properties, previews, should_do/should_not_do), use handoff_get_component instead.**
handoff_get_design_artifactGet design artifact by id.
handoff_list_design_artifactsList saved design library artifacts. Registry mode only.
Design Workbench
handoff_extract_design_assetsDEPRECATED — use handoff_transition_to_dev, which this now forwards to. Runs the dev handoff for a design artifact.
handoff_generate_design_imageQueue an AI design image generation (async). A durable background runner processes the job within ~1 min; poll handoff_get_design_job for the result. Requires server AI to be configured.
handoff_get_design_jobPoll a design image generation job (from handoff_generate_design_image). Read-only.
handoff_set_design_statusSet a design artifact's lifecycle status (draft → review → approved). Moving to review or approved kicks off server-side asset extraction + spec generation (when server AI is configured), so the artifact's spec/assets are ready for handoff_generate_component_from_design.
handoff_transition_to_devTransition a design artifact to developer-ready: extracts its assets (backgrounds, states, icons) and generates the full specification — props, behavior, accessibility, text inventory, design-token mapping against the registry's real tokens, and a brand-voice check of the copy. One operation; poll handoff_get_design_artifact and read `devHandoff` for stage-level progress (extracting_assets → generating_spec → ready). Read the result with handoff_get_component_spec.
Pages & Compositions
handoff_compose_pageCompose a WHOLE playground page in ONE call — prefer this over calling handoff_scaffold_args per block and then handoff_create_page. For each block it runs the same scaffold handoff_scaffold_args runs (seeding correctly-shaped `args` from a real preview), merges your `args` over that seed per field, validates every block against its contract, and then writes the page once. Nothing is written unless every block passes: an unknown component id, a missing id or an out-of-contract value refuses the whole call and names the block. **Migrating an imported page? Pass `importId`** and an out-of-contract value stops being a refusal: the page is written as a DRAFT with every value exactly as the source page had it, and the broken rules come back in `violations` ({sectionIndex, blockId, field, rule, limit, actual, excerpt}) and are saved with the page. Report them and let a person decide the words — NEVER shorten or rewrite somebody's copy to fit a limit, and never re-send a value you edited as if it were theirs. Publishing that page to a CMS target refuses until the list is empty. An unknown component id still refuses, import or not. **Pass `pageType` and the page is planned against this registry's fixture for that type** — the same record handoff_get_page_fixture returns: section order, the props a good page of this type carried, and how many items its repeaters held. Your `blocks` list is never reordered; omit `blocks` entirely and the fixture's own sections become the plan. `plan.note` says what the fixture did, including when there was none. **Pass `sourceCopy` when the user supplied copy.** A string is a copy deck: nothing is placed from it, but every value is checked against it so the report can say which slots hold the user's words. The structured form (one object per section, aligned to `blocks`) is PLACED by role — verbatim inside each field's declared limit — and what code could not place comes back as `unplaced` for you. Never paraphrase supplied copy and never invent copy to fill a gap. Returns `pageId`, `editUrl`, a per-block `report` of {seeded, basePreview, warnings, unknownKeys, emptySlots} and a per-section `coverage` / `coverageReport`: what came from the supplied copy, from the fixture, from the block's own example, what you wrote, and which REQUIRED slots are still empty. **A required slot with no value is a question for the user, not a gap to fill** — ask what belongs there rather than writing something plausible. Pass `dryRun: true` to get the whole report with nothing saved. The page id is derived from `title` unless you pass `id`. If the brief came with images, upload them with handoff_upload_asset first and pass the returned URLs in `args` — or generate one with handoff_generate_image. Do NOT invent image paths.
handoff_create_pageCompose a NEW playground page (landing page) from component blocks and save it (source: playground). **For a whole page, use handoff_compose_page instead** — it scaffolds every block, validates them and writes the page in ONE call. Use this one when you already hold finished args for every block, or when you need to choose the page id yourself. If the brief came with images, upload them with handoff_upload_asset first and pass the returned URLs in `args` (or generate one with handoff_generate_image). Each block is {id (component id), preview? (existing preview key), args? (prop values)}. To compose blocks that render WELL (not just validly), call handoff_scaffold_args for each component first — it returns correctly-shaped `args` (seeded from a real preview) + per-field shapes; tweak and pass them here. Or reference an existing preview by key via `preview` and override only what changes in `args`. Every block is validated against its contract (unknown ids / out-of-contract args are rejected). Richtext fields are stored as Portable Text; HTML in and out by default, so pass the HTML you would write and read it back the same way. Any VISUAL slot you leave unset renders blank — the returned `report[].emptySlots` flags those. The returned `editUrl` opens the saved page in the playground, rendering your exact args. When the page has a type ("a product page", "a customer story"), call handoff_get_page_fixture for that type FIRST and plan section by section against what it returns — it is a real page this team marked as good, and it carries the section order, the block choices and the theme rhythm a four-block guess misses.
handoff_delete_pageArchive a playground page. **This is not a hard delete** — the record and its history stay, the page is taken out of every listing, and any build briefs made from it (plus the pages built from those) are archived with it. There is no un-archive yet, so treat it as final from a caller's point of view. Added because a page composed by mistake previously had no way to be cleaned up at all.
handoff_get_pageGet one playground page (pattern): its ordered block composition ([{id, preview?, args}]) + metadata. Read this before editing/swapping blocks, then pass the modified blocks to handoff_update_page. Richtext fields are STORED as Portable Text but returned as HTML by default — HTML in and out, so blocks you read here can be edited and passed straight back. Ask for `richTextFormat: "portableText"` for the canonical stored shape, or "markdown" to read the copy without the markup.
handoff_get_page_fixtureGet the fixture for a page type: its ordered sections (what each is for + which blocks satisfy it), the props observed on the real page (theme per section, the light/dark rhythm, image treatment, CTA style) and any per-field intent notes. **Call this before composing a page of a named type and plan section by section against it.** If there is no exact type, the nearest by name is returned with `matched: false` — follow it knowingly, it describes a neighbouring page type. If nothing is close, `fixture` is null and the instruction is to compose from the catalog; there is no default shape.
handoff_import_legacy_exemplarsSeed this registry with the platform's three observed exemplar page shapes — product, solution and customer story — as fixtures with no template behind them (admin + sync:write). **For a registry with no templates to mark yet.** They were read off one company's live site, so they arrive with a verdict saying to review and replace them with a marked template. Idempotent: a page type that already exists is skipped and named, never overwritten.
handoff_list_page_fixturesList the page types this registry has a fixture for — a fixture being a real template someone marked as a good page of that type. Returns the type key, its display name, the first line of the verdict and the template it came from. Read-only. Call this to find out what page types this team has house shapes for before composing; then handoff_get_page_fixture for the one you need. An empty list is a real answer: this registry has marked nothing, and there is no default set to fall back on.
handoff_list_pagesList playground pages — saved compositions of component blocks ("patterns"). Use to see or reuse existing landing pages before composing a new one. Optionally filter by group.
handoff_mark_template_as_fixtureMark a TEMPLATE as a good page of a named type, and store the fixture derived from it (owner or admin + sync:write). Structure and observed props are READ OFF the template — they are not yours to write — so what you supply is the page type, a one-paragraph verdict on why this page is good, and any per-field intent notes. Re-marking the same type re-derives from the template as it is now. Refuses a page that is not a template: "others may build from this" is a separate decision.
handoff_scaffold_argsGet a ready-to-fill `args` template for ONE component so you dispatch blocks/previews that render WELL instead of guessing prop shapes. **Composing a whole page? Call handoff_compose_page once instead** — it runs this scaffold for every block and writes the page in a single call. Use this tool for a single block, a preview, or to inspect a component's field shapes. If the brief came with images, upload them with handoff_upload_asset first and pass the returned URLs in `args` (or generate one with handoff_generate_image). Seeds `args` from a real preview (correctly-shaped slots, images, arrays) when one exists, and annotates every field with its editorType + expected shape (richtext = HTML string, image = { src, alt, … }, etc). Fill/tweak the returned `args`, then pass it to handoff_create_page (as a block's args) or handoff_create_preview. Call this BEFORE authoring to avoid empty slots / wrong-shaped values. Image slots never come back pointing at a build path the registry cannot serve: an unservable src is replaced with an image from the asset library (reported in `seededImages`) or left empty and named in the note — swap in the right picture, or add one with handoff_upload_asset. If you are building a page of a named type, call handoff_get_page_fixture for that type FIRST and plan section by section against it — the fixture says which blocks a good page of that type uses, so it tells you which components to scaffold before you scaffold them.
handoff_update_pageUpdate a playground page: metadata and/or its full block composition. To add/remove/swap/reorder blocks (e.g. swap in a new hero), read the page with handoff_get_page, modify the blocks array, and pass the FULL new array here. Blocks are validated against contracts before saving. Richtext fields take HTML (or the Portable Text a "portableText" read returned) and are stored as Portable Text.
Previews
handoff_create_previewAuthor a NEW registry preview for a component — a named, semantic value-set (e.g. a "Primary CTA" button). Call handoff_scaffold_args first for correctly-shaped `values`. Values are validated against the component contract; invalid values are rejected, not saved. This is how Claude publishes a configured, meaningful example to the workbench. Richtext fields are stored as Portable Text; HTML in and out by default — pass the HTML you would write. Returns `verifyUrl` — the component page with `?preview=<key>`, which opens on this value-set. **It resolves the preview through a live API call, so on a statically exported registry it will not appear until `handoff-app build` runs.** Also returns a `report` (empty visual slots + each field's editorType) to self-check the values. Set `purpose` so the preview is grouped and picked correctly — an unpurposed preview lands in the component page's trailing "Other" group. Once a value-set is approved, mark it canonical with handoff_promote_preview.
handoff_preview_componentRender an INTERACTIVE, inline preview of a component (embedded app) — the real rendered component with responsive width controls. Pass a component id and optionally a preview key. Use when the user wants to SEE a component, not just read its data (handoff_get_component). With no key it picks by `purpose` — the component's `builder` seed, else its `example`.
handoff_promote_previewApprove a registry preview: mark its value-set canonical (semantic="canonical") so it reads as the blessed, reference example for its component. Use after review to promote a value-set authored via handoff_create_preview / handoff_update_preview. Lifecycle only — it does not touch `purpose`, which says what the preview is FOR. Zero contract change — no rebuild, no migration.
handoff_update_previewUpdate an existing registry preview (by its id). Provide any of title / values / semantic / purpose / rationale. Changed values are re-validated against the component contract; richtext fields are stored as Portable Text, and HTML is accepted (and returned) as it always was. `purpose` is authoring classification (what the value-set is FOR); to approve/canonicalize a value-set instead, use handoff_promote_preview, which sets `semantic` and leaves `purpose` alone.
Documentation
handoff_create_doc_pageCreate a NEW markdown doc page. Fails if the slug already exists (use handoff_update_doc_page). The page appears in the sidebar nav automatically.
handoff_get_doc_pageGet one markdown doc page by slug — its frontmatter + markdown body.
handoff_list_doc_pagesList markdown documentation pages (slug, title, description). These are doc/content pages — distinct from playground pages (block compositions; use handoff_list_pages for those).
handoff_update_doc_pageUpdate an existing markdown doc page (by slug). Fails if it does not exist. Provide markdown and/or title/frontmatter; frontmatter is merged with the existing.
Change Inquiry
handoff_change_whyThe reason a specific change was made: returns the human-authored push message if present, otherwise generates and caches a one-sentence AI summary from the diff. Pass the change type and id as returned by handoff_recent_changes / handoff_component_history.
handoff_component_historyVersion history for one component (newest first): version number, when, who, the "why" when recorded, and the CATEGORIES that moved — contract | visual | content | asset | config. Contract versions also carry the property-level delta (added / removed / retyped / newly required), so this answers "can I still pass <prop>?" as well as "what changed in <component>" and "how has it evolved".
handoff_recent_changesRecent changes across the design system, grouped by PUSH — one entry per push, newest first, not one per row. Each push gives who pushed it, when, the "why" (the push message, or a drafted summary), the commit, and the entities it touched with the categories that moved: contract (properties/slots), visual (artifacts/theme), content (previews/docs/metadata), asset (images), config (source/handoff config). Use to answer "what changed recently/lately/since <when>". Filter with contractOnly to answer "did anything break?". For the reason behind one specific row, follow up with handoff_change_why using its entity type + changeId.
Build & Sync
handoff_enqueue_buildDEPRECATED — server-side builds retired. Builds run locally via `handoff-app build`. Returns workspace-mode notice.
handoff_list_reference_materialsList reference material ids and sizes.
handoff_sync_pullFetch a bounded page of sync changes since cursor (JSON patches for local apply). Registry mode only. Results are paginated: if the response has `hasMore: true`, pull again with `since` set to `nextCursor` and repeat until `hasMore` is false to drain the full feed.
handoff_sync_pushUpload sync changes (requires sync:write). Registry mode only.
handoff_sync_statusRemote sync cursor and health. Returns workspace-mode notice if no registry is connected.
Other
handoff_component_contract_historyContract history for one component: only the versions whose properties or slots changed, each with the property delta (added / removed / retyped / newly required) and whether it was breaking. Use before upgrading a consumer, or to answer "can I still pass <prop>?" / "when did <prop> become required?" / "has this component ever broken its API?". Ignores the visual, content, asset and config versions that handoff_component_history also lists.
handoff_delete_doc_pageDelete a markdown doc page by slug. Removes it from the sidebar nav and records the deletion in the changelog. Fails if the slug does not exist.
handoff_get_importRead one imported page back, routed against the kind canon as it stands, **and get the input handoff_compose_page takes**. This is the hop between importing a page and composing it: `composeInput.blocks` is the block list in the page’s own band order and `composeInput.sourceCopy` is one entry per block, positionally aligned to it — pass both straight to handoff_compose_page with `dryRun: true` first, **together with `composeInput.importId`**. That id is what lets a value the target contract refuses (an over-long headline from the source page) be written as a draft with the violation recorded, instead of refusing the page outright; leave it out and an imported page with one long line composes to nothing at all. Where the section’s kind has a field map, its args are already filled by code rather than by you. **`composeInput.omitted` is the honest half.** Chrome and drop sections are not blocks and are left out; a `feed` (a data integration), a `gap` (content with no component yet) and an undecided kind are listed there with the reason. Report them to the user — never move their copy into another block and never write a section to replace one. A slot marked `truncated` is copy the import could not fully recover: say so, do not complete it. **Every image field in `blocks[].args` is a registry asset URL** — the import re-hosted it. An image the registry does not hold is left OUT of the args and named in `fieldMaps[].images` with the reason, so a field reading as empty (or as EMPTY REQUIRED when you compose) is the honest signal that one is missing: report it, and never paste the source site’s URL back in. Sections whose kind nobody has decided are decided once, per KIND, on the import review surface (/app/library/imports), and that decision replays across every page already imported.
handoff_import_pageImport ONE page from an external source and record it as sections (admin + sync:write). Takes a crawl geometry export, a page schema, or a CMS connector document, and writes the normalised record: per-section structural signature, structured source copy with provenance, observed props and the crop box. **Nothing here decides anything.** A section whose structure nobody has decided comes back `undecided`, which is the honest answer — a person decides once per KIND, choosing one of five routes (block, feed, chrome, drop, gap) on the import review surface, and that decision then replays across every page already imported. **Images are re-hosted into this registry**, so a page composed from the import points at registry asset URLs and never hot-links the source site: pass the export’s own downloaded files as `assets` (no network), or set `fetchImages: true` to let the registry fetch what it was not given. Each slot keeps its original `src` as provenance; an image that could not be re-hosted is reported in `images.failures` and withheld from composition rather than hotlinked. Idempotent: re-importing the same source reference refreshes the measurement in place, keeps every human judgement and keeps (never deletes) a section that has since disappeared. A crawl export with no page geometry is REFUSED before any write, naming the flag to re-crawl with — an import with no sections would look like a page with nothing on it.
handoff_list_review_queueList playground pages awaiting review — typically pages built by someone outside the team through a guest share link. Each entry carries who submitted it (a self-declared, UNVERIFIED name), the template it was built from, the share link that admitted them, and their note to the reviewer. Decide an entry with handoff_review_page.
handoff_move_doc_pageMove (rename) a markdown doc page from one slug to another, preserving its content. Fails if fromSlug does not exist or toSlug is already taken. Use this instead of delete+create so the page keeps its history and the nav tree updates atomically.
handoff_review_pageApprove a submitted playground page, or send it back to its author. "approve" marks it approved; "reject" returns it to draft, which also re-opens editing for the guest who built it, so a rejection with a message is how you ask for another pass. Visibility is never changed — promoting a page to a wider audience stays a separate, deliberate act. Maintainer only.