Throughline Docs
Pages · MCP

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://mcp.criticalperiod.co/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. An app connected by signing in reads what a production-wide key reads, unless the person who connected it is an outside company's seat: then it meets the door that company's engagement-bound key meets, and reads only the scene and character bundles, for what the seat was given (a sign-in reads no turnovers and cannot be granted read:money). 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. Two ways in, and the metadata names both: an OAuth flow against the authorization server at api.criticalperiod.co (dynamic client registration, PKCE — connect from any MCP-aware client and sign in as yourself), or a bearer access key minted in Settings, presented in the header:

{ "resource": "https://mcp.criticalperiod.co/mcp",
  "bearer_methods_supported": ["header"],
  "authorization_servers": ["https://api.criticalperiod.co"] }

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

Signed in as yourself, you read what Throughline shows you. Connect with the OAuth flow and every tool answers for you. get_schedule gives the days you can see and would be sent (on an episodic show, the days your episodes shoot), and each day lists only the scenes you can see. get_asset won't open an item you can't open. The lists match the app: list_assets and list_activity still show an item you can't open by name, the way your item list and activity feed do, but the item itself stays closed. Someone from an outside company connected this way reads the days, deliveries and approvals they were given, plus the two bundles for what they were given. An access key reads the production-wide answer, as in the table.

An app connected with your own sign-in can't send turnovers or read purchase orders, and asked to, it says so plainly: "You can't send turnovers from a connected app." (A key that holds submit:turnover can send them.)

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_turnovers—list_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.