relaygpu-mcp
Official# @relaygpu/mcp
Relay's MCP server. Your agent can find a Relay model, read its input schema, run it, poll long tasks, upload files, run workflows and search the Relay docs, all from Claude Code, Cursor, VS Code or Claude Desktop. It is built on [`@relaygpu/client`](https://www.npmjs.com/package/@relaygpu/client). The tools are generic and take the model as a string, so a model added to Relay works without a new release.
Connect to the hosted server, or run it locally:
| | How | Credential |
|---|---|---|
| **Hosted** (Streamable HTTP) | `https://mcp.relaygpu.com/mcp`, nothing to install | `X-API-Key: relay_sk_...` header, or `Authorization: Bearer <relay key or dashboard JWT>` |
| **Local** (stdio) | `npx -y @relaygpu/mcp` (Node ≥ 20) | `RELAY_API_KEY=relay_sk_...` in its environment; optional `RELAY_BASE_URL` (default `https://relaygpu.com`) |
The server never stores your key: the hosted one reads it from each request and uses it for that request only, the local one reads it from its environment.
## Install
Claude Code, Codex, Cursor and VS Code connect to the hosted server: no Node, no `npx`. Claude Desktop accepts remote servers only through OAuth, which v0 does not offer yet, so it uses the local server through a one-click bundle. Every client can also run the local server; see [Local server](#local-server-stdio).
### Claude Code
Terminal, in the CLI or the VS Code / JetBrains extension alike:
```bash
claude mcp add --transport http --scope user relay https://mcp.relaygpu.com/mcp --header "X-API-Key: relay_sk_..."
```
`--scope user` makes it available in every project; without it the server exists only in the directory you ran the command in. Check it with `/mcp` in a new session. If the terminal answers `command not found: claude`, the extension is installed but the CLI is not: install it with `curl -fsSL https://claude.ai/install.sh | bash` (or `npm install -g @anthropic-ai/claude-code`).
### Codex
Codex sends the key as a Bearer token from an environment variable, so the key stays out of `config.toml`. Set `RELAY_API_KEY` in your shell profile, then:
```bash
codex mcp add relay --url https://mcp.relaygpu.com/mcp --bearer-token-env-var RELAY_API_KEY
```
### Cursor
[](cursor://anysphere.cursor-deeplink/mcp/install?name=relay&config=eyJ1cmwiOiJodHRwczovL21jcC5yZWxheWdwdS5jb20vbWNwIiwiaGVhZGVycyI6eyJYLUFQSS1LZXkiOiJyZWxheV9za18uLi4ifX0=)
The button installs the entry below with a placeholder key; replace `relay_sk_...` in Cursor's MCP settings afterwards. By hand: `~/.cursor/mcp.json` (all projects) or `.cursor/mcp.json` (one project).
```json
{
"mcpServers": {
"relay": {
"url": "https://mcp.relaygpu.com/mcp",
"headers": { "X-API-Key": "relay_sk_..." }
}
}
}
```
### VS Code (Copilot agent mode)
This installs the server for VS Code's own agent (Copilot Chat). Using Claude Code inside VS Code? Use the [Claude Code](#claude-code) command instead: the Claude Code extension does not read VS Code's MCP config.
[](https://insiders.vscode.dev/redirect/mcp/install?name=relay&config=%7B%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fmcp.relaygpu.com%2Fmcp%22%2C%22headers%22%3A%7B%22X-API-Key%22%3A%22%24%7Binput%3Arelay-key%7D%22%7D%2C%22inputs%22%3A%5B%7B%22type%22%3A%22promptString%22%2C%22id%22%3A%22relay-key%22%2C%22description%22%3A%22Relay%20API%20key%20%28relay_sk_...%29%22%2C%22password%22%3Atrue%7D%5D%7D)
VS Code asks for the key once and keeps it in its secret storage. By hand, in `.vscode/mcp.json` (one workspace) or with **MCP: Open User Configuration** (every workspace):
```json
{
"inputs": [{ "type": "promptString", "id": "relay-key", "description": "Relay API key (relay_sk_...)", "password": true }],
"servers": {
"relay": {
"type": "http",
"url": "https://mcp.relaygpu.com/mcp",
"headers": { "X-API-Key": "${input:relay-key}" }
}
}
}
```
### Claude Desktop
1. Download [`relay.mcpb`](https://github.com/OpenGPU-Network/relaygpu-mcp/releases/latest/download/relay.mcpb).
2. Double-click it. Claude Desktop opens an install dialog: click **Install**.
3. The extension installs switched off. Open Settings → Extensions → **Relay** → **Configure**.
4. Paste your Relay key and click **Save**. The key stays in Claude Desktop's secure storage.
5. Switch **Relay** on.
The bundle runs on Claude Desktop's built-in Node, so nothing else needs to be installed. `get_credits` and `get_usage` read the account, so they take a dashboard JWT or a custom-tier superkey, never a plain key; every other tool works with a plain key.
By hand instead: Settings → Developer → Edit Config opens `claude_desktop_config.json` in your default editor. Add the block below at the top level, save, then quit Claude Desktop (⌘Q) and reopen it:
```json
{
"mcpServers": {
"relay": {
"command": "npx",
"args": ["-y", "@relaygpu/mcp"],
"env": { "RELAY_API_KEY": "relay_sk_..." }
}
}
}
```
### Local server (stdio)
Any client can run the server on your machine instead; it needs Node 20 or newer on the PATH the client sees (see [Troubleshooting](#troubleshooting)).
```bash
claude mcp add relay --scope user --env RELAY_API_KEY=relay_sk_... -- npx -y @relaygpu/mcp
codex mcp add relay --env RELAY_API_KEY=relay_sk_... -- npx -y @relaygpu/mcp
```
Cursor (`~/.cursor/mcp.json`) and VS Code take the same `command` / `args` / `env` entry as the Claude Desktop block above; VS Code adds `"type": "stdio"` and lists it under `"servers"`.
Ready-to-copy files are in [`examples/`](examples/).
## Troubleshooting
- **"Server disconnected", "spawn npx ENOENT" or "Executable not found: npx"** (Claude Desktop, Cursor). Apps opened from the Dock or Start menu do not load your shell profile, so a Node installed with nvm, Volta or Homebrew is not on their PATH. Use the Claude Desktop bundle, or put the full path in `"command"`: run `which npx` in a terminal and paste the result (for example `/Users/you/.nvm/versions/node/v22.22.0/bin/npx`).
- **`TypeError: Relay: no global fetch`**: the client started Node 16 or older. Install Node 20 or newer, or point `"command"` at a newer `npx`.
- **Windows**: if the client cannot start `npx`, use `"command": "cmd"` with `"args": ["/c", "npx", "-y", "@relaygpu/mcp"]`.
- **The first start takes a few seconds**: `npx` downloads the package once (about 5 s); later starts take about 1 s.
## Tools
Tools marked *keyless* work without a credential on the local server. Keyless tools answer the public catalog; with a credential, `search_models`, `get_model`, `get_pricing`, `estimate_cost` and `run_model`'s model lookup answer your account's: the models your tier and grants can run appear and run. The others are billed or account tools and need one. A hosted server with OAuth on (see [Self-hosting](#self-hosting)) takes no request without a credential, keyless tools included.
| Tool | What it does | Example |
|---|---|---|
| `search_models` *keyless* | Find models by tag and/or text; `image-to-image` also lists every image model that takes an input image (`accepts: ["image"]`) | "Find a text-to-video model from Kling" → `{tag: "text-to-video", query: "kling"}` |
| `get_model` *keyless* | Route, request schema, example, pricing and status of one model | `{model: "Qwen/qwen-image"}` |
| `get_pricing` *keyless* | Prices (public; your account's with a credential) for one model or all of them, including the `media_storage` SKUs | `{model: "KlingTeam/v3-T2V"}` |
| `estimate_cost` *keyless* | Estimate from the price list (public; your account's with a credential; not an invoice) | `{model: "KlingTeam/v3-T2V", usage: {duration_seconds: 5, quality_mode: "std"}}` |
| `run_model` | Run any model by name and wait up to 45 s | `{model: "Qwen/qwen-image", input: {prompt: "a red fox", size: "512x512"}}` |
| `check_task` *keyless* | Poll a task (waits up to 30 s) | `{task_id: "direct:…", wait_seconds: 30}` |
| `upload_file` | Host a file and get a URL for any `*_url` input (or an image editor's `image`), or a one-time upload slot for a local file on the hosted server | `{path: "./clip.mp4", retention: "relay1d"}` · `{slot: true}` |
| `list_workflows` | List the workflow templates | "Which workflows can I run?" |
| `run_workflow` | Start a workflow run, wait up to 45 s | `{workflow_id: "script-voiceover", inputs: {messages: [{role: "user", content: "a lighthouse at dusk"}], voice: "Cherry"}}` |
| `check_workflow_run` *keyless* | Poll a run (waits up to 30 s) | `{run_id: "…", wait_seconds: 30}` |
| `cancel_workflow_run` | Cancel a run that is still in progress | `{run_id: "…"}` |
| `get_credits` | Credit balance, promos and consumption (JWT or custom-tier superkey) | "How many credits do I have left?" |
| `get_usage` | Usage analytics (JWT or custom-tier superkey) | `{period: "7d"}` |
| `search_docs` *keyless* | Search the Relay docs | `{query: "idempotency key"}` |
## Local files
The hosted server cannot read files on your computer. How a local file reaches a model, per client:
| Client | Local file → model |
|---|---|
| Claude Code, Cursor, VS Code, Codex (hosted server) | `upload_file({slot: true})` returns a `curl` line; the agent runs it in your shell with the file's path (no key in the shell, single use, 10 minutes) and it prints the file's `url`; pass that `url` to `run_model` (for example as `image`). |
| claude.ai, Claude Desktop, ChatGPT | `upload_file({slot: true})` returns a link: open the link in your browser, pick the photo, and paste the link the page shows back into the chat; the model passes it to `run_model`. No key leaves the chat, single use, 10 minutes. A public URL of the file works too. |
| Local server (stdio), any client | `upload_file({path: "./photo.png"})` uploads the file and returns its `url`. |
Image editors: `search_models({tag: "image-to-image"})` lists every image model that takes an input image, each with `accepts: ["image"]`.
## Notes
- **Auth.** On the local server, and on a hosted server without OAuth, `tools/list` and the keyless tools answer without a credential. A hosted server with OAuth on answers every `/mcp` request without a credential with `401` and `WWW-Authenticate: Bearer resource_metadata="…", scope="relay"`, which is how clients start the "Connect Relay" sign-in; `X-API-Key` and `Authorization: Bearer <relay key or dashboard JWT>` keep working there. A billed tool called without one returns a `MISSING_CREDENTIALS` tool error that names what to set: the `X-API-Key` header (hosted) or `RELAY_API_KEY` (stdio). Errors from Relay come back as tool errors that include the error class, `code`, `requestId` and the server's detail. Your key never appears in a tool result or a log line.
- **Long tasks.** `run_model` waits up to 45 s (`wait_seconds`, 0–45). If the task is still running after that, it returns the `task_id` and the line "Do not resubmit: call check_task with this task_id". Every submit carries an `Idempotency-Key`, so a retry does not bill twice. Pass your own `idempotency_key` if your agent might send the same submit again. Since 0.1.7 every model whose input schema takes `async` (image, video, audio) is submitted as a task, so a long image run returns a `task_id`, never a 524; chat models still answer inline.
- **Expiry.** Each result link comes with its expiry: "expires 1 h after completion unless store_output was set". To keep results longer, pass `store_output` with a `media_storage` SKU from `get_pricing` (for example `relay7d`). That SKU adds a per-file fee.
- **Image links.** The image routes that answer base64 (Gemini, gpt-image, FLUX-2-pro) also answer Relay's own links (`urls`); the server returns those links in place of the base64, for 1 h or the `store_output` lifetime. A result that carries base64 without a Relay link is uploaded through `/v2/files` (at the `store_output` retention, else `relay1h`, free within the daily quota) and comes back as a URL and a `file_id`. Image content blocks are opt-in: pass `inline_images: true`, which applies to images of 1 MB or less.
- **Uploads.** On stdio, `upload_file` streams a local `path` of up to 100 MB. The hosted server cannot read your disk: use `slot: true` (see [Local files](#local-files)), `base64` (up to 4 MB) or a public `url`. Retention defaults to `relay1h`.
## Self-hosting
```bash
docker build -t relaygpu-mcp .
docker run -p 2300:2300 -e PORT=2300 -e RELAY_BASE_URL=https://relaygpu.com relaygpu-mcp
```
The server listens on `PORT` (default 2300) and calls Relay at `RELAY_BASE_URL`. Endpoints:
- `POST /mcp`: Streamable HTTP, stateless.
- `GET /healthz`: returns `{status, version}`.
- `GET /metrics`: Prometheus text with `mcp_tool_calls_total{tool,outcome}` and `mcp_tool_seconds{tool}`.
- `GET /.well-known/mcp/server-card.json`: the server and its tools.
- `GET /.well-known/oauth-protected-resource` (and `…/mcp`): the RFC 9728 protected-resource metadata, only with OAuth on.
### OAuth ("Connect Relay")
OAuth is on when both `OAUTH_ISSUER` and `MCP_RESOURCE_URL` are set; with either unset the server behaves as above (no metadata route, keyless tools answer without a credential). All three variables belong to the **MCP app** and are read once at start:
| Variable | Prod | Staging (VPN) | Notes |
|---|---|---|---|
| `OAUTH_ISSUER` | `https://relaygpu.com` | `https://staging-back-c54ef4a.opengpu.network` | Relay's authorization server; the JWKS is read from `{issuer}/v2/oauth/jwks`, the exchange posts to `{issuer}/v2/oauth/token` |
| `MCP_RESOURCE_URL` | `https://mcp.relaygpu.com/mcp` | `http://relay-mcp-staging.internal.opengpu.network/mcp` | This server's public `/mcp` URL: the access token's `aud` must equal it, and the metadata URL is its origin + `/.well-known/oauth-protected-resource` |
| `OAUTH_MCP_CLIENT_SECRET` | secret | secret | The `relay-mcp` client's secret: the same value as **Relay's** `OAUTH_MCP_CLIENT_SECRET` in the same environment, never shared across environments. Without it every exchange fails (`503`) |
A Bearer value that is a JWT issued by `OAUTH_ISSUER` is an access token: the server verifies it (ES256 against the issuer's JWKS, `aud`, `exp` with 60 s skew), exchanges it at Relay for a 5-minute Relay credential and keeps that credential in memory per token hash until 30 s before it expires. A bad, expired or revoked token gets `401` with `WWW-Authenticate: Bearer error="invalid_token", …`; an authorization server that cannot be reached gets `503`. Tokens, credentials and the secret are never logged.
Logs are JSON lines on stderr, `WARNING` and above by default (`LOG_LEVEL`). Without Docker, run `npm ci && npm run build && node dist/http.js`.
## License
MIT
TDQS
Scored across 14 tools
Tools are largely distinct and the descriptions explicitly cross-reference their partners (run_model → check_task, run_workflow → check_workflow_run), which resolves most ambiguity. The only soft overlaps are the account/money trio get_pricing, estimate_cost, get_usage and get_credits, which an agent could momentarily confuse, plus the parallel check_task/check_workflow_run pair that requires reading descriptions to separate.
Every tool follows a strict snake_case verb_noun pattern (list_workflows, search_models, get_model, run_model, check_task, cancel_workflow_run, upload_file, estimate_cost). The run/check/cancel triplet for workflows and the run/check pair for tasks are named symmetrically, reinforcing the pattern.
14 tools sit squarely in the well-scoped range for an inference/media platform. Each tool maps to a distinct stage: discovery, schema lookup, pricing, execution, polling, cancellation, upload, and account state, with no obviously redundant entries.
The surface covers the full lifecycle for models and workflows: discover → inspect schema → estimate cost → run → poll → cancel → store outputs, plus upload, usage, credits, and docs. The notable gap is asymmetry in cancellation: workflow runs can be cancelled but async model tasks (check_task) have no cancel or list/history operation, which could strand long-running or unwanted tasks.