REST API Surface
Every per-DCC server and the multi-DCC gateway expose the same /v1/* REST surface, alongside their MCP endpoint. This page is the integration contract for traditional callers (cURL, CI pipelines, studio automation, non-MCP tooling) — anything that can speak HTTP can drive a DCC through these routes without touching the MCP protocol stack.
Relationship to MCP — Gateway MCP's
call_tool/describe_tool/search_toolswrappers route through the same code path as the REST endpoints. Choosing MCP vs REST is a transport decision, not a feature decision; the envelopes are identical.
Endpoints
| Method | Path | Purpose |
|---|---|---|
GET | /v1/healthz | Liveness probe. 200 {"status": "ok"} as long as the HTTP handler is up. |
GET | /v1/readyz | Three-state readiness: 200 Ready / 503 Booting / body omitted Unreachable (see below). |
GET | /v1/skills | Flat listing of loaded tools, deterministically sorted. |
POST | /v1/search | Fuzzy / exact search across loaded + unloaded skills. |
POST | /v1/load_skill | Load a discovered skill without using MCP tools/call. |
POST | /v1/unload_skill | Unload a skill without using MCP tools/call. |
POST | /v1/describe | Return the full input schema + annotations for one tool_slug. |
GET | /v1/tools/{slug} | Alias of /v1/describe (read-only lookup via URL). |
POST | /v1/call | Invoke a tool by slug. This is the canonical invocation plane. |
POST | /v1/call_batch | Gateway only: invoke up to 25 ordered tool calls with optional stop_on_error. |
GET | /v1/context | Scene / document snapshot (per-DCC or gateway-aggregated). |
GET | /v1/resources | MCP-style resource list. |
GET | /v1/resources/{uri} | Read one percent-encoded resource URI. |
GET | /v1/resources/{uri}/events | Server-Sent Events for resource changes. |
GET | /v1/prompts | MCP-style prompt template list. |
GET | /v1/prompts/{name} | Render one prompt; pass JSON object arguments in ?args=.... |
GET | /v1/jobs/{id}/events | Server-Sent Events for one async job. |
DELETE | /v1/jobs/{id} | Cancel one async job. |
GET | /v1/openapi.json | Auto-generated OpenAPI 3.x document for code-gen clients. |
The gateway exposes the same paths as an aggregating facade. Gateway capability slugs use <dcc>.<id8>.<tool> and are obtained from POST /v1/search; direct per-DCC REST slugs use <dcc>.<skill>.<action> and do not include an instance id prefix.
POST /v1/call — the invocation contract
Request body
{
"tool_slug": "maya.a1b2c3d4.create_sphere",
"arguments": { "radius": 2.0, "segments": 32 },
"meta": { "progressToken": "session-42" }
}| Field | Required | Notes |
|---|---|---|
tool_slug | ✅ | Gateway: <dcc>.<id8>.<tool>. Direct per-DCC REST: <dcc>.<skill>.<action>. Get valid slugs from POST /v1/search or GET /v1/skills — do not construct them by hand. |
arguments | ❌ | Canonical tool-specific input, matching MCP tools/call. Missing / null / empty string normalizes to {}; JSON objects are used as-is; JSON strings that parse to objects are accepted for wrapper compatibility; arrays, booleans, numbers, and non-object strings are rejected. |
params | ❌ | Backward-compatible alias for arguments. Prefer arguments in new clients so REST and MCP examples stay identical. |
meta | ❌ | MCP-style sidecar. Missing / null normalizes to absent. If provided, it must be an object (or an object-shaped JSON string). Honored keys: progressToken, dcc.async, dcc.wait_for_terminal. |
The canonical normalization rules live in dcc-mcp-wire; Python host wrappers can reuse them via dcc_mcp_core.host.normalize_tool_arguments() and normalize_tool_meta() instead of hand-rolling JSON coercion.
Wrapper payloads and object-shaped arguments
When a host (Maya, Blender, Houdini…) or a connector (Zapier, n8n, a CI runner) wraps the gateway call surface, the inner payload passed to call_tool / call_tools MUST remain a single JSON object with:
tool_slug— a string (e.g."maya.a1b2c3d4.create_sphere")arguments— omitted for no-arg tools, or a JSON object{}meta(optional) — a JSON object{}
Backend-specific fields such as code, script, file_path, or radius belong inside arguments, never at the wrapper top level.
✅ Correct payload (object-shaped arguments)
{
"tool_slug": "maya.a1b2c3d4.create_sphere",
"arguments": {
"radius": 2.0,
"segments": 32
},
"meta": {
"progressToken": "session-42"
}
}❌ Incorrect payloads (common failure modes)
1. Backend fields placed at the wrapper top level
{
"tool_slug": "maya.a1b2c3d4.execute_python",
"code": "cmds.polySphere()"
}Fix: move tool input under arguments: { "tool_slug": "...", "arguments": { "code": "..." } }.
2. Non-object JSON such as arrays, booleans, or numbers
{
"tool_slug": "maya.a1b2c3d4.create_sphere",
"arguments": ["radius", 2.0]
}Error you'll see:
Validation error: document root must be an objectWhy? The server validates arguments against the tool's JSON Schema, whose root is an object.
3. Missing arguments for no-arg tools
{
"tool_slug": "maya.a1b2c3d4.list_scene"
}This is now valid and normalizes to {}. Explicit "arguments": {} is still recommended in examples because it makes wrapper intent obvious.
4. Double-stringified payload (wrapper serializes twice)
# ❌ WRONG: serializing the entire payload twice
import json
payload = {
"tool_slug": "maya.a1b2c3d4.create_sphere",
"arguments": json.dumps({"radius": 2.0}) # becomes a string
}
requests.post(url, json=payload) # serializes again → double-stringified# ✅ CORRECT: pass objects directly
import json
payload = {
"tool_slug": "maya.a1b2c3d4.create_sphere",
"arguments": {"radius": 2.0} # object, not string
}
requests.post(url, json=payload) # serializes once → correctTesting wrapper payloads end-to-end
Validate locally with
jqor a JSON schema validator:bashecho '$PAYLOAD' | jq .arguments # must be an object `{}`, not a stringCall
POST /v1/describefirst to fetch the tool's schema, then validate yourargumentsagainst it.Enable audit logging (
DCC_MCP_GATEWAY_AUDIT_DIR) and inspect the JSONL rows:call.request.arguments— must be an object, not a string.call.error— if present, check whether it mentions "document root must be an object".
Test with parsed objects in your wrapper:
python# ✅ GOOD: parse the JSON response before passing to the next layer response = requests.post(url, json=payload) result = response.json() # parse once process(result) # pass parsed object
MCP tools/call equivalent
When calling via MCP (not REST), the same rule applies:
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "maya.a1b2c3d4.create_sphere",
"arguments": {"radius": 2.0, "segments": 32} // ✅ object, not string
}
}Remember: MCP params.arguments is omitted or a JSON object {}, never an array/number/boolean.
Success response — 200 OK
{
"slug": "maya.a1b2c3d4.create_sphere",
"output": { "sphere_id": "pSphere1" },
"validation_skipped": false,
"request_id": "req-7f3c..."
}slug always echoes the slug the caller used so clients can correlate batched dispatches without threading request IDs through their own bookkeeping.
Error response — structured, kebab-cased
{
"kind": "unknown-slug",
"message": "no action registered for slug 'maya.a1b2c3d4.make_sphere'",
"hint": "call /v1/search to list available tools",
"request_id": "req-7f3c...",
"candidates": ["maya.a1b2c3d4.create_sphere"]
}Error-kind vocabulary (HTTP status in parentheses):
unknown-slug(404) — no action matched;candidatesmay carry suggested slugs.ambiguous(409) — slug matched multiple actions;candidateslists all of them.skill-not-loaded(409) — slug is valid but the owning skill isn't loaded. Call MCPload_skillfirst; on the gateway you may target a backend withinstance_idordcc.invalid-params(400) — JSON-Schema validation failed against normalizedarguments.unauthorized(401) — theAuthGaterejected the request. Defaults to localhost-only on per-DCC servers; installBearerTokenGatefor remote access.not-ready(503) —/v1/readyzis red; DCC is still starting up.affinity-violation(409) — the caller tried to invoke a main-thread tool from a worker thread.bad-request(400) — malformed envelope (missingtool_slug, bad JSON, etc.).backend-error(502) — the owning DCC process responded but the tool failed.instance-offline(503) — gateway only — the<id8>prefix resolves to an instance that is no longer live.schema-unavailable(502) — gateway only — the owning DCC stopped answeringtools/listbetween discovery and call.internal(500) — the REST layer itself failed; check server logs.
Request ID
Every request gets a request_id (client-supplied X-Request-Id header wins, otherwise the server generates one). The id flows into the audit log, the response envelope, and the MCP _meta.request_id field on the gateway so MCP and REST callers can trace the same unit of work.
POST /v1/call_batch — gateway ordered batches
/v1/call_batch is the REST twin of the gateway MCP call_tools wrapper. Use it when an agent must execute several backend tools in a known order without paying one HTTP/MCP round-trip per step.
{
"calls": [
{ "tool_slug": "maya.a1b2c3d4.create_sphere", "arguments": { "radius": 2.0 } },
{ "tool_slug": "maya.a1b2c3d4.assign_material", "arguments": { "name": "mat_blue" } }
],
"stop_on_error": true
}Rules:
callsis required and capped at 25 entries.- Each entry uses the same
tool_slug/arguments/metawrapper shape asPOST /v1/call; missingargumentsnormalizes to{}. stop_on_error: truestops at the first failed call.falseexecutes all calls and returns per-call success/error envelopes.- Preserve response order to correlate results with the request array; do not infer order from request ids.
POST /v1/search
{
"query": "render",
"dcc_type": "maya",
"tags": ["batch"],
"loaded_only": false,
"limit": 20,
"mode": "fuzzy"
}query(required) — free-text.mode: "fuzzy"(default) uses a nucleo-matcher-backed scorer with typo / prefix tolerance;mode: "exact"falls back to the pre-#659 substring table.dcc_type,tags,loaded_only— progressive filters.loaded_only = falsesurfaces unloaded skills as search hits so agents can discoverload_skillcandidates.limit— the server enforces a ~512 B/hit token budget so search stays cheap for large catalogues.
Response shape (gateway + per-DCC are identical):
{
"total": 3,
"hits": [
{
"slug": "maya.a1b2c3d4.render_frame",
"skill_name": "maya-render",
"action_name": "render_frame",
"dcc": "maya",
"tags": ["batch"],
"loaded": false,
"next_step": {
"action": "load_skill",
"arguments": {
"skill_name": "maya-render",
"dcc": "maya",
"dcc_type": "maya",
"instance_id": "a1b2c3d4-0000-0000-0000-000000000001"
},
"rest": {"method": "POST", "path": "/v1/load_skill"},
"mcp": {"name": "load_skill"}
}
}
]
}When loaded=false, clients may POST next_step.arguments directly to /v1/load_skill, then repeat /v1/search or call /v1/describe for the same tool. Per-DCC REST omits instance_id because there is only one owning server; the gateway includes it so same-DCC multi-instance calls stay routed.
POST /v1/load_skill and /v1/unload_skill
{ "skill_name": "maya-render", "dcc": "maya", "instance_id": "a1b2c3d4" }skill_name is required. Gateway callers should pass the dcc / dcc_type and instance_id returned in /v1/search.next_step.arguments when more than one DCC instance is live. A successful load refreshes the gateway capability index, so the next /v1/search sees the newly callable actions.
POST /v1/describe
{ "tool_slug": "maya.a1b2c3d4.render_frame", "include_schema": true }Returns a record (compact capability descriptor) plus the full tool definition (input_schema, annotations, next-tool hints). include_schema defaults to true; set false on follow-up calls where only the metadata has changed.
Resources, prompts, and jobs over REST
GET /v1/resources returns {total, resources, request_id} where each record mirrors MCP's resource definition shape. To read a resource, percent-encode the entire URI into the path segment:
curl -s 'http://127.0.0.1:8765/v1/resources/scene%3A%2F%2Fcurrent'GET /v1/prompts returns {total, prompts, request_id}. Render one template with GET /v1/prompts/{name}; when the prompt declares arguments, pass a JSON object in the args query parameter. The gateway uses this same REST hop when serving MCP prompts/get, so prompt arguments are preserved end-to-end.
Async jobs can be watched with GET /v1/jobs/{id}/events and cancelled with DELETE /v1/jobs/{id}. Resource subscriptions use GET /v1/resources/{uri}/events. Both event endpoints are Server-Sent Events.
Three-state readiness (GET /v1/readyz)
| State | HTTP | Body | Meaning |
|---|---|---|---|
Ready | 200 | { "status": "ready", "process": "ok", "dispatcher": "ok", "dcc": "ok" } | Every readiness bit is green; POST /v1/call will dispatch. |
Booting | 503 | { "status": "booting", ... which bits are red } | The server is up but the DCC host or dispatcher hasn't finished initialising. The gateway keeps the instance's registry row but won't route traffic there. |
Unreachable | no response | — | /v1/readyz didn't answer in 5 s. Gateway falls back to GET /health for pre-#660 backends; still nothing → the instance is counted as a probe failure and deregistered after 3 consecutive misses. |
The distinction matters: "my tool disappeared" has two very different diagnoses and fixes. Surface process, dispatcher, dcc separately in your dashboards.
Envelope parity with MCP
| Concern | MCP call_tool (JSON-RPC) | REST POST /v1/call | Parity? |
|---|---|---|---|
| Success body | result.content[].text (str JSON) | {slug, output, validation_skipped, request_id} | ✅ same underlying CallOutcome |
| Error body | result.content[].text (str JSON) with isError: true | {kind, message, hint?, request_id, candidates?} | ✅ same ServiceError; MCP wraps it into the MCP CallToolResult shape |
request_id | _meta.request_id | top-level field | ✅ same value |
| Cancellation | notifications/cancelled | DELETE /v1/jobs/{id} for async jobs | ✅ both reach the cooperative-cancellation path |
| Progress/job events | notifications/progress and notifications/$/dcc.jobUpdated | GET /v1/jobs/{id}/events SSE | ✅ job lifecycle is available over both transports |
| Resource updates | notifications/resources/updated after resources/subscribe | GET /v1/resources/{uri}/events SSE | ✅ resource updates are available over both transports |
The contract is locked down by OpenAPI snapshot tests in crates/dcc-mcp-skill-rest/src/openapi.rs (call_request_schema_contract_is_stable, call_outcome_schema_contract_is_stable). Any change to these tests indicates a downstream-visible envelope break.
When to choose REST vs MCP
| You are … | Prefer |
|---|---|
| Writing an AI agent (Claude, Cursor, ChatGPT desktop, custom) | MCP. Use search_tools / describe_tool / call_tool against the gateway; get streaming events, progressive discovery, and the MCP capability registry. |
| Writing a cURL script / cron job / CI pipeline | REST. Pure HTTP + JSON, no MCP library required. |
| Writing an enterprise backend that talks to many DCCs | REST on the gateway. Single endpoint, same envelope across all DCCs, OpenAPI doc for code generation. |
| Writing an in-host plugin (Maya plug-in, Blender add-on) | Neither — call DccServerBase.register_* directly. REST / MCP are for external callers. |
| Debugging "why didn't my tool run?" | REST first: curl /v1/healthz then /v1/readyz then /v1/search. The three endpoints give you a straight line from "is the process alive" to "is my tool discoverable". |
Pluggable traits (for embedders)
The REST surface is composed of five traits, every one of them swappable. Defaults work out of the box for localhost-only development; production deployments replace the ones that matter to them.
| Trait | Default | Common overrides |
|---|---|---|
SkillCatalogSource | Live SkillCatalog | A test fixture that returns a canned action list; a read-through cache against a remote registry. |
ToolInvoker | DispatcherInvoker over ToolDispatcher | A queue-backed invoker that posts jobs onto the DCC main thread via QueueDispatcher. |
AuthGate | AllowLocalhostGate | BearerTokenGate::new(vec![token]) for remote access; a studio SSO gate for enterprise. |
AuditSink | NoopSink | A FileAuditSink that appends JSONL rows; a Kafka producer for central audit. |
ReadinessProbe | Static Ready | A probe that checks the DCC host's own ready signal and goes red while the scene is loading. |
This is the DIP story for the REST surface: handlers depend on the trait, not the concrete type, so you never touch handle_call to plug in custom auth / audit / invocation.
Next reads
- Gateway diagnostics — how to read contention, probes, election, and ghost eviction.
- CLI reference — launching per-DCC servers, tunnel relay + agent.
- AGENTS.md — rules for AI agents.