AppsGolem YouTube Cutter
io.github.apancyborg/appsgolem-mcp
Documentation
appsgolem-mcp (Node / TypeScript)
An MCP server for the AppsGolem YouTube
cutter API. It lets an AI agent (Claude Desktop, Claude Code, Cursor, …) cut
clips from YouTube videos — in any format the web cutter supports — and get a
direct download URL back. The REST logic lives in a small, dependency-light
client (src/client.ts); src/server.ts is the thin MCP tool layer over it.
Get your API key → appsgolem.com/agents — sign up, add prepaid credits, and generate a key (
ag_live_…). That page also has copy-paste setup for Claude Code, Codex, Cursor, and any MCP client, plus a prompt cookbook.
Requirements
- Node.js >= 18 (uses the global
fetch). - An AppsGolem API key (
ag_live_…) — get one at appsgolem.com/agents (sign up → add credits → generate a key in your dashboard). Credits are prepaid; buy a pack or a subscription, no auto-renewal required.
Install / connect (no manual install)
npx fetches and runs the server on demand — nothing to install globally.
Claude Desktop / Cursor — add to the client's MCP config (e.g.
claude_desktop_config.json):
{
"mcpServers": {
"appsgolem": {
"command": "npx",
"args": ["-y", "appsgolem-mcp"],
"env": { "APPSGOLEM_API_KEY": "ag_live_…" }
}
}
}
Claude Code — one command:
claude mcp add appsgolem -e APPSGOLEM_API_KEY=ag_live_… -- npx -y appsgolem-mcp
The server speaks MCP over stdio (the transport those clients use). A
missing APPSGOLEM_API_KEY is not fatal at startup — the server still starts
and advertises its tools; each call then returns a clear config_error telling
you to set the key.
Configuration
| Env var | Required | Default | Notes |
|---|---|---|---|
APPSGOLEM_API_KEY | yes | — | Your ag_live_… key. |
APPSGOLEM_API_BASE | no | https://appsgolem.com | Override for self-host / dev. |
Pricing
1 produced clip = 1 credit. 2160p (4K) = 4 credits per clip — except
audio_only, which stays 1. A source longer than 2 h adds +1 once
per job, but only when its duration is known (the surcharge is skipped if the
probe can't determine it). A batch/stitch of N clips costs N per-clip.
Failed cuts are never billed.
Tools
The server exposes three tools. A call that passes MCP input-schema
validation returns a structured result — the API's own JSON on success, or
{ "error": … } on any handler/API failure — and never raises a protocol-level
error, so an agent always gets a usable object. (Invalid tool arguments are
rejected by the MCP SDK before the handler runs, as a text-only isError
result.)
1. cut_youtube_video
Cut a clip (or a batch of clips) from a YouTube video. By default it waits
until the clip is produced and returns its status (including a download_url
once a download token is ready); set wait: false to submit and return
immediately with the current job (its state is normally queued after dispatch).
Parameters
| Name | Type | Default | Notes |
|---|---|---|---|
url | string | — | Required. YouTube watch / share / youtu.be URL. Playlists are rejected. |
start | string | — | Clip start: "SS", "MM:SS", or "HH:MM:SS" (≤ 300 h). Omit when using clips. |
end | string | — | Clip end, same formats (≤ 300 h). Omit when using clips. |
resolution | string | 1080p | 144p · 240p · 360p · 480p · 720p · 1080p · 1440p · 2160p (4K; total cut ≤ 60 min). |
mode | string | video | video · audio_only · both · nosound · short · gif · frames (see Modes below). |
audio_format | string | — | The audio_only output format: mp3 · m4a · wav · flac (server defaults to mp3). both always produces MP3. |
bitrate | string | — | Lossy-audio bitrate 320 · 256 · 192 · 128 (default 320): MP3/M4A in audio_only, MP3 in both; ignored for WAV/FLAC. |
fast | boolean | false | Stream-copy (≈10× faster, keyframe-aligned); video / nosound / both only. Mutually exclusive with a non-1× speed — if both are set, fast wins and speed is forced to 1.0. |
speed | number | 1.0 | Playback speed 0.5 · 1 · 1.25 · 1.5 · 2. video / nosound / both / audio_only. |
interval_ms | integer | 2000 | frames sampling interval: 100 · 500 · 1000 · 2000 · 5000 · 10000 (non-sheet extraction is capped at 1,800 JPGs total across all clips). |
burn_ts | boolean | false | frames: burn the source timestamp onto each JPG. |
sheet | boolean | false | frames: return a single contact-sheet JPG (2–80 frames, single clip). Setting it disables burn_ts. |
clips | array | — | An array of 1–10 { start, end } ranges instead of start/end (an empty array is rejected). |
stitch | boolean | false | With 2+ clips, join them into one file (else a zip of clips); ignored for a single clip. video / audio_only / both / short / nosound. |
idempotency_key | string | — | A stable key (≤ 200 chars) so a retried request reuses the same job (sent as the Idempotency-Key header). |
wait | boolean | true | Poll until ready, up to the timeout_seconds polling deadline. |
timeout_seconds | integer | 300 | Polling deadline in seconds (default 300). It bounds the polling only — the initial submission and one in-flight status request (each up to a 30 s request timeout) can extend total wall-clock. |
Returns (wait: true, default) — the produced job status. download_url is
present once a download token is available; if it isn't yet, poll again:
{
"id": "e48db1a2-1c3d-4e5f-8a9b-0c1d2e3f4a5b",
"state": "produced",
"credits_reserved": 1,
"created_at": "2026-08-22T12:00:00+00:00",
"download_url": "https://appsgolem.com/v1/download/…/clip.mp4"
}
Returns (wait: false) — the job immediately, with its current state
(normally queued after dispatch) and no download_url yet; poll
get_cut_status with the id (or fetch poll_url):
{
"id": "e48db1a2-1c3d-4e5f-8a9b-0c1d2e3f4a5b",
"state": "queued",
"credits_reserved": 1,
"poll_url": "/v1/cuts/e48db1a2-1c3d-4e5f-8a9b-0c1d2e3f4a5b"
}
If the wait times out before the clip is ready, the result carries
"still_processing": true and the job id — poll get_cut_status with that
id. If the job reaches a terminal failure, the result is
{ "error": "cut_failed", "state": "failed" | "refunded", "id": … } (and no
credit is charged).
2. get_cut_status
Check a cut job by its id. Use it to poll a job started with
cut_youtube_video(wait=false) or one that timed out.
| Name | Type | Notes |
|---|---|---|
job_id | string | Required. The job id (a UUID) returned by cut_youtube_video. |
Returns — the job's state; once produced/delivered it also carries a
download_url when a download token is available (otherwise poll again):
{ "id": "e48db1a2-…", "state": "queued", "credits_reserved": 1, "created_at": "…" }
States progress accepted → queued → produced → delivered, or
failed → refunded on error.
3. get_account_balance
Return the API account's spendable credit balance and current hourly cap. No parameters.
Returns
{ "balance": 412, "hourly_cap": 60 }
Modes
mode | Output | Notable options |
|---|---|---|
video | Video file, no watermark — normally MP4; fast preserves the source container (e.g. WebM at high res) | resolution, fast, speed |
audio_only | mp3 / m4a / wav / flac | audio_format, bitrate, speed |
both | Video + MP3 together, as a zip (fast may preserve the video's source container) | bitrate, fast, speed |
nosound | Video with no audio track — normally MP4; fast preserves the source container | resolution, fast, speed |
short | Portrait 9:16 — AI smart-crop when applicable, else a letterbox-blur fallback whose exact aspect depends on the source (Shorts / Reels / TikTok) | resolution |
gif | Animated GIF (≤ 5 min; no multi-clip) | resolution |
frames | JPG stills | interval_ms, burn_ts, sheet |
Example prompts
Because the agent picks the parameters from your request, you drive it in plain language:
- "Cut 0:30 to 1:15 from https://youtu.be/dQw4w9WgXcQ in 1080p." →
cut_youtube_video(url, start="0:30", end="1:15") - "Grab the audio of that video from 2:00 to 5:00 as an mp3." →
mode="audio_only", audio_format="mp3" - "Make a vertical short of the 10:00–10:45 highlight." →
mode="short", start="10:00", end="10:45" - "Turn 0:05–0:12 into a GIF." →
mode="gif" - "Extract a contact sheet of frames every 5 seconds from 1:00 to 2:00." →
mode="frames", interval_ms=5000, sheet=true - "Stitch 0:10–0:20 and 1:00–1:10 into one clip." →
clips=[{start:"0:10",end:"0:20"},{start:"1:00",end:"1:10"}], stitch=true - "Do a fast, stream-copy cut of 0:00–0:30." →
fast=true - "How many API credits do I have left?" →
get_account_balance()
Result & error shapes
Every result from a handler is a plain object (MCP argument-validation
failures are the exception — see the Tools note above). On failure the object has an error code
(the tool call still succeeds):
error | When |
|---|---|
config_error | APPSGOLEM_API_KEY is missing. |
invalid_api_key | The key was rejected (401). |
invalid_job_id | job_id isn't a UUID. |
not_found | No such job for this account (404). |
cut_failed | The job reached failed/refunded (never billed). |
network_error | Connection/transport failure or request timeout. |
bad_request | The configured API base/path couldn't be built into a URL. |
http_error | A ≥400 response whose JSON body isn't an { error: … } object (carries status). |
bad_response | A success response whose body isn't a JSON object (array/scalar/null), or — with wait: true — a cut submission that came back without a usable job id. |
API-level errors — validation 400, 402 insufficient_credits,
404 not_found, 409 idempotency_conflict / duplicate_in_flight,
429 rate_limited, and 503 rate_limiter_unavailable / database_busy — are
returned as the API's own error body plus a status field. A 429 also
includes retry_after (seconds, from Retry-After) plus a reason
(submission_cap · request_rate · poll_rate) so an agent can tell which
limit it hit and back off correctly. A 503 is usually transient (retry after
a short delay), but a persistent 503 is a server-side problem to report — not
to retry indefinitely.
Status polling is not rate-limited for normal use — checking a job's
progress never counts against your submissions/hour cap. You never need to
throttle get_cut_status yourself: cut_youtube_video(wait=true) polls and
paces for you (honoring the server's poll_after cadence and any Retry-After),
so a single tool call handles even long jobs.
Produced download links expire after a bounded window (~72 hours by default on appsgolem.com; configurable per deployment) — fetch the file within it.
A relative download_url (the API returns a path) is resolved to a full URL
against the configured API base only when the result stays on that origin.
Any download_url that resolves off the API's origin — an absolute URL to
another host, or a relative path that escapes the origin — is dropped for
safety: the field is removed and the result carries download_url_dropped: true,
so you can tell it apart from a job that simply isn't produced yet. When
cut_youtube_video is waiting for you (wait=true, the default), an off-origin
link surfaces as a download_url_unsafe error instead of handing back a bad URL.
Develop
npm install
npm run build # tsc -> dist/
npm test # builds, then runs node --test (no network)
npm start # run the stdio server locally (key needed for calls, not startup)
Publishing
npm publish (from this directory) makes npx appsgolem-mcp work for everyone.
The prepare script builds dist/ automatically on install/publish.
