---
title: 'WarpEngine Admin API'
url: 'https://wiki.teletypegames.org/engines/warp-engine/admin-api'
markdown: 'https://wiki.teletypegames.org/engines/warp-engine/admin-api.md'
lang: en
date: '2026-09-25'
description: 'Since WarpEngine 1.0 everything the ActiveAdmin resources do is also a JSON API under /api/admin/v1: the catalog with its releases, assets, links and images, the status and category vocabulary, platform links, download statistics, pipelines, the artifact store and the application tokens. It is off…'
taxonomy:
  tag:
    - engine
    - reference
---

# WarpEngine Admin API

> Since [WarpEngine](https://wiki.teletypegames.org/engines/warp-engine) 1.0 everything the ActiveAdmin resources do is also a JSON API under `/api/admin/v1`: the catalog with its releases, assets, links and images, the status and category vocabulary, platform links, download statistics, pipelines, the artifact store and the application tokens. It is off until the host turns it on, it describes itself in an OpenAPI 3.1 document, and it can serve the same operations to LLM agents as an MCP server. The read side for catalog readers stays [WECP](https://wiki.teletypegames.org/engines/warp-engine/wecp).

# Turning it on

```ruby
# config/initializers/warp_engine.rb
WarpEngine.configure do |c|
  c.admin_api.enabled    = true
  c.admin_api.mcp        = true                           # needs gem "mcp" in the Gemfile
  c.admin_api.rate_limit = { to: 300, within: 1.minute }  # per token
end
```

| Key | Default | Meaning |
|---|---|---|
| `enabled` | `false` | Off: every Admin API address answers 404 |
| `token_owner_class` | `application_token_owner_class` | The model whose tokens may hold admin scopes |
| `authenticator` | `:application_token` | Or a callable that turns a request into a `WarpEngine::AdminApi::Principal` |
| `resources` | `:all` | Or a list, for example `%i[softwares releases downloads]`; the rest answers 404 |
| `read_only` | `false` | `true`: every write answers 405 and leaves the document |
| `page_size` / `max_page_size` | `50` / `200` | Paging of the lists |
| `rate_limit` | `nil` | `{ to:, within: }` per token, counted in `Rails.cache`; 429 with `Retry-After` past it |
| `openapi_requires_auth` | `true` | `false`: the OpenAPI document is public |
| `docs_ui` | `false` | `true`: a Redoc page at `/api/admin/v1/docs` (follows the document's auth rule) |
| `mcp` | `false` | `true`: MCP at `/api/admin/v1/mcp` |
| `mcp_exclude` | `application_tokens`, `device_grants` | Resources MCP leaves out |

On teletypegames.org the Admin API and MCP are on since 1.0.0, with AdminUser tokens.

# Tokens and scopes

```
Authorization: Bearer 3f9c…
```

An [application token](https://wiki.teletypegames.org/engines/warp-engine) issued in the admin (App Tokens → Admin API scope) or through the API itself. Three scopes, none implying another:

| Scope | Opens |
|---|---|
| `admin:read` | every GET |
| `admin:write` | every create, change, delete, trigger, sync and file operation |
| `admin:tokens` | the application token and device sign-in resources, reading included |

Only a token whose owner is the admin owner class may hold them — a publisher's token never becomes an admin token. A token can grant no admin scope it lacks, no `unrestricted`, and no expiry past its own; it cannot rotate a token wider than itself either, since rotation hands over the new secret. It may issue publishing tokens (`update`, `upload`) and client tokens (`catalog`).

# Shape

- JSON, camelCase keys in bodies, answers and query parameters (`perPage`, `needsReview`).
- A list: `{ "data": [ … ], "meta": { "page", "perPage", "total", "totalPages", "nextPage" } }`; `page` and `perPage` choose, `sort=title` or `sort=-createdAt` orders.
- One record: `{ "data": { … } }`. A delete answers 204.
- An error: `{ "error": "Validation failed", "details": { "name": ["has already been taken"] } }`.
- Status codes: 400 malformed parameter, 401 no or non-admin token, 403 missing scope, 404 no such record or resource not enabled, 405 read-only, 409 conflict, 422 validation, 429 rate limit, 502 the CI server failed, 503 CI not configured.
- A delete does what the admin does: the record is destroyed, a token is revoked. The last write wins, as in the admin; there are no batch operations.
- Every answer carries `WarpEngine-Version` and `WarpEngine-Admin-Api: 1`.
- Every successful write instruments `warp_engine.admin_api.write` with the principal, the operation and `via` (`http` or `mcp`), for a host's audit log.

# Operations

| Resource | Operations |
|---|---|
| Softwares | `GET/POST /softwares`, `GET/PATCH/DELETE /softwares/{id}` (`include=releases,images,externalLinks,extras`) |
| Releases | `GET/POST /softwares/{softwareId}/releases`, `GET/PATCH/DELETE /releases/{id}` |
| Release assets | `GET/POST /releases/{releaseId}/assets`, `GET/PATCH/DELETE /assets/{id}` |
| External links | `GET/POST /softwares/{softwareId}/external_links`, `GET/PATCH/DELETE /external_links/{id}` |
| Software images | `GET/POST /softwares/{softwareId}/images`, `POST …/images/upload` (multipart), `GET/PATCH/DELETE /software_images/{id}` |
| Additional resources | `GET/PATCH /softwares/{softwareId}/extras/{association}` — a host's own records, see below |
| Vocabulary | `/software_statuses`, `/software_categories`, `/platform_links`, each with `GET/POST` and `GET/PATCH/DELETE /{id}` |
| Downloads | `GET /downloads`, `GET /downloads/{id}`, `/downloads/summary`, `/downloads/top_files`, `/downloads/top_softwares`, `/softwares/{id}/download_stats` |
| Pipelines | `GET /pipelines`, `GET/PATCH /pipelines/{id}`, `GET /pipelines/{id}/runs`, `POST /pipelines/{id}/trigger`, `POST /pipelines/sync` |
| Files | `GET /files?dir=`, `POST /files` (multipart), `POST /files/directories`, `PATCH /files`, `DELETE /files?path=` |
| Tokens | `GET/POST /application_tokens`, `GET/PATCH/DELETE /application_tokens/{id}`, `POST /application_tokens/{id}/rotate`, `GET /device_grants[/{id}]` |
| Meta | `GET /me` — the caller, its scopes and the resources it may use |

Creating a token is the one answer that carries its `plainToken`; it is never shown again. A host's `additional_resources` appear under `extras` once the host gives them an `api_schema` — on teletypegames.org that is the SEO metadata of a software.

# OpenAPI

`GET /api/admin/v1/openapi.json` (or `openapi.yaml`) is generated from the same registry that routes the requests, at request time, so it describes exactly what this host serves: resources left out, writes under `read_only`, and pipelines or device sign-ins where the host has no CI or no client sign-in are not in it. Each operation carries `x-required-scope`, and its `operationId` is also its MCP tool name. It needs an `admin:read` token unless the host made it public.

# MCP

With `c.admin_api.mcp = true`, `POST /api/admin/v1/mcp` is a stateless Streamable HTTP MCP server — no session, no stream, one JSON answer per request — built on the official Ruby SDK. The same bearer token and scopes apply.

- Every operation is a tool: `software_list`, `software_update`, `release_create`, `pipeline_trigger`, `download_summary`, `admin_whoami` …, with the same JSON Schema as input and the same answer as structured content. A failed operation is a tool error carrying the HTTP status.
- `tools/list` shows only what the token may call, after `resources`, `read_only` and the CI and sign-in conditions.
- Uploads stay out, and so do the token resources by default: a fresh plain token has no business in a model's context, its transcript and its logs.

```
claude mcp add --transport http warp-admin https://teletypegames.org/api/admin/v1/mcp \
  --header "Authorization: Bearer 3f9c…"
```

# Discovery

`GET /api/service` ([WECP](https://wiki.teletypegames.org/engines/warp-engine/wecp)) carries an `admin` key: null while the Admin API is off, otherwise its version, `url`, `openapiUrl` and `mcpUrl`. The addresses are public; what lives there is not.

'; var prevLevel = 2; for (var i = 0; i < headings.length; i++) { var h = headings\[i\]; var text = h.textContent.trim(); var slug = text.toLowerCase() .replace(/\[áàâä\]/g,'a').replace(/\[éèêë\]/g,'e').replace(/\[íìîï\]/g,'i') .replace(/\[óòôöő\]/g,'o').replace(/\[úùûüű\]/g,'u') .replace(/\[^a-z0-9\]+/g, '-').replace(/^-|-$/g, ''); if (!slug) slug = 'heading-' + i; if (used\[slug\]) { slug += '-' + i; } used\[slug\] = true; h.id = slug; var level = parseInt(h.tagName.charAt(1)); var cls = level > 2 ? ' class="toc-child"' : ''; html += '- [' + text + '](#'%20+%20slug%20+%20')
'; } html += '
'; toc.innerHTML = html; // Scroll-spy: highlight current heading var links = toc.querySelectorAll('a'); var ids = \[\]; for (var j = 0; j < links.length; j++) ids.push(links\[j\].getAttribute('href').slice(1)); function updateActive() { var current = ''; for (var k = 0; k < ids.length; k++) { var el = document.getElementById(ids\[k\]); if (el && el.getBoundingClientRect().top <= 80) current = ids\[k\]; } for (var m = 0; m < links.length; m++) { links\[m\].parentNode.classList.toggle('active', links\[m\].getAttribute('href') === '#' + current); } } window.addEventListener('scroll', updateActive, { passive: true }); updateActive(); })();

---

## Navigation

- Parent: [WarpEngine](https://wiki.teletypegames.org/engines/warp-engine.md)
- Next: [WECP](https://wiki.teletypegames.org/engines/warp-engine/wecp.md)
