# 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

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.

## Recommended Agent Workflow

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

| 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.

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 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 `.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`](https://zebrafish.dev/api/v1/openapi.json). MCP tool input schemas are tested against the corresponding v1 operations so schema drift fails CI.
