Skip to content

JSON-RPC API

The HTTP control plane (daedalus.api) exposes the in-process training API over a single JSON-RPC 2.0 endpoint. It is a thin FastAPI app (src/daedalus/api/app.py) whose handlers dispatch into the shared METHOD_HANDLERS table in src/daedalus/api/methods.py — the same table the MCP server uses, so the two surfaces can never drift.

Endpoints

MethodPathPurpose
POST/rpcJSON-RPC 2.0 — single request or a batch array
GET/healthzLiveness check — returns {"status": "ok"}

The app intentionally disables the OpenAPI docs UIs (docs_url=None, redoc_url=None): the JSON-RPC contract is the interface.

Starting the server

bash
uv run daeda serve-api --host 127.0.0.1 --port 8000

Both flags are optional (defaults shown). The server runs under uvicorn; the /rpc handler is synchronous and Starlette runs it in a threadpool, so the blocking in-process / network JSON-RPC handlers never stall the event loop.

No auth yet

The server has no authentication or authorization. Bind it to a trusted network boundary only (127.0.0.1 by default); do not expose it publicly as-is.

Methods

The method names come straight from METHOD_HANDLERS:

MethodParamsReturns
catalog.list_views(none)One row per feature view (name, entity, join keys, feature count, source)
catalog.show_viewnameA view's schema: timestamp field, source, fields with dtypes
service.list(none)One row per feature service (name, version, owner, grain, column count, serving flag)
service.shownameA service's full column contract (kind, dtype, default, nullable)
pipeline.compileserviceThe compiled operator pipeline: operators + a graph of nodes/edges
runs.materialize_partitionservice, partitionTriggers a daily training partition; returns {run_id, status}
runs.getrun_idRun status, partition, start/end times, metadata
runs.listservice?, limit?Recent runs (default limit 20)
runs.logsrun_id, tail?Run log lines
assets.lineage(none)Asset lineage graph (nodes + edges)

The catalog.*, service.*, and pipeline.compile methods resolve and compile in-process. The runs.* and assets.lineage methods delegate to a Dagster runs backend: a DagsterGraphQLRunsBackend when DAGSTER_GRAPHQL_URL (or ApiContext.dagster_graphql_url) is set, otherwise an ephemeral in-process InProcessDagsterRunsBackend. See Dagster Orchestration.

Example request

bash
curl -s http://127.0.0.1:8000/rpc \
  -H 'Content-Type: application/json' \
  -d '{
        "jsonrpc": "2.0",
        "method": "catalog.list_views",
        "params": {},
        "id": 1
      }'
json
{
  "jsonrpc": "2.0",
  "result": [
    {
      "name": "user_likes",
      "entity": "user",
      "join_keys": ["user_id"],
      "n_features": 6,
      "source": "likes"
    }
  ],
  "id": 1
}

Compiling a service to its operator DAG:

bash
curl -s http://127.0.0.1:8000/rpc \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","method":"pipeline.compile","params":{"service":"dssm_ranking"},"id":2}'

A request with id omitted is a JSON-RPC notification: it is processed but produces no response body (HTTP 204 No Content).

Error responses

Errors use standard JSON-RPC error objects, {code, message, data?}:

CodeMeaningRaised when
-32600Invalid RequestPayload is not a valid JSON-RPC request object
-32601Method not foundmethod is not in METHOD_HANDLERS
-32602Invalid paramsMissing/invalid params, or an unknown view/service/run
-32000Internal errorUnexpected handler failure (server-side)
json
{
  "jsonrpc": "2.0",
  "error": {
    "code": -32601,
    "message": "Method not found",
    "data": { "method": "catalog.no_such_method" }
  },
  "id": 3
}

CORS

Cross-origin access is controlled by the DAEDALUS_API_CORS_ORIGINS environment variable — a comma-separated allow-list. It defaults to * (permissive, for local development); lock it down in production:

bash
DAEDALUS_API_CORS_ORIGINS="https://ui.example.com,https://admin.example.com" \
  uv run daeda serve-api --host 0.0.0.0 --port 8000

The GUI planned in a separate stack consumes exactly this contract — see Platform overview.

See also