Throughline Docs

MCP server

POST /mcp is live — an MCP (Model Context Protocol) server onto the REST API's own surface: the same Throughline access keys, the same scopes, the same clamped answers. It isn't a second API. The MCP layer resolves your bearer key and calls the identical function the matching REST endpoint calls — nothing about what a key may see changes depending on which door you come through, and a refusal on one is worded and coded identically on the other.

Endpoint

POST https://throughline-backend-idkf.onrender.com/mcp
Authorization: Bearer tl_your_access_key_here
Content-Type: application/json

Streamable HTTP: one JSON-RPC 2.0 message per request body, one transport, no SSE fallback, no batching — send a single JSON-RPC object, not an array. A request body over 12MB is refused before it's even parsed.

Authentication

Whatever your key can do over REST, it can do here, and nothing more. A production-wide key reads and writes across the whole production, scoped by read/write; an engagement-bound vendor key reaches only the bundle/turnover/media/purchase-order tools, clamped to its own engagement. Revoking a key from Settings bites on the very next call, MCP included.

Two tiers of method:

  • initialize, tools/list, resources/list, prompts/list, prompts/get — any live key. These only describe what exists, the same way an OpenAPI document describes routes it doesn't let you call.
  • tools/call, resources/read — need the specific scope the equivalent REST endpoint needs. A missing scope, an engagement door, or a tenancy-crossed id refuses with the identical HTTP status and message the REST call would give — see Errors below.

Discovery

GET /.well-known/oauth-protected-resource

Public, no key required. Protected-resource metadata (RFC 9728) so an MCP/OAuth-aware client can register against this resource ahead of time. There's no OAuth authorization server behind this yet — a bearer key minted in Settings is the auth — so the response names no authorization_servers:

{ "resource": "https://throughline-backend-idkf.onrender.com/mcp",
  "bearer_methods_supported": ["header"] }

Tools

Fifteen tools — the REST read surface, both bundles, turnovers and turnover submissions. The ten plain-read ones refuse an engagement-bound key outright, identically to their REST endpoints; the five marked engagement-aware work for either shape of key — unclamped for a production-wide key, clamped to its own reach for a vendor key.

REST endpoints with no tool yet: /script-changes, /people, /jobs, the two /media reads, and the three write endpoints — POST /assets, POST /assets/{node_id}/notes and POST /approvals/{id}/decide.

ToolScope neededSame as
get_productionreadGET /production
list_departmentsreadGET /departments
get_department (slug)readGET /departments/{slug}
list_scenesreadGET /scenes
list_assets (department?, kind?, status?)readGET /assets
get_asset (node_id)readGET /assets/{node_id}
get_schedule (episode_id?)readGET /schedule
list_deliveriesreadGET /deliveries
list_approvals (status?)readGET /approvals
list_activity (limit?)readGET /activity
get_purchase_ordersread:money, engagement-awareGET /purchase-orders
get_scene_bundle (node_id)read, engagement-awareGET /bundles/scene/{node_id}
get_character_bundle (node_id)read, engagement-awareGET /bundles/character/{node_id}
list_turnoversread, engagement-awareGET /turnovers
submit_turnover (delivery_id, title?, note?, files)submit:turnover, engagement-awarePOST /turnovers/{id}/submissions

submit_turnover's files travel as base64 — JSON-RPC has no multipart — one object per file: {"filename": "shot_010.mov", "content_base64": "…"}. For a large upload, the REST endpoint is the better choice: it streams the bytes instead of holding the whole request as one JSON string in memory.

Search is deliberately not a tool here. Every tool above is one /api/v1 endpoint's own function, called unchanged — and the in-product search screen has no /api/v1 endpoint behind it. This layer never invents a surface REST doesn't already expose, which is the same rule that keeps it from inventing an authorization decision.

Resources

Six stable documents — the singleton reads a client wants as ambient context rather than a call with arguments. Each needs read, same door as its matching tool above.

URISame as
throughline://productionget_production
throughline://departmentslist_departments
throughline://scheduleget_schedule
throughline://scriptlist_scenes
throughline://deliverieslist_deliveries
throughline://approvalslist_approvals

Prompts

Four canned questions — pure text templates with no data access of their own, telling a client's model which tool to call and how to read the answer.

PromptArgumentsPoints at
scene_statusnode_idget_scene_bundle
pending_turnoverslist_turnovers
whats_changedlimit?list_activity
how_to_submit_workdelivery_idsubmit_turnover

Example session

POST /mcp
{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}}
{"jsonrpc": "2.0", "id": 1, "result": {
  "protocolVersion": "2025-06-18",
  "capabilities": {"tools": {}, "resources": {}, "prompts": {}},
  "serverInfo": {"name": "throughline", "version": "1.0.0"}
}}
POST /mcp
{"jsonrpc": "2.0", "id": 2, "method": "tools/call",
 "params": {"name": "get_production", "arguments": {}}}
{"jsonrpc": "2.0", "id": 2, "result": {
  "content": [{"type": "text", "text": "{\n  \"id\": 1,\n  \"title\": \"Example Production\", …\n}"}],
  "structuredContent": {"id": 1, "title": "Example Production", "…": "…"},
  "isError": false
}}

A tool's own document rides twice in the result: as a text block (always present) and, when the underlying value is an object rather than a list, a second time as structuredContent.

Errors

Two shapes, deliberately different:

  • Auth, scope, engagement-door and tenancy refusals are REST's own bytes, unwrapped — the whole HTTP response is the identical status code and {"detail": "…"} body the matching REST call would give.
  • JSON-RPC's own protocol errors — an unknown method, an unknown tool/resource/prompt name, a malformed argument — get a JSON-RPC error object instead, because there's no REST call for those to match:
error.codeMeaning
-32700Parse error — the body isn't valid JSON
-32600Invalid Request — missing method, or a batched array (not supported)
-32601Method, tool, resource, or prompt not found
-32602Invalid params — a required argument is missing or the wrong shape

For example, calling submit_turnover with a key that lacks the submit:turnover scope returns a plain HTTP 403 with {"detail": "This key does not have the 'submit:turnover' scope."} — not a JSON-RPC error envelope, and the identical body POST /v1/turnovers/{id}/submissions would give the same key.