BuildOnce MCP Server
BuildOnce provides a production-grade Model Context Protocol (MCP) server allowing AI agents like Claude to create, edit, organize, and render branded marketing templates directly from conversational chat interfaces.
Streamable HTTP
Modern POST streaming protocol
OAuth 2.1 + PKCE
Dynamic client registration (RFC 7591)
31 MCP Tools
Templates, brand kit, render, assets, social
https://buildonce.app/v1/mcp
Connecting to Claude
BuildOnce integrates seamlessly with Claude via Claude.ai Custom Connectors or Claude Desktop.
Option A: Claude.ai Custom Connector
Connect BuildOnce directly inside Claude.ai web or mobile app (requires a Pro, Team, or Enterprise account):
- Name: BuildOnce
- Server URL:
https://buildonce.app/v1/mcp
Claude Desktop uses the same Custom Connector flow as Claude.ai — open Settings → Connectors → Add custom connector inside the app and follow the same steps as above. BuildOnce's OAuth flow requires the connector UI; there is no supported hand-edited claude_desktop_config.json entry for it.
Try It Out in Claude
Once connected, ask Claude any of these prompts to test the integration:
"List all the templates in my BuildOnce workspace and show their tags and dimensions."
"Fetch my brand kit colors and fonts, and create a new 1080x1080 template for a Summer Sale."
"Search for minimalist coffee shop photos on Unsplash to use in my new banner design."
"Render my latest template to a PNG and give me the download URL."
Connecting Cursor & Other MCP Clients
Any MCP client supporting the 2024-11-05 / 2025-03-26 specification over HTTP can connect by specifying the server URL:
{
"mcpServers": {
"buildonce": {
"url": "https://buildonce.app/v1/mcp"
}
}
}OAuth 2.1 Specification Details
BuildOnce adheres strictly to RFC 8414 (Authorization Server Metadata), RFC 9728 (OAuth 2.0 Protected Resource Metadata), and RFC 7591 (Dynamic Client Registration).
Authorization Server Metadata
https://buildonce.app/.well-known/oauth-authorization-server
Protected Resource Metadata
https://buildonce.app/.well-known/oauth-protected-resource
For full implementation details, visit the MCP OAuth API Reference.
All 31 Available Tools
Every tool is a first-class wrapper around BuildOnce services. Each tool contains declared annotations (readOnlyHint, destructiveHint, openWorldHint) compliant with Claude connector directory requirements.
Template Management
list_templatesList templates in the workspace with tags, description, dimensions, and per-page summaries.
| Param | Type | Notes |
|---|---|---|
| search | string | Optional free-text search filter. |
| tags | string[] | Optional tag filter. |
| folderId | string | Filter by folder ID, or "root" for unfiled templates. |
| sort | string | Sort order for the results. |
| page | number | Page number (defaults to 1). |
| limit | number | Items per page (1–100). |
get_templateFetch a single template's metadata by its ID.
| Param | Type | Notes |
|---|---|---|
| id* | string | Template public ID. |
create_templatescope: template:writeCreate a new template with one blank page of the given dimensions.
| Param | Type | Notes |
|---|---|---|
| name* | string | Template name (1–120 chars). |
| description | string | Optional description (up to 500 chars). |
| tags | string[] | Up to 20 search tags. |
| width | number | Page width in pixels (1–20,000). |
| height | number | Page height in pixels (1–20,000). |
| folderId | string | Optional folder ID to file directly into. |
| pages | object[] | Start with your own pages instead of one blank page (up to 100). |
update_templatescope: template:writeRename, re-describe, or update tags on an existing template.
| Param | Type | Notes |
|---|---|---|
| id* | string | Template public ID. |
| name | string | New template name. |
| description | string | New description. |
| tags | string[] | New tags array. |
duplicate_templatescope: template:writeMake a complete copy of a template and its current draft canvas.
| Param | Type | Notes |
|---|---|---|
| id* | string | Source template ID. |
| name | string | Optional name for the duplicate copy. |
list_template_foldersList all organization folders in the workspace, with template counts.
move_templatescope: template:writeMove a template into a folder, or back to root (unfiled).
| Param | Type | Notes |
|---|---|---|
| id* | string | Template ID. |
| folderId | string | null | Target folder ID, or null for root. |
Canvas Drafts, Versions & Variables
get_template_draftFetch a template's full editable canvas design document (Fabric.js pages, objects, layers).
| Param | Type | Notes |
|---|---|---|
| id* | string | Template ID. |
| format | "compact" | "full" | "outline" | "compact" (default) omits default-valued properties; "outline" returns a lightweight per-layer summary for finding what to patch. |
| pageIds | string[] | Read only these pages (up to 100). The result is marked partial. |
| layerIds | string[] | Read only these layers (up to 500). The result is marked partial. |
| fields | string[] | Read only these layer properties (up to 50). id and type are always included. |
save_template_draftscope: template:writeReplace a template's design document and automatically snapshot version history.
| Param | Type | Notes |
|---|---|---|
| id* | string | Template ID. |
| design* | object | Full design JSON document ({ version, designName, pages }). |
| ifRevision* | number | The revision this save started from — fails with DRAFT_REVISION_CONFLICT if someone else saved since. |
patch_template_draftscope: template:writeEdit part of a template's design without resending the whole document: change properties, add/remove/reorder layers, add/remove/resize pages.
| Param | Type | Notes |
|---|---|---|
| id* | string | Template ID. |
| ifRevision* | number | The revision this patch started from — fails with DRAFT_REVISION_CONFLICT if someone else saved since. |
| ops* | object[] | Patch operations (set, remove, add, move, set_page, add_page, remove_page), applied in order, up to 200. set on a layer can carry animation fields, and set_page a page's duration and transition. |
| dryRun | boolean | Validate and report the result without saving. |
list_template_versionsList the 10 most recent version snapshots of a template.
| Param | Type | Notes |
|---|---|---|
| id* | string | Template ID. |
get_template_versionFetch an earlier version's design document to inspect before restoring.
| Param | Type | Notes |
|---|---|---|
| id* | string | Template ID. |
| versionId* | string | Version ID. |
restore_template_versionscope: template:writeRoll back a template to an earlier snapshot (creates an undo snapshot of the current state).
| Param | Type | Notes |
|---|---|---|
| id* | string | Template ID. |
| versionId* | string | Version ID to restore. |
| ifRevision | number | Fail with DRAFT_REVISION_CONFLICT instead of replacing edits made since this revision. |
list_template_parametersList dynamic variable fields (text placeholders, image overrides) derived from canvas layers.
| Param | Type | Notes |
|---|---|---|
| id* | string | Template ID. |
Rendering & Output
render_templatescope: render:writeRender a template draft to PNG, JPEG or PDF, or the whole design as an animated MP4, WebM or GIF video, optionally applying variable modifications. A video costs 1 credit per started second (up to 60 s).
| Param | Type | Notes |
|---|---|---|
| id* | string | Template ID. |
| format | "png" | "jpeg" | "pdf" | "mp4" | "webm" | "gif" | Output file format (defaults to png). A video format renders every page as one video; if it is still rendering after about 20 seconds, the result is { renderId, status } — poll get_render. |
| modifications | Record<string, string> | Key-value variable overrides. |
| pageId | string | Specific page ID to render. |
| allPages | boolean | Render every page in the template. |
| preview | boolean | Free watermarked preview with per-layer layout info — check your work before a real render. |
| pageIds | string[] | Preview only — restrict to these pages (up to 12). |
| inline | "none" | "thumbnail" | "full" | How many page thumbnails to embed in the result (defaults to "thumbnail", the first 3 pages). |
| video | { fps?, scale?, quality? } | Video only. fps 1–60 (GIF up to 15), scale 0.5 | 1 | 2, quality "small" | "standard" | "high". |
| at | number[] | Preview only — up to 12 moments, in seconds: a free sheet of the video's frames at those times, with the video's duration and price. |
| animation | { pages?, layers? } | Change timing and animation for this render or preview only — different cuts of one template. Same shape as the render API's animation field. |
get_renderscope: render:writeA render by renderId: status (queued, processing, succeeded or failed), its files and credits. Use it to wait for a video render_template returned as queued.
| Param | Type | Notes |
|---|---|---|
| renderId* | string | The renderId from render_template. |
Brand Kit & Stock Imagery
get_design_guideThe design guides. By default, the canvas guide: how a page.json is built — layer fields, text, images, shapes, variable bindings and limits. With topic: "animation", how to animate layers and pages for video: enter and exit presets, ambient motion, text reveals, page timing and transitions.
get_brand_kitFetch the workspace brand kit: logos (primary and dark-mode), brand fonts, color palette, and tone guidelines.
upload_assetscope: asset:writeAdd a file to the workspace's media library and get back its id and a hosted URL. Used to fill the brand kit or add an image to a design from a brand brief.
| Param | Type | Notes |
|---|---|---|
| kind* | "image" | "font" | "icon" | What kind of file is being uploaded. |
| url | string | A public http(s) URL for the server to download — pass this or base64, not both. |
| base64 | string | The file's bytes as base64 (bare or a data: URL) — pass this or url, not both. |
| filename | string | Name for the stored file; for a font, also sets its font family. |
update_brand_kitscope: asset:writeSave the workspace's brand: colors, logos, fonts and brand guidelines. Only the fields passed change. Requires the workspace admin role.
| Param | Type | Notes |
|---|---|---|
| colors | object[] | Replaces the whole palette: { name, hex, role } entries. |
| primaryLogoAssetId | string | null | Asset ID from upload_asset; null clears the logo. |
| reversedLogoAssetId | string | null | Logo for dark backgrounds; null clears it. |
| fontAssetIds | string[] | Replaces the whole list of brand font asset IDs. |
| styleGuide | string | Freeform brand voice/guidelines text (up to 20,000 chars); "" clears it. |
search_stock_photosSearch royalty-free stock photography from Pexels and Unsplash to inject into designs.
| Param | Type | Notes |
|---|---|---|
| query | string | Search terms (e.g., "minimalist workspace"). |
| provider | "pexels" | "unsplash" | Filter by provider or omit for all. |
| page | number | Page number (defaults to 1). |
| perPage | number | Results per page (1–80). |
Social Publishing
Render a design and post it in the same conversation. Accounts are connected on the dashboard's Social page; these tools post to the accounts already connected to the workspace. A post goes to a real account, so the agent confirms the account, text and media with you first.
get_platform_requirementsscope: social:readEach platform's rules — character limit, required media, image or video, formats, which fields apply, platformOptions and delete support — from the same table posts are checked against.
| Param | Type | Notes |
|---|---|---|
| platform | string | One platform, or omit for all. |
list_social_accountsscope: social:readList the social accounts connected to the workspace, with each one's platform, name and whether it needs reconnecting.
list_social_postsscope: social:readList the workspace's posts, newest first, in every status — check whether a scheduled post went out or why one failed.
| Param | Type | Notes |
|---|---|---|
| status / platform / accountId | string | Filters: status (scheduled, publishing, published, failed, canceled, deleted), platform, or one account. |
| from / to | string | ISO 8601 dates bounding when the post was created. |
| page | number | Page number (defaults to 1). |
| limit | number | Posts per page (1–100). |
schedule_social_postscope: social:writeSchedule a post on one connected account; it publishes by itself at the scheduled time.
| Param | Type | Notes |
|---|---|---|
| accountId* | string | Account ID from list_social_accounts. |
| content* | string | Post text (up to 3,000 characters). |
| mediaUrls | string[] | Up to 9 public image/video URLs — a render_template URL works as is. |
| linkUrl | string | Link to attach. |
| title / description / tags | string / string / string[] | LinkedIn and YouTube only. |
| accountIds | string[] | Post to several accounts at once instead of accountId — one result per account. |
| platformOptions | object | Per platform: instagram.postType (feed, story, reel), youtube.privacy / categoryId, pinterest.boardId, x.replyToPostId. |
| idempotencyKey | string | Sending the same key again returns the original post instead of posting twice. |
| dryRun | boolean | Check the post against the platform's rules and return valid, errors and warnings — nothing is created or posted. |
| scheduledAt* | string | ISO 8601 with a timezone, e.g. "2026-10-08T09:00:00+05:00". Must be in the future. |
publish_social_postscope: social:writePublish a post on one connected account right now. Returns published, or failed with the reason.
| Param | Type | Notes |
|---|---|---|
| accountId* | string | Account ID from list_social_accounts. |
| content* | string | Post text (up to 3,000 characters). |
| mediaUrls | string[] | Up to 9 public image/video URLs — a render_template URL works as is. |
| linkUrl | string | Link to attach. |
| title / description / tags | string / string / string[] | LinkedIn and YouTube only. |
| accountIds | string[] | Post to several accounts at once instead of accountId — one result per account. |
| platformOptions | object | Per platform: instagram.postType (feed, story, reel), youtube.privacy / categoryId, pinterest.boardId, x.replyToPostId. |
| idempotencyKey | string | Sending the same key again returns the original post instead of posting twice. |
| dryRun | boolean | Check the post against the platform's rules and return valid, errors and warnings — nothing is created or posted. |
get_social_postscope: social:readOne post by ID, in full — status, lastError, externalPostId, publishedAt. Poll it after publishing a video.
| Param | Type | Notes |
|---|---|---|
| postId* | string | Post ID. |
update_scheduled_postscope: social:writeChange a scheduled post's text, media, link or time, keeping the same ID. Checked like a new post.
| Param | Type | Notes |
|---|---|---|
| postId* | string | A post that is still scheduled. |
| content / mediaUrls / linkUrl / scheduledAt … | optional | Only the fields that change. |
retry_social_postscope: social:writePublish a failed post again as the same post. Refused with RETRY_UNSAFE when the first attempt may already be live (it timed out or was interrupted).
| Param | Type | Notes |
|---|---|---|
| postId* | string | A post with status failed. |
delete_social_postscope: social:writeRemove a published post from its platform. Works on X, Facebook, LinkedIn, Pinterest, Discord and Bluesky; Instagram, TikTok, Threads and YouTube return unsupported_by_platform with the reason.
| Param | Type | Notes |
|---|---|---|
| postId* | string | A published post. |
| confirm* | true | Required — deleting is permanent. |
cancel_social_postscope: social:writeCancel a scheduled post before it goes out. A post already sent can't be canceled.
| Param | Type | Notes |
|---|---|---|
| postId* | string | Post ID from schedule_social_post or list_social_posts. |
Was this page helpful?