Anjin Media
Docs navigation

Docs

MCP & agents

The same service as the REST API, exposed as thirteen tools an agent can call. One endpoint, one bearer key, per-tool scopes - and a plan an agent has to read before it can render anything.

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

shell
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:

.mcp.json
{
  "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
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"}'
200 OK · text/event-stream
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

ToolScopeWhat it does
list_source_groupsreadThe footage library, newest first, with camera_count and a duration on every row. Parameters: limit (1–100, default 50), cursor.
get_source_groupreadOne 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_transcriptreadWhat 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_speakersreadThe 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

ToolScopeWhat it does
create_cutwriteThe 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_planreadThe plan document verbatim - every entry, its text and its source timecodes. The tool the loop turns on. Parameters: cut_id.
update_cut_planwriteStructural edits applied directly: no planner call, synchronous, and free. Parameters: cut_id, remove, reorder (a complete ordering of what remains), pin, unpin.
revise_cut_planwriteA 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

ToolScopeWhat it does
render_cutwriteProduces 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_rendersreadEvery render on the cut with its status and, once complete, signed URLs for the video and its sidecars. Parameters: cut_id.
delete_renderadminPurges 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

ToolScopeWhat it does
list_brand_kitsreadThe kits a cut can carry: colours, caption style, logo and end-card assets. Parameters: limit (1–100, default 50), cursor.
name_speakerswriteGive 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. 1

    create_cut

    The brief and one to four ready source groups. This returns an id and planning, not a plan - the planner is still reading the transcripts.
  2. 2

    get_cut_plan

    Poll until status reads planned, 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. 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_plan applies it directly with no planner call at all. Re-read the plan after either.
  4. 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 through get_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_cut and name_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:

a batched request body
[
  {"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:

400 Bad Request
{
  "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:

tool result content · isError: true
{
  "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.

401 Unauthorized · application/problem+json
{
  "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.