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.
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#
Install the Zebrafish skills in the agent workspace:
npx zebrafish-skills install npx zebrafish-skills checkThe install adds the router skill, production playbooks, verification script, and optional non-blocking hooks for supported coding-agent workspaces.
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-agentRun only:
runs:triggerRead/debug runs:
workflows:readorruns:triggerAuthor drafts:
workflows:read,workflows:write,runs:triggerPublish: add
workflows:publishProject files:
campaigns:read,campaigns:write,assets:read,assets:writeStudio:
models:read,studio:read,studio:write
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.
Add the server to Claude Code:
claude mcp add zebrafish https://zebrafish.dev/api/mcp --header "Authorization: Bearer $ADFISH_API_KEY"For Codex, add the same remote MCP server to
~/.codex/config.toml:[mcp_servers.zebrafish] url = "https://zebrafish.dev/api/mcp" [mcp_servers.zebrafish.http_headers] Authorization = "Bearer adf_..."For another MCP client, configure a remote Streamable HTTP server at
https://zebrafish.dev/api/mcpwith the headerAuthorization: Bearer <your-key>.In the agent session, ask:
Read the Zebrafish agent playbook, then list my workflowsExpected output:
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)Ask:
Run my Blog Post Illustrations workflowExpected output:
{ "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/mcpwith a scoped API key. This is the best path for coding agents and operator agents outside the web app.In-app
/agenttools: Zebrafish's own agent exposes the same MCP-backed capabilities as prefixed tools such aszebrafish_list_workflowsandzebrafish_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 withAuthorizationandIdempotency-Keyheaders.Human UI: Users inspect workflows and runs, manage projects, files, Studio, approvals, settings, and billing in the app.
/workflowsis 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.
Recommended Agent Workflow#
For reliable autonomous work, keep the same loop for every task:
Prime: call
get_agent_playbookfor non-trivial tasks.Discover:
list_workflows,list_campaigns,list_models, or the relevant list/get tool.Inspect:
get_workflow,get_workflow_context,get_campaign_knowledge,list_campaign_assets,get_run_trace, orget_run_events.Plan: choose the narrowest write tool and ask the user/client for approval before spend, publish, cancel, approval responses, or asset/campaign writes.
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_creditsrequires an explicitidempotencyKey(it spends real money). Payment tools are advertised only whenAGENT_PAYMENTS_ENABLEDis on; rail availability still depends on the fake webhook secret or Stripe Checkout unit price + webhook configuration described in the API key docs.Verify: poll
get_run,get_run_events, orget_studio_generationuntil 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#
| Tool | Description | Required scope | Example input |
|---|---|---|---|
get_agent_playbook | Read the compact operating playbook agents should follow before non-trivial work: loop, safety rules, tool-family workflows, verification, and recovery. | Valid API key | {} |
list_skills | List hosted Zebrafish agent skills with HTML and raw SKILL.md URLs. | Valid API key | {} |
get_skill | Read one hosted Zebrafish SKILL.md by name. | Valid API key | { "name": "zebrafish-ugc-ad-pack" } |
list_workflows | List all workflows in your Zebrafish org. | workflows:read or runs:trigger | { "limit": 10 } |
run_workflow | Run a workflow in your Zebrafish org. | runs:trigger | { "id": "wf_blog_post_illustrations", "inputs": { "postUrl": "https://example.com/post" } } |
get_run | Get a workflow run by ID. | workflows:read or runs:trigger | { "runId": "run_01JZ8A1M8QF7YH6K4R5S2T9V0B" } |
list_runs | List workflow runs in your Zebrafish org. | workflows:read or runs:trigger | { "workflowId": "wf_blog_post_illustrations", "status": "completed", "limit": 20 } |
create_workflow_from_template | Create a starter workflow and initial draft revision. | workflows:write | { "templateId": "image_prompt_starter", "name": "Campaign image generator" } |
get_workflow_context | Read workflow definition, revision metadata, and safe campaign context for agent authoring. | workflows:read | { "workflowId": "wf_blog_post_illustrations" } |
get_agent_authoring_guide | Read the versioned workflow-authoring guide for external agents. | workflows:read | {} |
validate_workflow_definition | Validate a draft workflow definition and return structured repair diagnostics. | workflows:write | { "workflowId": "wf_blog_post_illustrations", "inputs": [], "steps": [] } |
save_draft_revision | Save a validated draft workflow revision using optimistic concurrency. | workflows:write | { "workflowId": "wf_blog_post_illustrations", "basedOnRevisionId": "rev_123", "inputs": [], "steps": [] } |
test_run_draft | Save 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_revision | Publish the latest workflow revision with optimistic concurrency. | workflows:publish | { "workflowId": "wf_blog_post_illustrations", "revisionId": "rev_124", "expectedLatestRevisionId": "rev_124" } |
get_run_detail_errors | Read redacted run and step errors without Trigger tokens or provider internals. | workflows:read | { "runId": "run_01JZ8A1M8QF7YH6K4R5S2T9V0B" } |
list_campaigns / get_campaign | Read project metadata and briefs. Tool names remain campaign-named for compatibility. | campaigns:read | { "archived": false } |
get_campaign_knowledge | Export a project's OKF brand knowledge bundle. | campaigns:read | { "campaignId": "cmp_1" } |
create_campaign / update_campaign | Create a project, or update exactly one project field: name, brief, or default status. | campaigns:write | { "name": "Spring launch" } |
get_workflow_campaign / attach_campaign_to_workflow | Read or change a workflow's project attachment. | campaigns:read / campaigns:write | { "workflowId": "wf_1", "campaignId": "cmp_1" } |
list_campaign_workflows / bulk_attach_campaign_workflows | Read or bulk attach workflow project assignments. | campaigns:read / campaigns:write | { "campaignId": "cmp_1", "workflowIds": ["wf_1"] } |
list_campaign_assets | List project file metadata. | assets:read | { "campaignId": "cmp_1" } |
create_campaign_asset_upload / finalize_campaign_asset_upload | Create 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_url | Fetch 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_approval | Cancel runs or approve/reject pending approval steps. | runs:manage | { "runId": "run_1" } |
get_run_trace | Read a full redacted run trace with step outputs and timeline events. | workflows:read | { "runId": "run_1" } |
get_workflow | Get 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_costs | Get 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_url | Get 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_events | List 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_generation | Create 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_generation | Poll 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_generations | List the org's studio generations, newest first, cursor-paginated; filter by campaignId. | studio:read | { "limit": 20, "campaignId": "cmp_1" } |
list_models | List models available for studio generation: curated catalog + the org's verified models, with input constraints and pricing estimates. | models:read | {} |
purchase_credits | Buy 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_purchase | Poll a credit purchase until terminal (confirmed, failed, expired). Use after requires_action or pending_settlement. | billing:purchase_credits | { "purchaseId": "cp_1" } |
add_org_model | Register 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.
Call
create_workflow_from_templateif you need a new starter workflow.Call
get_workflow_contextand copyrevisions.latest.id.Call
get_agent_authoring_guide.Edit the workflow
inputsandsteps.Call
validate_workflow_definitionuntilokistrue.Call
save_draft_revisionwithbasedOnRevisionId.Call
test_run_draft.Call
get_run_detail_errorsto debug redacted failures.Call
publish_workflow_revisionwithrevisionIdandexpectedLatestRevisionIdboth 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 endpoint | MCP equivalent | Scope |
|---|---|---|
POST /workflows/from-template | create_workflow_from_template | workflows:write |
GET /workflows/:id/authoring-context | get_workflow_context | workflows:read |
GET /workflow-authoring-guide | get_agent_authoring_guide | workflows:read |
POST /workflows/:id/draft/validate | validate_workflow_definition | workflows:write |
POST /workflows/:id/draft-revisions | save_draft_revision | workflows:write |
POST /workflows/:id/draft-test-runs | test_run_draft | workflows:write + runs:trigger |
POST /workflows/:id/revisions/:revisionId/publish | publish_workflow_revision | workflows:publish |
GET /runs/:id/debug | get_run_detail_errors | workflows:read |
GET /campaigns | list_campaigns | campaigns:read |
POST /campaigns | create_campaign | campaigns:write |
GET /campaigns/:id | get_campaign | campaigns:read |
GET /campaigns/:id/knowledge | get_campaign_knowledge | campaigns:read |
PATCH /campaigns/:id | update_campaign | campaigns:write |
GET /workflows/:id/campaign | get_workflow_campaign | campaigns:read |
PUT /workflows/:id/campaign | attach_campaign_to_workflow | campaigns:write |
GET /campaigns/:id/assets | list_campaign_assets | assets:read |
POST /campaigns/:id/assets/presign | create_campaign_asset_upload | assets:write |
POST /campaigns/:id/assets/finalize | finalize_campaign_asset_upload | assets:write |
POST /runs/:id/cancel | cancel_run | runs:manage |
POST /runs/:id/approvals | respond_run_approval | runs:manage |
GET /runs/:id/trace | get_run_trace | workflows:read |
GET /workflows/:id | get_workflow | workflows:read |
GET /runs/:id/costs | get_run_costs | usage:read |
POST /campaigns/:campaignId/assets/:assetId/url | get_campaign_asset_url | workflows:read |
GET /runs/:id/events | get_run_events | workflows:read |
POST /studio/generations | create_studio_generation | studio:write |
GET /studio/generations/:id | get_studio_generation | studio:read |
GET /studio/generations | list_studio_generations | studio:read |
GET /models | list_models | models:read |
POST /models | add_org_model | models: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.internalhosts are rejectedNo 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:
{
"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:
{
"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:
{
"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:
{
"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:
{
"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:
{
"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.