MCP server
Connect any MCP client to the hosted Windpaint MCP server: authentication, all 20 tools, and the errors agents see.
The Windpaint MCP server gives an agent Windpaint’s generation API as MCP tools: list capabilities, estimate, upload, generate, wait, download, and run products. It’s a thin client of the public API, so every tool call is an ordinary API request made with your key, billed and scoped like one.
Hosted endpoint (streamable HTTP):
https://mcp.windpaint.ai/mcp
The quickest way to connect, from Claude Code:
claude mcp add --transport http windpaint https://mcp.windpaint.ai/mcp \
--header "Authorization: Bearer $WINDPAINT_API_KEY"
For Claude Code, Codex and Cursor, the skills plugin installs this server along with instructions for using it. See Claude Code, Codex and Cursor.
Authentication
Every request to the hosted server must carry your API key:
| Header | Required | Value |
|---|---|---|
Authorization | Yes | Bearer aak_... |
X-Windpaint-Project | No | A project id or slug. Sets the default project for every tool call on this connection. |
The server passes the key through to the API for each call and stores nothing. There is no OAuth sign-in; clients that can only connect through OAuth can’t use the hosted server yet. Create a key in Settings → API Keys (API keys).
Projects
Every job and asset belongs to a project. For each tool call the project is, in order:
- The tool’s
projectargument (id or slug), if given. - The
X-Windpaint-Projectheader on the connection. - Your organization’s default project.
Asset ids only work as inputs inside the project they belong to.
Generic client config
Most MCP clients accept a JSON block like this. Check your client’s docs for where the file lives and whether it supports environment variable interpolation in headers, so the key doesn’t have to be written into the file.
{
"mcpServers": {
"windpaint": {
"type": "http",
"url": "https://mcp.windpaint.ai/mcp",
"headers": {
"Authorization": "Bearer aak_...",
"X-Windpaint-Project": "my-project"
}
}
}
}
Drop X-Windpaint-Project to use your default project.
Files
The server runs on Windpaint’s side, so it can’t see your disk:
- Uploading:
upload_assettakes a publichttp(s)URL, which the server fetches, or the file contents asdata_base64. URLs that point at private or local addresses, or that redirect, are refused. A local filepathis refused. - Downloading:
download_assetreturns a signed URL valid for 15 minutes. The agent fetches it itself, for example withcurl -o out.png "<url>". The signed URL needs no key; don’t share it, and don’t store it. Calldownload_assetagain for a fresh one.
If an agent mostly works with files on disk, it can also use the CLI, which uploads local paths and saves outputs directly.
Tools
The server also sends the client short instructions: check list_products first, estimate before large runs, wait on video instead of blocking, and chain steps by passing asset ids from one job’s outputs into the next job’s inputs.
A typical flow: list_capabilities → upload_asset (start frames, references) → estimate_cost → generate → wait_for_job → download_asset.
Catalog and cost
| Tool | What it does |
|---|---|
list_capabilities | Every capability with its input slots, whether a prompt is required, allowed aspect ratios, and the models that implement it with available, resolutions, durations, licence and the current credit price per tier. Takes no arguments. |
estimate_cost | Credits a generation would cost, without running it. Also validates the arguments and returns the chosen model, output width/height, and the organization’s available_credits and held_credits. |
get_balance | The organization’s credits: available, held, totals by_source, next expiry, and each credit lot. Takes no arguments. |
estimate_cost parameters:
| Parameter | Type | Default | Notes |
|---|---|---|---|
capability | string | required | For example image.generate, video.generate |
prompt | string | "" | Required by most capabilities |
model | string | first available model | |
inputs | object | {} | {slot: [asset_id, ...]} |
resolution | string | model default | 1k, 2k, 480p, … as listed |
aspect_ratio | string | 16:9 for video, 1:1 otherwise | |
quality | string | none | No model accepts a quality tier today |
duration_s | integer | model’s first duration | Video |
project | string | default project | Id or slug |
credits is null when that tier has no price and can’t be submitted. available_credits and held_credits are left out when the key can’t read billing.
Generation
| Tool | What it does |
|---|---|
upload_asset | Upload a reference image, start frame, video or audio file and get back an asset. PNG, JPEG, WebP, MP4, MP3, WAV, up to 50 MiB. Free. |
generate | Run a capability. Returns the queued job at once, or with wait=true, polls until it finishes. |
get_job | Current status of a job, with outputs (asset ids and URLs), images/video shortcuts, and credits estimated and charged. |
wait_for_job | Poll a job until it finishes or the timeout passes. Returns the final status, or the latest status with a note to call again. |
cancel_job | Cancel an unfinished job. A render already running isn’t interrupted, but its output is discarded and its credits released. |
download_asset | Get a 15-minute signed download URL for an asset. |
list_recent_jobs | Recent jobs in a project, newest first, with status, outputs and credits. |
list_assets | Uploads and outputs in a project, newest first. |
upload_asset takes exactly one of url or data_base64:
| Parameter | Type | Default | Notes |
|---|---|---|---|
url | string | Public http(s) URL the server fetches | |
data_base64 | string | File contents; pass filename or content_type with it | |
filename | string | taken from the URL | |
content_type | string | guessed from the filename | For example image/png |
project | string | default project |
generate parameters:
| Parameter | Type | Default | Notes |
|---|---|---|---|
capability | string | required | |
prompt | string | "" | |
model | string | first available model | |
inputs | object | {} | {"start_frame": [id]} for video.generate; ids from upload_asset or an earlier job’s outputs |
resolution | string | model default | |
aspect_ratio | string | 16:9 for video, 1:1 otherwise | |
quality | string | none | |
duration_s | integer | model’s first duration | |
seed | integer | random | Set it to reproduce or vary a result |
project | string | default project | |
wait | boolean | false | Poll until the job finishes |
timeout_s | integer | 120 | Max 600. Only used with wait=true |
Other generation tools:
| Tool | Parameter | Type | Default | Notes |
|---|---|---|---|---|
get_job | job_id | string | required | The request_id from generate |
wait_for_job | job_id | string | required | |
timeout_s | integer | 300 | Max 600 | |
cancel_job | job_id | string | required | |
download_asset | asset | string | required | Asset id, or an asset URL from a job’s outputs |
list_recent_jobs | limit | integer | 20 | 1–200 |
project | string | default project | ||
list_assets | limit | integer | 50 | 1–200 |
project | string | default project |
Waiting tools poll every 2 seconds at first, backing off to 10 seconds, and report progress to clients that show it. Images finish in seconds; video takes minutes, so the usual pattern is generate with wait=false, then wait_for_job with timeout_s=600, called again if the job is still running.
Projects
| Tool | Parameter | Type | Default | What it does |
|---|---|---|---|---|
list_projects | archived | boolean | false | Projects in your organization. true lists archived ones. |
create_project | name | string | required | Create a project. Returns it with its id and slug. |
slug | string | derived from the name | Lowercase letters, digits and hyphens | |
description | string | none |
Products
Products are multi-step workflows run with one call, such as text-to-clip.
| Tool | What it does |
|---|---|
list_products | Products with their input form, outputs, steps, available with unavailable_reasons, and credits_estimate at default settings. |
get_product | One product by slug. |
estimate_product | Credits a run would cost with these inputs, validated against the product’s form, plus available_credits. |
run_product | Start a run. Returns the queued run, or with wait=true, polls and returns the run with named outputs. |
get_run | A run’s status, each step’s status and outputs, the run’s outputs, and credits. |
list_runs | Recent runs, newest first. |
cancel_run | Cancel an unfinished run and its pending and running steps. |
| Tool | Parameter | Type | Default | Notes |
|---|---|---|---|---|
list_products | category | string | all | image, video or audio |
get_product | slug | string | required | |
estimate_product | slug | string | required | |
inputs | object | {} | Text and choice values as strings; media as one asset id each | |
project | string | default project | ||
run_product | slug | string | required | |
inputs | object | {} | ||
project | string | default project | ||
wait | boolean | false | ||
timeout_s | integer | 300 | Max 600 | |
get_run | run_id | string | required | |
wait | boolean | false | ||
timeout_s | integer | 300 | Max 600 | |
list_runs | limit | integer | 20 | 1–200 |
product | string | all | Product slug | |
project | string | default project | ||
cancel_run | run_id | string | required |
Errors agents see
Tool failures come back as MCP tool errors with a plain message. API errors are passed through as Windpaint API error <status>: <message>, using the API’s own message (see Errors).
| Message | Cause | What to do |
|---|---|---|
Missing API key. Send 'Authorization: Bearer <windpaint api key>' with each request. | No Authorization header | Fix the client config |
Windpaint API error 401: Unauthorized | Key is wrong, expired or revoked | Create a new key |
Windpaint API error 403: Forbidden | The key’s role can’t do this | Use a key with a broader role |
Not enough credits. This needs 4 credits; 1.2 are available. Top up in the Windpaint dashboard (Settings → Billing), or pick a cheaper option (lower resolution, shorter duration, another model) and check estimate_cost. | 402 billing.insufficient_credits | Buy credits in Settings → Billing; nothing was charged |
Windpaint API error 422: No model implements image.edit yet. | Capability has no model today | Check list_capabilities for available models |
Windpaint API error 422: Inputs must be asset ids or URLs returned by /v1/generation. | An external URL was passed in inputs | upload_asset with url first, then pass the asset id |
Windpaint API error 422: Input asset not found in this project. | Asset id from another project, or a typo | Use an asset from the same project |
Windpaint API error 503: ... | Model not available, or the tier has no price | Pick another listed model or tier |
`path` only works when the MCP server runs locally. Use url or data_base64. | upload_asset was given a local file path | Upload by URL or base64 |
Give exactly one of path, url or data_base64. | upload_asset called with none or both of url and data_base64 | |
That URL points at a private or local address. | upload_asset with an internal URL | Use a public URL or data_base64 |
That URL redirects; pass the final URL. | upload_asset with a redirecting URL | Resolve the redirect first |
File exceeds 50 MiB. | Upload too large | |
Cannot tell the file type; pass content_type (e.g. image/png). | No extension and no content type | Pass content_type |
'...' is not an asset id or asset URL. | download_asset got something else | Pass an id from outputs |
A wait that runs out of time is not an error. The tool returns the job’s current status with a note such as Still in_progress after 600s; call wait_for_job again. The job is still running and still billed; resubmitting would create and bill a second job.