Setup guideAPI setup
0/4
  1. Choose API pathAPI is the route.
  2. Copy OpenAPI pathFirst machine contract is ready.
  3. Read API keysScopes and bearer auth are clear.
  4. Ready to runYou have enough context to call v1.
Get startedZebrafish MCP Server

Zebrafish MCP Server

The Zebrafish MCP server lets Claude Code, Codex, and other MCP-capable agents operate Zebrafish through typed tools instead of hand-written curl commands. It is for developers and operators who want an agent to list workflows, start runs, inspect status, manage projects/files, author draft workflow revisions, answer approvals, and use Studio/model tools while preserving API key scopes, request IDs, rate limits, audit events, and support links.

Raw

The MCP boundary requires a valid Zebrafish API key, then each tool enforces the scope listed below through the delegated /api/v1 route. A key with only campaigns:write, assets:write, or runs:manage can call its matching least-privilege tools without also granting workflows:read.

Quick Start#

  1. Install the Zebrafish skills in the agent workspace:

    BASH
    npx zebrafish-skills install
    npx zebrafish-skills check

    The install adds the router skill, production playbooks, verification script, and optional non-blocking hooks for supported coding-agent workspaces.

  2. Create a Zebrafish API key with the narrowest scope set for the job. For agent-assisted OAuth/sign-in, open: https://zebrafish.dev/agent-onboarding?preset=authoring-agent

    • Run only: runs:trigger

    • Read/debug runs: workflows:read or runs:trigger

    • Author drafts: workflows:read, workflows:write, runs:trigger

    • Publish: add workflows:publish

    • Project files: campaigns:read, campaigns:write, assets:read, assets:write

    • Studio: models:read, studio:read, studio:write

  3. If the agent will run paid workflows or Studio generations, continue from the key modal to Settings -> Spend and buy prepaid credits through Stripe Checkout.

  4. Add the server to Claude Code:

    BASH
    claude mcp add zebrafish https://zebrafish.dev/api/mcp --header "Authorization: Bearer $ADFISH_API_KEY"
  5. For Codex, add the same remote MCP server to ~/.codex/config.toml:

    TOML
    [mcp_servers.zebrafish]
    url = "https://zebrafish.dev/api/mcp"
    
    [mcp_servers.zebrafish.http_headers]
    Authorization = "Bearer adf_..."
  6. For another MCP client, configure a remote Streamable HTTP server at https://zebrafish.dev/api/mcp with the header Authorization: Bearer <your-key>.

  7. In the agent session, ask: Read the Zebrafish agent playbook, then list my workflows

    Expected output:

    TEXT
    Playbook loaded: discovery loop, safety rules, approval boundaries, and verification steps.
    Blog Post Illustrations (wf_blog_post_illustrations)
    Weekly Launch Recap (wf_weekly_launch_recap)
    Paid Social Variants (wf_paid_social_variants)
  8. Ask: Run my Blog Post Illustrations workflow

    Expected output:

    JSON
    {
      "id": "run_01JZ8A1M8QF7YH6K4R5S2T9V0B",
      "workflowId": "wf_blog_post_illustrations",
      "status": "queued"
    }

Main Agent Modes#

Agents interact with Zebrafish in four main modes:

  • Direct hosted MCP: Claude Code, Codex, or another external MCP client calls https://zebrafish.dev/api/mcp with a scoped API key. This is the best path for coding agents and operator agents outside the web app.

  • In-app /agent tools: Zebrafish's own agent exposes the same MCP-backed capabilities as prefixed tools such as zebrafish_list_workflows and zebrafish_upload_campaign_asset_from_url. Unsafe write/spend/cancel tools go through the app's approval UX. This is the in-app conversational path for workflow authoring: brief the agent, approve draft, test-run, and publish actions, then inspect the resulting runs.

  • Public REST /api/v1: CI systems and custom integrations call REST directly with Authorization and Idempotency-Key headers.

  • Human UI: Users inspect workflows and runs, manage projects, files, Studio, approvals, settings, and billing in the app. /workflows is the observability surface for workflow definitions, statuses, costs, approvals, outputs, and failures. MCP covers the agent-safe public operations, not every private UI action.

Product note: the human UI calls these workspaces Projects. The public API, MCP tools, scopes, and IDs still use campaign/campaigns names for compatibility until /projects aliases are introduced.

For reliable autonomous work, keep the same loop for every task:

  1. Prime: call get_agent_playbook for non-trivial tasks.

  2. Discover: list_workflows, list_campaigns, list_models, or the relevant list/get tool.

  3. Inspect: get_workflow, get_workflow_context, get_campaign_knowledge, list_campaign_assets, get_run_trace, or get_run_events.

  4. Plan: choose the narrowest write tool and ask the user/client for approval before spend, publish, cancel, approval responses, or asset/campaign writes.

  5. Act: call the write tool. MCP clients may omit idempotencyKey; Zebrafish derives one for the tool call. Provide a stable key only when intentionally retrying the same operation. Exception: purchase_credits requires an explicit idempotencyKey (it spends real money). Payment tools are advertised only when AGENT_PAYMENTS_ENABLED is on; rail availability still depends on the fake webhook secret or Stripe Checkout unit price + webhook configuration described in the API key docs.

  6. Verify: poll get_run, get_run_events, or get_studio_generation until a terminal state, then use debug/cost/asset URL tools as needed.

Coverage Boundaries#

MCP exposes every major agent-safe product capability: hosted skill discovery, workflow run lifecycle, draft authoring, project reads/writes, workflow-project attachment, file upload/readback, run cancellation, approval responses, redacted traces/events, per-run costs, Studio generation polling, model catalog reads, and org model registration.

It intentionally does not expose account creation, sign-in, API key creation, billing/payment settings, organization membership, admin spend-cap changes, private dashboard configuration, or raw Trigger/FAL/R2 credentials. Those stay in the human UI or protected internal services.

Good Starting Prompts#

Use prompts that name the goal and the allowed scope:

  • Read the Zebrafish agent playbook, list my workflows, inspect the one named Blog Post Illustrations, run it with sample-safe inputs, then poll until it finishes.

  • Create a project named Spring Launch, attach it to workflow wf_1, upload this public brief URL as a project file, and verify the file is listed.

  • Inspect workflow wf_1, read the authoring guide, propose a draft change, validate it, save a draft revision, run a draft test, and stop before publishing.

  • List available Studio models, pick the cheapest model that can generate an image, create one generation for this prompt, and poll until terminal. Ask before spending.

  • Find run run_1, stream events until terminal, then summarize trace errors and cost breakdown.

Available Tools#

ToolDescriptionRequired scopeExample input
get_agent_playbookRead the compact operating playbook agents should follow before non-trivial work: loop, safety rules, tool-family workflows, verification, and recovery.Valid API key{}
list_skillsList hosted Zebrafish agent skills with HTML and raw SKILL.md URLs.Valid API key{}
get_skillRead one hosted Zebrafish SKILL.md by name.Valid API key{ "name": "zebrafish-ugc-ad-pack" }
list_workflowsList all workflows in your Zebrafish org.workflows:read or runs:trigger{ "limit": 10 }
run_workflowRun a workflow in your Zebrafish org.runs:trigger{ "id": "wf_blog_post_illustrations", "inputs": { "postUrl": "https://example.com/post" } }
get_runGet a workflow run by ID.workflows:read or runs:trigger{ "runId": "run_01JZ8A1M8QF7YH6K4R5S2T9V0B" }
list_runsList workflow runs in your Zebrafish org.workflows:read or runs:trigger{ "workflowId": "wf_blog_post_illustrations", "status": "completed", "limit": 20 }
create_workflow_from_templateCreate a starter workflow and initial draft revision.workflows:write{ "templateId": "image_prompt_starter", "name": "Campaign image generator" }
get_workflow_contextRead workflow definition, revision metadata, and safe campaign context for agent authoring.workflows:read{ "workflowId": "wf_blog_post_illustrations" }
get_agent_authoring_guideRead the versioned workflow-authoring guide for external agents.workflows:read{}
validate_workflow_definitionValidate a draft workflow definition and return structured repair diagnostics.workflows:write{ "workflowId": "wf_blog_post_illustrations", "inputs": [], "steps": [] }
save_draft_revisionSave a validated draft workflow revision using optimistic concurrency.workflows:write{ "workflowId": "wf_blog_post_illustrations", "basedOnRevisionId": "rev_123", "inputs": [], "steps": [] }
test_run_draftSave and test-run an unpublished draft once per derived or supplied idempotency key.workflows:write + runs:trigger{ "workflowId": "wf_blog_post_illustrations", "revision": { "basedOnRevisionId": "rev_123", "inputs": [], "steps": [] }, "inputs": {} }
publish_workflow_revisionPublish the latest workflow revision with optimistic concurrency.workflows:publish{ "workflowId": "wf_blog_post_illustrations", "revisionId": "rev_124", "expectedLatestRevisionId": "rev_124" }
get_run_detail_errorsRead redacted run and step errors without Trigger tokens or provider internals.workflows:read{ "runId": "run_01JZ8A1M8QF7YH6K4R5S2T9V0B" }
list_campaigns / get_campaignRead project metadata and briefs. Tool names remain campaign-named for compatibility.campaigns:read{ "archived": false }
get_campaign_knowledgeExport a project's OKF brand knowledge bundle.campaigns:read{ "campaignId": "cmp_1" }
create_campaign / update_campaignCreate a project, or update exactly one project field: name, brief, or default status.campaigns:write{ "name": "Spring launch" }
get_workflow_campaign / attach_campaign_to_workflowRead or change a workflow's project attachment.campaigns:read / campaigns:write{ "workflowId": "wf_1", "campaignId": "cmp_1" }
list_campaign_workflows / bulk_attach_campaign_workflowsRead or bulk attach workflow project assignments.campaigns:read / campaigns:write{ "campaignId": "cmp_1", "workflowIds": ["wf_1"] }
list_campaign_assetsList project file metadata.assets:read{ "campaignId": "cmp_1" }
create_campaign_asset_upload / finalize_campaign_asset_uploadCreate a presigned upload, then finalize it after uploading local bytes yourself. Prefer upload_campaign_asset_from_url when the asset is already available at an HTTPS URL.assets:write{ "campaignId": "cmp_1", "filename": "brief.pdf", "mimeType": "application/pdf", "sizeBytes": 12345 }
upload_campaign_asset_from_urlFetch a public HTTPS image, PDF, PPTX, text, or markdown file, upload it as a project file, and finalize it in one tool call.assets:write{ "campaignId": "cmp_1", "url": "https://example.com/brief.pdf" }
cancel_run / respond_run_approvalCancel runs or approve/reject pending approval steps.runs:manage{ "runId": "run_1" }
get_run_traceRead a full redacted run trace with step outputs and timeline events.workflows:read{ "runId": "run_1" }
get_workflowGet a single workflow by ID. Use instead of list_workflows when you already know the ID.workflows:read{ "id": "wf_blog_post_illustrations" }
get_run_costsGet per-step cost breakdown for a single run. Returns total and per-step provider costs in USD micros plus rollup completeness.usage:read{ "runId": "run_01JZ8A1M8QF7YH6K4R5S2T9V0B" }
get_campaign_asset_urlGet a short-lived presigned download URL (valid 300 s) for a campaign asset. Use after finalize_campaign_asset_upload to read back uploaded file content.workflows:read{ "campaignId": "cmp_1", "assetId": "ca_1" }
get_run_eventsList step events for a run in cursor order. Poll with the returned after cursor; the response includes the top-level run status so agents can stop on terminal states (completed, failed, cancelled, dispatch_failed).workflows:read{ "runId": "run_01JZ8A1M8QF7YH6K4R5S2T9V0B", "after": "1000", "limit": 50 }
create_studio_generationCreate an async studio media generation (spends real money; MCP clients may omit idempotencyKey; retries with the same supplied key never double-spend). variants 2-4 creates a compare batch. The studio tools return 404 when the studio surface is disabled for the deployment.studio:write{ "modelId": "openai/gpt-image-2", "config": { "input": { "prompt": "A lighthouse" } } }
get_studio_generationPoll a studio generation until status is terminal (succeeded, failed, timed_out). On success, outputUrl is the durable R2 URL and saveStatus shows library persistence state (saving/saved/save_failed). outputExpiresAt is null when the durable URL is set; falls back to the ephemeral FAL URL (~7 days) if auto-save is still in progress.studio:read{ "id": "sg_1" }
list_studio_generationsList the org's studio generations, newest first, cursor-paginated; filter by campaignId.studio:read{ "limit": 20, "campaignId": "cmp_1" }
list_modelsList models available for studio generation: curated catalog + the org's verified models, with input constraints and pricing estimates.models:read{}
purchase_creditsBuy org-bound prepaid credits (spends real money; bounded by per-key spend limits). Requires an explicit idempotencyKey — reuse it to retry the same purchase, never to start a new one. May return requires_action with a handoff URL a human must open to finish payment; credits become usable only after settlement. Hidden when agent payments are disabled for the deployment.billing:purchase_credits{ "credits": 25, "rail": "stripe_checkout", "idempotencyKey": "purchase-2026-07-09-001", "agentContext": { "reason": "Prepay approved Studio job" } }
get_credit_purchasePoll a credit purchase until terminal (confirmed, failed, expired). Use after requires_action or pending_settlement.billing:purchase_credits{ "purchaseId": "cp_1" }
add_org_modelRegister a FAL endpoint as an org model via the derived-profile flow. Curated model IDs are rejected; new models stay unverified until an admin runs the in-app verify flow.models:write{ "endpointId": "fal-ai/some-new-model" }

Experimental Authoring Proof Path#

The authoring lifecycle targets an existing workflow or a starter created from image_prompt_starter. Campaign mutation, asset upload/finalize, approval/cancel, and redacted trace reads are now separate tools with narrower scopes.

  1. Call create_workflow_from_template if you need a new starter workflow.

  2. Call get_workflow_context and copy revisions.latest.id.

  3. Call get_agent_authoring_guide.

  4. Edit the workflow inputs and steps.

  5. Call validate_workflow_definition until ok is true.

  6. Call save_draft_revision with basedOnRevisionId.

  7. Call test_run_draft.

  8. Call get_run_detail_errors to debug redacted failures.

  9. Call publish_workflow_revision with revisionId and expectedLatestRevisionId both set to the current latest revision id.

Every authoring error includes top-level docsUrl; when the server can provide it, details includes machine-readable code, cause, fix, retryable, and correlationId.

The same proof path is exposed as public REST endpoints under /api/v1:

REST endpointMCP equivalentScope
POST /workflows/from-templatecreate_workflow_from_templateworkflows:write
GET /workflows/:id/authoring-contextget_workflow_contextworkflows:read
GET /workflow-authoring-guideget_agent_authoring_guideworkflows:read
POST /workflows/:id/draft/validatevalidate_workflow_definitionworkflows:write
POST /workflows/:id/draft-revisionssave_draft_revisionworkflows:write
POST /workflows/:id/draft-test-runstest_run_draftworkflows:write + runs:trigger
POST /workflows/:id/revisions/:revisionId/publishpublish_workflow_revisionworkflows:publish
GET /runs/:id/debugget_run_detail_errorsworkflows:read
GET /campaignslist_campaignscampaigns:read
POST /campaignscreate_campaigncampaigns:write
GET /campaigns/:idget_campaigncampaigns:read
GET /campaigns/:id/knowledgeget_campaign_knowledgecampaigns:read
PATCH /campaigns/:idupdate_campaigncampaigns:write
GET /workflows/:id/campaignget_workflow_campaigncampaigns:read
PUT /workflows/:id/campaignattach_campaign_to_workflowcampaigns:write
GET /campaigns/:id/assetslist_campaign_assetsassets:read
POST /campaigns/:id/assets/presigncreate_campaign_asset_uploadassets:write
POST /campaigns/:id/assets/finalizefinalize_campaign_asset_uploadassets:write
POST /runs/:id/cancelcancel_runruns:manage
POST /runs/:id/approvalsrespond_run_approvalruns:manage
GET /runs/:id/traceget_run_traceworkflows:read
GET /workflows/:idget_workflowworkflows:read
GET /runs/:id/costsget_run_costsusage:read
POST /campaigns/:campaignId/assets/:assetId/urlget_campaign_asset_urlworkflows:read
GET /runs/:id/eventsget_run_eventsworkflows:read
POST /studio/generationscreate_studio_generationstudio:write
GET /studio/generations/:idget_studio_generationstudio:read
GET /studio/generationslist_studio_generationsstudio:read
GET /modelslist_modelsmodels:read
POST /modelsadd_org_modelmodels:write

upload_campaign_asset_from_url is an MCP convenience tool built on top of the REST presign/finalize pair; there is no separate public REST endpoint for URL fetch uploads.

MCP clients may omit idempotencyKey for mutating tools. Zebrafish derives a trusted key from the MCP request/tool/payload and forwards it to the internal REST handler. Provide a stable idempotencyKey only when you are deliberately retrying the same operation across a client reconnect or a new session.

Exception: purchase_credits REQUIRES an explicit idempotencyKey. A derived key cannot distinguish an agent-level retry from a new purchase, and every new key spends real money — reuse the same key when retrying the same purchase.

REST clients must still send the Idempotency-Key header on create-from-template, draft save, draft test-run, publish, campaign writes, asset upload/finalize, cancel, approval, and Studio generation requests. Reuse a key only for retries of the same payload; reusing it with a different payload returns 409 idempotency_conflict.

create_campaign_asset_upload is the exception to the normal 48-hour replay window: presigned PUT URLs expire after 15 minutes, so upload-creation idempotency is capped below that expiry instead of replaying stale upload URLs.

Asset Uploads#

Use upload_campaign_asset_from_url by default when an agent can point to a public source file. It fetches the source, validates size/MIME/magic bytes, creates the presigned upload, PUTs the bytes, finalizes the asset, and returns the asset IDs in one call.

Source URL requirements:

  • HTTPS only

  • Publicly routable host; localhost, private IPs, .local, and .internal hosts are rejected

  • No redirects; pass the final direct URL

  • Allowed MIME: image PNG/JPEG/WebP/GIF, PDF, PPTX, plain text, or markdown

  • Size at or below the project file limit

Use create_campaign_asset_upload plus finalize_campaign_asset_upload only when the agent has local bytes that are not already available through a public HTTPS URL. In that flow the agent must PUT bytes to the returned presigned URL before finalizing.

Hosted design_workflow Decision#

No hosted design_workflow LLM tool is exposed in this slice. External agents get the same public-safe authoring guide, campaign context, assets, validation, draft save/test, publish, and trace operations, but they spend their own model budget. Revisit a hosted design tool only with an explicit product decision for app-managed LLM spend, billing, rate limits, and prompt/version ownership.

Failure Modes#

MCP clients display tool failures as isError: true results. The text content is the v1 API error object, preserved with type, message, requestId, docsUrl, and any details.

Missing key, 401:

JSON
{
  "isError": true,
  "_meta": { "status": 401 },
  "content": [
    {
      "type": "text",
      "text": "{\n  \"type\": \"authentication_error\",\n  \"message\": \"Invalid or unauthorized credentials.\",\n  \"requestId\": \"req_01HV8XYZABC\",\n  \"docsUrl\": \"https://zebrafish.dev/docs/api-keys#auth-errors\"\n}"
    }
  ]
}

Insufficient scope, 403:

JSON
{
  "isError": true,
  "_meta": { "status": 403 },
  "content": [
    {
      "type": "text",
      "text": "{\n  \"type\": \"insufficient_scope\",\n  \"message\": \"This API key does not have the required scope: runs:trigger.\",\n  \"requestId\": \"req_01HV8XYZABC\",\n  \"docsUrl\": \"https://zebrafish.dev/docs/api-keys#scopes\"\n}"
    }
  ]
}

Workflow not found, 404:

JSON
{
  "isError": true,
  "_meta": { "status": 404 },
  "content": [
    {
      "type": "text",
      "text": "{\n  \"type\": \"not_found\",\n  \"message\": \"Workflow not found\",\n  \"requestId\": \"req_01HV8XYZABC\",\n  \"docsUrl\": \"https://zebrafish.dev/docs/api-keys#not-found\"\n}"
    }
  ]
}

Spend cap exceeded, 402:

JSON
{
  "isError": true,
  "_meta": { "status": 402 },
  "content": [
    {
      "type": "text",
      "text": "{\n  \"type\": \"spend_cap_exceeded\",\n  \"message\": \"This run would exceed your organization's monthly spend cap.\",\n  \"requestId\": \"req_01HV8XYZABC\",\n  \"docsUrl\": \"https://zebrafish.dev/docs/api-keys#spend-cap\",\n  \"details\": {\n    \"currentUsdMicros\": \"6500000\",\n    \"reservedUsdMicros\": \"0\",\n    \"capUsdMicros\": \"7000000\",\n    \"estimateUsdMicros\": \"1000000\",\n    \"projectedUsdMicros\": \"7500000\"\n  }\n}"
    }
  ]
}

Insufficient prepaid usage balance, 402:

JSON
{
  "isError": true,
  "_meta": { "status": 402 },
  "content": [
    {
      "type": "text",
      "text": "{\n  \"type\": \"insufficient_credits\",\n  \"message\": \"Insufficient prepaid usage balance.\",\n  \"requestId\": \"req_01HV8XYZABC\",\n  \"docsUrl\": \"https://zebrafish.dev/docs/api-keys#prepaid-usage-balance\",\n  \"details\": {\n    \"requiredUsdMicros\": \"1250000\",\n    \"availableUsdMicros\": \"500000\"\n  }\n}"
    }
  ]
}

Rate limit, 429:

JSON
{
  "isError": true,
  "_meta": { "status": 429 },
  "content": [
    {
      "type": "text",
      "text": "{\n  \"type\": \"rate_limit_exceeded\",\n  \"message\": \"API key rate limit exceeded. Retry after the server-provided cooldown.\",\n  \"requestId\": \"req_01HV8XYZABC\",\n  \"docsUrl\": \"https://zebrafish.dev/docs/api-keys#rate-limits\"\n}"
    }
  ]
}

Schema Reference#

The canonical schema reference is the v1 OpenAPI document at /api/v1/openapi.json. MCP tool input schemas are tested against the corresponding v1 operations so schema drift fails CI.