# Connecting an AI agent to RunApps

This page is for an AI coding agent (Claude Code, Codex, Cursor, Gemini CLI, any MCP client) that a RunApps user has asked to connect. Follow it top to bottom; the user only has to click **Approve** once in a browser. You never ask for, see or type the user's password.

RunApps (https://www.runapps.ai) publishes web apps that call AI models: a static frontend bundle (zip) is served on its own `<slug>.runapps.dev`, users sign in with their RunApps account and pay for their own model usage, and the creator earns a fee. Through the MCP server you can create apps, upload bundles, set their metadata and publish them for the user.

## 1. Request a key

```
POST https://www.runapps.ai/api/agent/device/code
Content-Type: application/json

{"name": "<your name> @ <machine>", "scopes": "read,develop,publish,gateway"}
```

Response:

```json
{
  "device_code": "…",
  "user_code": "ABCD-EFGH",
  "verification_uri": "https://www.runapps.ai/agent/authorize",
  "verification_uri_complete": "https://www.runapps.ai/agent/authorize?code=ABCD-EFGH",
  "expires_in": 600,
  "interval": 5,
  "scopes": "read,develop,publish,gateway"
}
```

Show `verification_uri_complete` to the user verbatim and ask them to open it and approve. `name` is the key's identity: approving a request with the same name revokes the previous key with that name, so re-running setup does not pile keys up. `scopes` is optional; the default is `read,develop,publish,gateway`.

Scopes:

| scope | lets you |
|---|---|
| `read` | list the user's apps, their status, review notes; the model catalogue |
| `develop` | create apps, edit name / description / icon / category, upload bundles |
| `publish` | change visibility: submit for review, make unlisted or public |
| `gateway` | call models at `/v1` with this key, billed to the user (to test the app you build) |
| `danger` | delete apps, take published apps down. Not in the default; ask for it only when the user wants that and say why |

The user can untick scopes on the approval page, and later change the key's scopes (or rename / revoke it) under API Keys at https://www.runapps.ai/dashboard/api; a change applies to your next call. A tool you call without its scope returns an error naming the scope; ask the user to grant it there rather than working around it.

## 2. Poll for the key

```
POST https://www.runapps.ai/api/agent/device/token
Content-Type: application/json

{"device_code": "…"}
```

every `interval` seconds. `status` is:

- `pending` — keep waiting (the user has not decided yet)
- `slow_down` — you polled too fast; wait `interval` seconds
- `denied` — the user said no; stop and tell them
- `expired` — the code aged out (10 minutes) or was already collected; start over at step 1
- `approved` — the key, **returned exactly once**:

```json
{
  "status": "approved",
  "api_key": "rk_…",
  "key_prefix": "rk_9911ad65d",
  "scopes": "read,develop,publish,gateway",
  "mcp_url": "https://www.runapps.ai/api/mcp",
  "docs_url": "https://www.runapps.ai/agent/setup.md"
}
```

Write `api_key` into your MCP configuration immediately. Never paste it into the conversation, a log, a commit or a repository.

## 3. Connect

MCP Streamable HTTP at `https://www.runapps.ai/api/mcp` with the header `Authorization: Bearer rk_…`.

Claude Code:

```bash
claude mcp add --transport http runapps "https://www.runapps.ai/api/mcp" --header "Authorization: Bearer rk_…"
```

Codex (`~/.codex/config.toml`):

```toml
[mcp_servers.runapps]
url = "https://www.runapps.ai/api/mcp"
http_headers = { Authorization = "Bearer rk_…" }
```

Cursor, Claude Desktop, Windsurf and others: an HTTP MCP server with the same URL and header in their MCP settings. Then call `tools/list`; the `instructions` returned by `initialize` describe the recommended workflow.

For a one-off call without an MCP client, the endpoint is plain JSON-RPC: POST the request body with `Content-Type: application/json` and `Accept: application/json, text/event-stream`.

## 4. The tools

| tool | scope | what it does |
|---|---|---|
| `list_apps` | read | the user's apps: id, slug, name, visibility, has_bundle, addresses |
| `get_app` | read | one app in full, including private prompt ids found in its bundle and the review note |
| `get_review` | read | visibility (`draft`, `pending`, `public`, `private`, `rejected`) and the reviewer's note |
| `check_bundle` | read | inspect the current bundle: entry file, SDK loaded or not, warnings |
| `list_models` | read | the model catalogue: the exact `model` names to use, capability, prices |
| `create_app` | develop | create a draft and get its slug |
| `update_app` | develop (+ publish for `visibility`) | name, description, long_description (Markdown), category, icon_url, visibility |
| `upload_bundle` | develop | a one-time URL to `PUT` the zip to (10 minutes, 50 MB) |
| `upload_bundle_base64` | develop | the zip inline as base64, for small bundles when you cannot run curl |
| `unpublish_app` | danger | public → private (unlisted) |
| `delete_app` | danger | delete the app and its bundle |
| `revoke_my_key` | — | revoke the key this connection uses |

Categories: Creative, Content, Productivity, Marketing, Engineering, Design, Research, Data Analysis, Business, Finance.

## 5. The workflow

1. **`create_app`** with a name. You get the slug (`proj-xxxxxxxx`). The slug is assigned by the platform (an admin may later give a listed app a readable one); neither you nor the user can choose it. Develop locally against it:
   ```html
   <script src="https://www.runapps.ai/sdk/latest.js"></script>
   <script>
     const client = new RunApps({ project: "<slug>" }); // no key: the user signs in
     const r = await client.chat.create({ model: "Claude Sonnet 4.6", messages: [{ role: "user", content: "hi" }] });
   </script>
   ```
   Sign-in from `http://localhost:<port>` works; calls go through the real gateway on the user's balance. The SDK docs are the README of https://github.com/runapps/sdk-js (npm `@runappsai/sdk`). Never put an `rk_` key in an app: the app signs its users in.
2. **Build and zip.** Zip the **contents** of the build folder so `index.html` is at the zip root (`cd dist && zip -r ../app.zip .`). Absolute asset paths (`/assets/…`) work because the app runs at the root of its own origin. Dot files and folders (`.private/…`) are never served; put private prompts in `.private/prompts/<id>.md` and call them with `prompt: { id }`.
3. **`upload_bundle`**, then `PUT` the zip to the returned URL:
   ```bash
   curl -sS -X PUT -H 'Content-Type: application/zip' --data-binary @app.zip '<upload_url>'
   ```
   The response is the bundle report (same as `check_bundle`): fix the warnings it lists.
4. **`update_app`** with `description` (one line), `long_description` (Markdown: what the user gets, how it works), `category` and an `icon_url` (square PNG/SVG, https or data URL).
5. **Ship.** `update_app` with `visibility: "private"` (unlisted: anyone with the link, no review) or `"public"` (library listing; the first time it goes to review, every later update is live at once). `get_review` shows the state and the reviewer's note if it was rejected; the user is also emailed.

The app runs at `https://<slug>.runapps.dev`; its page is `https://www.runapps.ai/projects/<slug>`.

## Rules

- Tools marked `[danger]` (`unpublish_app`, `delete_app`) change what other people can use. Ask the user before calling them and pass `confirm: true` only after they agree.
- Model calls you make with the `gateway` scope cost the user money. Test with short prompts and cheap models (`list_models` shows prices).
- The key is the user's. Do not share it between machines or users; request a new one per machine instead (the `name` tells them apart under API Keys).
- To disconnect for good, call `revoke_my_key` with `confirm: true`; the user can also revoke it at https://www.runapps.ai/dashboard/api.

A skill with this page, the tool reference and the SDK docs is at https://github.com/runapps/skills (`npx skills add runapps/skills`).
