Docs / API and MCP

API and MCP

relay exposes the same read model three ways: this site, a small JSON API under /api/v1/, and an MCP server at /mcp. All three read through one data interface; none of them call a model.

Base URL and versioning

Base URL: https://relay.kyro.house/api/v1. The version is in the path. Changes follow the versioning and deprecation policy: additive changes ship in place, breaking changes get a new path segment, and retired operations announce themselves with Deprecation and Sunset headers (RFC 9745) before they go.

The OpenAPI 3.1 description is at /openapi.json and is listed in /.well-known/api-catalog (RFC 9727).

Authentication and scopes

Read endpoints work without credentials when the deploy enables public read (RELAY_PUBLIC_READ=1, the default for demo data). Otherwise, and for every write, send a bearer token:

Authorization: Bearer <token>

Scopes are named and granted per token:

Scope Grants
feed:read GET /api/v1/feed, /api/v1/projects, relay_get_feed, relay_get_project
memory:read relay_recall, relay_brief, relay_guidance, relay_feed, relay_project
memory:write relay_decide, relay_prefer
assets:read GET /api/v1/assets, GET /api/v1/assets/{id}, relay_asset_list, relay_asset_get
assets:write POST /api/v1/assets, finalize, publish, relay_asset_request_upload, relay_asset_finalize
assets:review PUT /api/v1/assets/{id}/review

Tokens are created with the CLI on a machine that holds the database connection; the web app never mints them, it only checks them:

pnpm relay token create --name ci-runner --scopes feed:read,assets:write   # prints the token once
pnpm relay token ls
pnpm relay token revoke ci-runner

Only the SHA-256 of a token is stored (tokens table). A deployment without a database can set RELAY_API_TOKENS instead (token:scope,scope;token2:scope), and RELAY_MEMORY_TOKEN is one shared secret that grants every scope. The bearer scheme and scopes are also published at /.well-known/oauth-protected-resource (RFC 9728).

Rate limits

Every API response carries structured rate-limit headers (IETF RateLimit header fields):

RateLimit-Policy: "default";q=60;w=60
RateLimit: "default";r=57;t=42

The default policy is 60 requests per 60 seconds per client. Over the limit you get 429 Too Many Requests with a Retry-After header and a problem document.

Errors

Errors are application/problem+json (RFC 9457):

{
  "type": "https://relay.kyro.house/docs/api#errors",
  "title": "Not Found",
  "status": 404,
  "detail": "No project with slug 'nope'.",
  "instance": "/api/v1/projects/nope"
}

Unknown paths under /api/v1/ return 404 in this shape, never an HTML shell.

Operations

GET /api/v1/info

operationId: getInfo. Name, version, description, the five rules, data mode, and links to every machine-readable surface.

curl -s https://relay.kyro.house/api/v1/info | jq .

GET /api/v1/feed

operationId: listFeed. Events newest first, sanitized: payload and dedupe_key are omitted. Query: project, kind, actor, lane, limit (1-200, default 50), before and after (ISO timestamps) for paging.

curl -s "https://relay.kyro.house/api/v1/feed?kind=step.usage&limit=5" | jq '.events[] | {ts, actor, model, cost_equiv}'

GET /api/v1/projects

operationId: listProjects. Every linked project with last activity, open issues, decision count and digest coverage.

GET /api/v1/projects/{slug}

operationId: getProject. One project with its current decisions, models and recent events.

Assets

The asset library (docs/ASSETS.md): media agents produce, stored privately, versioned, reviewed. Every view URL is signed and expires after an hour. Uploads need a database and a store (RELAY_DATA=db); the demo deployment answers 501 to uploads and serves mock assets.

Operation Scope Does
GET /api/v1/assets (listAssets) assets:read pending first, then newest; filter by project, status, kind, issue, pr
POST /api/v1/assets (createAsset) assets:write multipart file + metadata ingests in one call; JSON {"presign": true, "project", "filename"} reserves an id and returns a one-time upload URL
POST /api/v1/assets/{id}/finalize (finalizeAsset) assets:write after the presigned PUT: probe, thumbnail, create the row
GET /api/v1/assets/{id} (getAsset) assets:read metadata, the version chain, signed url, thumb_url, bundle_url
PUT /api/v1/assets/{id}/review (reviewAsset) assets:review {"decision": "approved" | "rejected", "note"}; a rejection note becomes a project decision and, with a GitHub token, reopens the issue
POST /api/v1/assets/{id}/publish (publishAsset) assets:write copy to the public Blob store and record a permanent URL (501 without one)

One-call upload:

curl -X POST https://relay.kyro.house/api/v1/assets -H "Authorization: Bearer $TOKEN" \
  -F file=@render.png -F project=atlas-site -F issue=118 -F title="Pricing hero" -F tags=ux,pricing

Presigned upload (bytes go straight to storage, never through the app or a model):

curl -s -X POST https://relay.kyro.house/api/v1/assets -H "Authorization: Bearer $TOKEN" -H "content-type: application/json" \
  -d '{"presign":true,"project":"atlas-site","filename":"walkthrough.mp4","issue":73}' > presign.json
curl -X PUT "$(jq -r .upload_url presign.json)" -H "content-type: video/mp4" --data-binary @walkthrough.mp4
curl -X POST "$(jq -r .finalize_url presign.json)" -H "Authorization: Bearer $TOKEN" -H "content-type: application/json" \
  -d "$(jq .finalize_body presign.json)"

Review:

curl -X PUT https://relay.kyro.house/api/v1/assets/<id>/review -H "Authorization: Bearer $TOKEN" -H "content-type: application/json" \
  -d '{"decision":"rejected","note":"Keep one accent hue; lower the chroma to match the light theme."}'

MCP server

https://relay.kyro.house/mcp speaks the MCP Streamable HTTP transport (POST JSON-RPC; stateless). It is the same server the CLIs talk to over stdio (relay-mcp): one tool set, gated by the scopes your token carries. Registry metadata is at /server.json and the server card at /.well-known/mcp/server-card.json.

Tool Scope What it does
relay_get_info public the same document as GET /api/v1/info
relay_get_methodology public the five rules and a task's life, as text
relay_get_feed feed:read events, with the same filters as the API
relay_get_project feed:read one project with decisions, models and recent events
relay_recall memory:read decisions and preferences relevant to a query
relay_brief memory:read the compiled brief for a project and intent
relay_guidance memory:read official vendor guidance for a model
relay_feed, relay_project memory:read compact text forms for a CLI session
relay_decide, relay_prefer memory:write record a decision; set a preference
relay_asset_list, relay_asset_get assets:read the library, and one asset with signed URLs
relay_asset_request_upload, relay_asset_finalize assets:write reserve an id and upload URL; turn the upload into an asset

With public read enabled (the demo default) the feed:read and memory:read tools answer without a token; everything else returns an error naming the missing scope.

Claude Code:

claude mcp add --transport http relay https://relay.kyro.house/mcp

CLI

The relay CLI lives in the repo (pnpm relay --help). First calls:

pnpm relay doctor          # what is installed, logged in, reachable
pnpm relay feed --limit 20 # the same feed, in the terminal
pnpm relay usage --window 7d