The MCP server runs inside the same process as the API and delegates every tool to the exact function the matching REST route calls, so a tool call and its REST equivalent behave identically - same validation, same lifecycle, same errors. What MCP adds is that the workflow becomes something an agent can complete on its own: find the footage, read what is in it, propose an edit, take a correction, and render.
Connecting#
One endpoint, and it is the only one:
https://anjin-media-api.fly.dev/mcp
Authenticate with the same mk_live_ key the REST API takes, as an Authorization header on every request. Keys are minted in the dashboard under Settings → API keys; see Authentication.
Claude Code
claude mcp add --transport http anjin-media \ https://anjin-media-api.fly.dev/mcp \ --header "Authorization: Bearer $ANJIN_API_KEY"
Add --scope project to write it into the repository's .mcp.json instead of your own machine's configuration.
Any streamable-HTTP client
Anything that speaks streamable HTTP and can set a header will connect. The shape most clients take:
{
"mcpServers": {
"anjin-media": {
"type": "http",
"url": "https://anjin-media-api.fly.dev/mcp",
"headers": {
"Authorization": "Bearer ${ANJIN_API_KEY}"
}
}
}
}Read the key from the environment where your client supports it, as above. A key pasted into a configuration file is a key that ends up in version control, and the only remedy for that is revoking it.
Checking it works
No client needed. A single request lists the tools, which is also the cheapest proof that your key resolves and carries at least one scope:
curl -sS https://anjin-media-api.fly.dev/mcp \
-H "authorization: Bearer $ANJIN_API_KEY" \
-H "content-type: application/json" \
-H "accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'event: message
data: {"result":{"tools":[{"name":"list_source_groups","description":"List the source groups …The thirteen tools#
Grouped by what you would use them for. The scope column is the scope that tool requires on your key - nothing else about a tool is gated.
Explore the footage
| Tool | Scope | What it does |
|---|---|---|
list_source_groups | read | The footage library, newest first, with camera_count and a duration on every row. Parameters: limit (1–100, default 50), cursor. |
get_source_group | read | One group in full, including each file and its own ingest status - the way to confirm a group has reached ready. Parameters: source_group_id. |
get_transcript | read | What was actually said, with word-level timing and speaker labels. Read this before writing a brief. Parameters: source_group_id, format (segments default, words, srt, vtt). |
get_speakers | read | The diarized labels on a group with talk time, segment counts and two sample quotes each - enough to tell people apart without watching. Parameters: source_group_id. |
Plan a cut
| Tool | Scope | What it does |
|---|---|---|
create_cut | write | The brief and the footage it may draw on. Returns immediately with status: "planning". Parameters: prompt (1–2,000 characters, not blank), source_group_ids (1–4), settings, brand_kit_id, auto_render, aspects. |
get_cut_plan | read | The plan document verbatim - every entry, its text and its source timecodes. The tool the loop turns on. Parameters: cut_id. |
update_cut_plan | write | Structural edits applied directly: no planner call, synchronous, and free. Parameters: cut_id, remove, reorder (a complete ordering of what remains), pin, unpin. |
revise_cut_plan | write | A plain-language instruction about the current plan. The cut returns to planning and a new version is written. Parameters: cut_id, instruction (1–500 characters, not blank). |
Render it
| Tool | Scope | What it does |
|---|---|---|
render_cut | write | Produces the finished files - one render per aspect. Its own description tells the agent this call spends. Parameters: cut_id, aspects (1–3), plan_version, crop_mode. |
get_renders | read | Every render on the cut with its status and, once complete, signed URLs for the video and its sidecars. Parameters: cut_id. |
delete_render | admin | Purges one rendered file and its sidecars from storage, then the row. The only tool that needs an admin key, and the only one that cannot be undone. Parameters: render_id. |
Manage
| Tool | Scope | What it does |
|---|---|---|
list_brand_kits | read | The kits a cut can carry: colours, caption style, logo and end-card assets. Parameters: limit (1–100, default 50), cursor. |
name_speakers | write | Give diarized labels display names, which then appear in every plan the planner writes afterwards. Parameters: source_group_id, names - a map of label to name, 1–60 characters each. |
What the schemas do not catch
Two refusals come from the service rather than the tool schema, so a call that looks valid to a client is still rejected:
Reading a cut's readiness
Readiness is status, never plan !== null: plan is null only before a cut's first plan is committed, and a cut revising back through planning hands back the previous version, fully populated. Wait on status reaching planned, or on a bumped plan_version - waiting on a non-null plan reads a revision as finished the instant it starts. The tool descriptions state this contract themselves.
The loop#
Four tools in order, and the order is the product. It is written into the tool descriptions themselves so an agent that never reads this page still follows it:
- 1
create_cut
The brief and one to fourreadysource groups. This returns an id andplanning, not a plan - the planner is still reading the transcripts. - 2
get_cut_plan
Poll untilstatusreadsplanned, then read every entry against the brief. This is the review step the whole loop exists for: the plan names the seconds of footage the edit is made of, and it is far quicker to correct than a finished file. - 3
revise_cut_plan - or update_cut_plan
Say what is wrong in a sentence and the planner writes a new version. For an edit you already know exactly - drop entry 4, swap 2 and 3 -update_cut_planapplies it directly with no planner call at all. Re-read the plan after either. - 4
render_cut
Only once the plan says what you want it to say. One to three aspects, one render each, and the files arrive throughget_renders.
The loop can run as many times as it takes. Every version is kept, and render_cut takes an explicit plan_version, so an agent can render the version a person approved rather than whatever is latest.
Scopes#
The connection is authenticated once and then each tool checks its own scope. That is a deliberate difference from the REST routes, where one scope gates the whole request: a single MCP connection multiplexes tools with different requirements, so gating the mount would stop a read-only key from doing the reads it is entitled to.
- Seven tools need
read. Everything that lists or fetches. - Five need
write.create_cut,revise_cut_plan,update_cut_plan,render_cutandname_speakers. - One needs
admin.delete_render, and nothing else on this surface.
A key without the scope a tool needs still sees that tool: tools/list returns all thirteen whatever your key carries, and the refusal happens at call time as a tool error. So a read-only key connects, explores the whole library, reads every plan, and cannot start anything.
Transport#
Streamable HTTP, and stateless. No session is established, no mcp-session-id is ever issued, and there is nothing to keep alive or resume: every request carries its own bearer key, which already identifies the caller. A client that reconnects has lost nothing, because there was nothing to lose.
One request per call
JSON-RPC batch arrays are refused. Send this:
[
{"jsonrpc":"2.0","id":1,"method":"tools/list"},
{"jsonrpc":"2.0","id":2,"method":"tools/list"}
]and the whole POST is rejected before the transport sees it:
{
"jsonrpc": "2.0",
"id": null,
"error": {
"code": -32600,
"message": "Batch requests are not supported - send one request per call (rate limiting is per-request)"
}
}The reason is the rate limiter: one HTTP request spends one token, and a batch would have driven any number of tool calls on that single token. Rejecting it keeps the published limits honest. Send one request per call and let your client pipeline them.
Rate limits
The same two buckets as REST apply here, counted per HTTP request: 60 a minute for the key and 120 a minute for the account across every key it holds. Every response - tool results and transport errors alike - carries x-ratelimit-remaining, and a refusal carries retry-after in whole seconds. An agent in a tight polling loop is the caller most likely to meet them: Errors & limits covers backing off properly, and the concurrency ceilings that sit behind them.
Errors#
A failing tool returns the same RFC 9457 problem document the REST API would have returned, serialised into the text content of a tool result with isError: true. Nothing is reworded on the way through - one code path builds both:
{
"type": "https://anjin.media/errors/insufficient_scope",
"title": "Insufficient Scope",
"status": 403,
"detail": "this API key lacks the required \"write\" scope",
"code": "insufficient_scope"
}So an agent can branch on code exactly as your backend would: 422 group_not_ready means wait for ingest, 409 busy means a planner run is already in flight, 404 not_found means the id is not yours.
{
"type": "https://anjin.media/errors/invalid_api_key",
"title": "Invalid Api Key",
"status": 401,
"detail": "Invalid API key",
"code": "invalid_api_key"
}Briefing an agent#
The tools are shaped so that plain instructions work. Three that do the right thing:
- Read first, then brief. “Look at my source groups, find the customer panel from last month, and read its transcript. Then draft a 60 second cut answering the data residency objection - open on the question as the audience puts it. Show me the plan before you render anything.”
- Argue with the plan. “That plan opens too slowly. Revise it to lead with the number, keep the second and fourth segments exactly as they are, and drop the pricing tangent. Then show me the new version.”
- Render, deliberately. “The plan is right. Render version 3 in 9:16 and 1:1, then give me the download links and tell me whether QC passed on both.”
The pattern in all three: read the footage before briefing against it, argue with the plan rather than the output, and make rendering an explicit instruction. An agent that renders first discovers what it got wrong in the most expensive place.
From here: Creating a composition documents every setting create_cut accepts, Composition results documents the plan document and the render sidecars an agent reads back, Working with an archive covers pointing an agent at a library rather than a single recording, and the MCP overview makes the case for the whole approach in fewer words.
