A Seedance 2.5 video pipeline over MCP: why generate_video returns a run id instead of the clip
A tool call that waits for a video is a tool call that times out. A Seedance 2.5 clip takes minutes to render, and an agent's tool call is not built to hang that long. So in the emaki MCP server, generate_video gives bac
A tool call that waits for a video is a tool call that times out. A Seedance 2.5 clip takes minutes to render, and an agent's tool call is not built to hang that long. So in the emaki MCP server, generate_video gives back a run id straight away, and a separate check_status tool reports when the file is ready. This post walks through the four tools, the remote endpoint you can call without installing anything, and a 40-line script that turns a shot list into finished clips through the same JSON-RPC calls an agent makes.
Some context: emaki is a Japanese-language site for ByteDance's Seedance video models, and the MCP server is its official one. It runs on your own emaki account, so every run costs the same credits as making the clip on the website.
The four tools
| Tool | What it returns |
|---|---|
generate_video |
A run id (the clip is still rendering) |
check_status |
generating, failed with a code, or the URL of the finished file |
generate_image |
The image URL; images are quick enough to wait for |
credits |
Credits left on the account; free to call |
generate_video defaults to Seedance 2.5: 4 to 30 seconds, 480p / 720p / 1080p, sound in the same pass. Seedance 2.0, its Fast and Mini variants, MiniMax H3 and Veo 3.1 are selectable with model. Pass image_url and the still becomes the first frame, which is the image to video flow from the site.
Two ways to connect
Local, as a stdio process:
{
"mcpServers": {
"emaki": {
"command": "npx",
"args": ["-y", "emaki-mcp"],
"env": { "EMAKI_API_KEY": "sk-..." }
}
}
}
Remote, over Streamable HTTP, nothing installed:
claude mcp add --transport http emaki https://emaki-mcp.emaki.deno.net/mcp \
--header "Authorization: Bearer sk-..."
The key travels with each request and the remote server stores nothing. You create it on the API key page.
Why a run id and not the clip
Three things pushed the design that way.
Renders take minutes. The npm package lets you pass wait: true and block for up to 15 minutes, which is fine in a terminal. The remote server never blocks on video: generate_video submits and answers at once. For images it polls every 5 seconds for up to 50 seconds before it gives up and hands back the id, because most images finish inside that window.
One video at a time per account. A second submit while the first is still rendering is refused with HTTP 409. The server turns that into a message the model can act on, not a stack trace:
generate_video refused: Video generation is already in progress. Wait for the current run to finish (check_status), then try again.
Every refusal says what to do next. No key, no credits, a model that needs a purchase, a prompt the filter stopped: each one comes back as isError: true with the next step in plain text. With no key at all, the remote endpoint answers:
No API key. Pass your emaki API key as "Authorization: Bearer sk-..." (create one at https://emaki.ai/settings/apikeys).
An agent reading that can tell the user exactly what is missing. A bare 401 would leave it guessing.
A shot list in, clips out
You do not need an MCP client to use an MCP server. The remote endpoint speaks plain JSON-RPC over HTTP, so a script can make the same calls an agent would. This one renders a three-shot list in order, one clip at a time, which is all the account allows anyway:
// shots.mjs: node shots.mjs (Node 18+, EMAKI_API_KEY in the environment)
const MCP = "https://emaki-mcp.emaki.deno.net/mcp";
let n = 0;
async function call(name, args = {}) {
const res = await fetch(MCP, {
method: "POST",
headers: {
"content-type": "application/json",
accept: "application/json",
authorization: `Bearer ${process.env.EMAKI_API_KEY}`,
},
body: JSON.stringify({ jsonrpc: "2.0", id: ++n, method: "tools/call", params: { name, arguments: args } }),
});
const { result } = await res.json();
const text = result.content[0].text;
if (result.isError) throw new Error(text);
return text;
}
const shots = [
"Night alley in Shinjuku, light rain. A woman in a yellow raincoat walks toward the camera. Slow push-in.",
"Same woman stops under a neon sign and looks up. Close-up, shallow depth of field.",
"Wide shot from a rooftop: she disappears into the crowd. The camera tilts up to the skyline.",
];
console.log(await call("credits"));
for (const prompt of shots) {
const started = await call("generate_video", {
prompt, duration: 5, resolution: "480p", aspect_ratio: "9:16",
});
const id = started.match(/id: (\S+)/)[1];
for (;;) {
await new Promise((r) => setTimeout(r, 5000));
const s = await call("check_status", { run_id: id });
if (s.startsWith("Run ready")) { console.log(s.split("\n")[2]); break; }
if (s.startsWith("Run failed")) { console.log(s); break; }
}
}
The first line it prints is the balance, from the free credits tool. Run it before a batch: three Seedance 2.5 clips at 480p and 5 seconds cost 90 credits each on the site's price list, 270 in total, and the script stops at the first refusal instead of half-finishing.
If you want typed functions instead of JSON-RPC, the same API ships as SDKs: npm install emaki-ai (generateVideo, submit, status, wait) and pip install emaki.
Where the 480p drafts go next
The shot list renders at 480p on purpose: it is the cheapest tier, the right size to check framing and motion. Two ways to finish a keeper:
- Run it again at
720por1080p. That is a fresh generation, so motion and framing will come out differently. - Keep the exact take and pass it through the AI video upscaler on the same account (2x or 4x, clips up to 15 seconds).
For the Japanese-language walkthroughs of the site itself, start at γ·γΌγγ³γΉ.
Your turn
When a tool call has to wait for something slow, do you let the agent block, or hand back an id and make it poll? I went with the id for video and a short wait for images, and I'd like to hear where that split lands for render or build jobs in your setup.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.
