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