Run a script that calls these tools
run_scriptWrite a JavaScript program that calls this server's other tools. You have the whole language: loops, arithmetic, functions, conditionals, and values carried from one call into the next — all running next to the tools instead of across the conversation. Use it whenever code says the thing more directly than a sequence of separate calls would, which is often.
COMPUTE, don't hand-write. Anything you would otherwise work out in your head and type as literals is better computed here — eased keyframe tracks, staggered start times, grid coordinates, derived palettes, positions from measured text widths. This is usually what makes motion look right: sample a curve at ten points and emit the values, rather than guessing four.
DON'T COMMENT THE SCRIPT. Nobody reads it — it runs once and is gone. Comments, blank lines and explanatory names are pure cost here. Write it dense.
The one thing to remember: inside a script you see only what you return or log, so a tool's own rich output (layout measurements, layout_qa, new ids) has to be surfaced deliberately. That also makes a single-call script worthwhile when a read is fat — find takes no projection argument, so logging the 200 characters you need is a real saving.
A fat read is usually better narrowed at the source than filtered here: get_clip takes select, and get_element_schema takes fields (name what you're setting and a 15KB schema becomes ~200 bytes). get_element_schema returns TEXT by default, which you cannot index into — pass format: 'json' when you want to compute over the schema rather than log it.
CALLING TOOLS Every tool except run_script and get_script_job is a global function taking exactly the arguments it takes normally, and returning its parsed result (those two are excluded so a script cannot recurse into itself or poll its own job). Calls are synchronous — do NOT use async/await, and there are no imports.
sleep(ms) waits, synchronously like everything else here. Use it to poll a generation: generate_media and voiceover_batch return before their work lands, and get_clip(select:['busy']) says what is still being written. A sleep longer than the script's remaining time FAILS the run with a budget error rather than overrunning it or returning early (a run that hands off to a background job gets the job budget, and a sleep already waiting picks that up) — and it holds one of the few concurrent script slots while it waits, so poll on the order of seconds, not milliseconds.
const p = create_project({ title: "Launch" }); const made = add_clips({ project_id: p.projectId, kind: "blank", clips: [{ duration: 4 }] });
Use the BARE tool name. If your client shows these tools under a prefix, the prefixed form works too — mcp__clueso_connect__add_elements and clueso__add_elements both resolve to add_elements. tools() lists every callable name; call(name, args) invokes one by a name computed at runtime.
WHY IT IS CHEAPER What you construct never passes through the conversation. Build an array in a loop and send it in ONE batched call — a 24x24 dot grid is six lines here versus 576 elements of JSON:
const els = []; for (let r = 0; r < 24; r++) for (let c = 0; c < 24; c++) els.push({ x: c * 60, y: r * 60, width: 8, height: 8, type_data: { backgroundColor: "#C462F5" } }); add_elements({ project_id, defaults: { clip_index: 0, element_type: "rectangle" }, elements: els });
READ, THEN WRITE — this is how you edit in bulk:
const clip = get_clip({ project_id, clip_index: 0, select: ["elements.name", "elements.y"] }); const captions = clip.elements.filter(e => e.name.startsWith("caption")); update_elements({ project_id, defaults: { clip_index: 0 }, updates: captions.map(e => ({ element_id: e.id, y: e.y + 40 })) });
WHAT COMES BACK
return a value to hand it back, and console.log anything you want to see — that output is all you pay for, so log summaries, not payloads. print_json(obj, maxBytes) logs an object and truncates it for you, which is safer than hand-rolling JSON.stringify(...).slice(...) at every call site.
You get { ok, calls, writes_applied, result, stdout }. writes_applied matters because a script is NOT a transaction: if it throws on call nine, the first eight writes already landed, and that count is how you tell.
A FAILED TOOL CALL THROWS. It does not return an error object, so a failure stops the script instead of letting it run on against bad state. Wrap a call in try/catch only when you genuinely intend to continue.
You get every tool's normal result, so succeeded/failed counts, per-element layout (font_size_px, text_width_px, natural_width_px, line_count, fits_width/fits_height/fits plus adjusted when your height was replaced — widen to natural_width_px, not text_width_px) and layout_qa findings are all readable in the script. Check them and react.
NO IMAGES are returned inside a script — a render in a loop would flood the reply. But you are not blind: get_clip({..., render: { save: true }}) gives the script a presigned_url and s3_key for the rendered frame, so it can render many frames, keep the URLs, and hand them on (update_clueprint takes one as source_url). _images_omitted counts the frames withheld; when the reply would be a bare array it arrives as {items, _images_omitted}. Call get_clip yourself, outside a script, when YOU need to look at one.
FINISHING
Fast scripts return their result here. If one is still running after wait_seconds (default 45s) you get { job_id, status: "processing" } and it keeps running — poll get_script_job. Set idempotency_key on anything that builds, so a retry cannot run it twice — a key that already ran is never re-executed, failed runs included.
LIMITS: 200 tool calls, 45s of run time inline and 5 minutes once it hands off to a job, 16KB each of logged output and returned value. dry_run runs reads for real and only records writes. Anything costing credits errors as it normally would.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| code | Yes | JavaScript. Tools are globals called synchronously — no async/await, no imports, no network or filesystem. `return` a value to hand it back. | |
| dry_run | No | Read-only tools still run; anything that writes is recorded and skipped. Use it to check what a script would do before it does it. | |
| wait_seconds | No | How long to hold this call open waiting for the script, default 45s. If it finishes in time you get the result here; if not you get a job_id to poll with get_script_job, and the script keeps running either way. | |
| idempotency_key | No | Pass a unique string so a retry cannot run the same build twice. If a script with this key already ran in this workspace, its job is returned and nothing executes again — including when that run FAILED, because a script is not a transaction and the writes it made before failing are still there. To act after a failure, read the project, repair what landed, and use a NEW key. Worth setting for anything that builds. | |
| timeout_seconds | No | Wall-clock budget for the SCRIPT — separate from wait_seconds, which is only how long this call stays open. By default a script gets 45s while you are waiting on it, and is raised to 5 minutes the moment it outlives the wait and becomes a job you poll with get_script_job. Set this only to override BOTH with one fixed budget. Watch the slow tools: analyze_audio in 'transcript' or 'beats' mode, export_project and auto_sync take tens of seconds each and will eat a budget fast. analyze_audio in 'features' mode is ~1s and is fine to loop over. |