# MCP server

rent.dev is an MCP server as well as a REST API. An agent that speaks MCP gets the same account, sandboxes and payments as 32 tools, and a second endpoint with no login for prices, the provider catalog, docs and signup.

`POST https://mcp.rent.dev/mcp`

## Two endpoints

| Endpoint | Credentials | Tools |
| --- | --- | --- |
| `https://mcp.rent.dev/mcp` | An API key or an OAuth access token. | 32: the provider catalog, funds, sandboxes, GPU pods and the account. |
| `https://mcp.rent.dev/mcp/public` | None. | `rent_quote`, `rent_list_providers`, `rent_search_docs`, `rent_signup`, `rent_supply_guide` |

`rent_list_providers` answers with the catalog as compact summaries: each provider's capabilities, whether an agent can drive it, and the cheapest hourly price among its matching offerings. The whole catalog, with each row's region, source and date, is on [the catalog page](/catalog). A listed provider may be unpriced: its terms may permit display but not comparison, no price row may match the filters, or its rows may carry no hourly figure.

Both are stateless Streamable HTTP. Every request is a `POST`, there is no session to open or keep, and any other method answers 405.

## Connect a client

```json
{
  "mcpServers": {
    "rent": {
      "url": "https://mcp.rent.dev/mcp",
      "headers": {
        "Authorization": "Bearer rent_sk_…"
      }
    }
  }
}
```

No key yet? Call `rent_signup` on the public endpoint. It answers the key once, with this endpoint's URL and the header to send. Pass `idempotency_key`, a random value you keep, to be able to repeat the call without making a second account.

```sh
curl -s https://mcp.rent.dev/mcp/public \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "MCP-Protocol-Version: 2025-11-25" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
```

The answer comes back as JSON or as an event stream with one message.

## Authentication

`/mcp` takes the credentials the REST API takes: `Authorization: Bearer rent_sk_…`, `X-API-Key`, or an OAuth access token issued for the resource `https://mcp.rent.dev/mcp`. Without one it answers 401, with a `WWW-Authenticate` challenge that points at the protected-resource metadata (RFC 9728).

Each tool checks its scope before it runs. An API key has every scope but `supply`, so the partner supply tools need an OAuth token that carries it; an API key gets a `forbidden` result from them. An OAuth token that lacks a scope gets a 403 `insufficient_scope` challenge naming the scope to ask for.

A request that carries a browser `Origin` must come from this server's origin, from the site's, or from localhost when the endpoint itself is local. Any other origin answers 403.

## Protocol versions

The server speaks MCP 2026-07-28: `server/discover`, and the client's version, capabilities and name in each request's `_meta`. A client on an earlier revision negotiates with `initialize`; these are accepted: `2025-11-25`, `2025-06-18`, `2025-03-26`, `2024-11-05`, `2024-10-07`.

A `tools/list` answer may be kept for 5 minutes (`ttlMs`). The list does not change within a deploy.

## A typical session

1. `rent_quote`: what a task costs.
2. `rent_get_balance`, and `rent_topup` when it is short.
3. `rent_create_sandbox`.
4. `rent_exec`, or `rent_start_process` for work that outlasts one call.
5. `rent_terminate_sandbox`.

## Tools on /mcp

| Tool | What it does | Scope | Hints |
| --- | --- | --- | --- |
| [rent_list_providers](#rent_list_providers) | List providers | none | read-only, idempotent |
| [rent_quote](#rent_quote) | Quote sandbox prices | none | read-only, idempotent |
| [rent_get_balance](#rent_get_balance) | Get the balance | `billing` | read-only, idempotent |
| [rent_topup](#rent_topup) | Top up the balance | `billing` | open world |
| [rent_create_sandbox](#rent_create_sandbox) | Create a sandbox | `sandbox` | open world |
| [rent_get_sandbox](#rent_get_sandbox) | Get a sandbox | `sandbox` | read-only, idempotent |
| [rent_list_sandboxes](#rent_list_sandboxes) | List sandboxes | `sandbox` | read-only, idempotent |
| [rent_exec](#rent_exec) | Run a command | `sandbox` | destructive, open world |
| [rent_start_process](#rent_start_process) | Start a background process | `sandbox` | destructive, open world |
| [rent_get_process_output](#rent_get_process_output) | Read process output | `sandbox` | read-only, idempotent, open world |
| [rent_run_code](#rent_run_code) | Run code in a kernel | `sandbox` | destructive, open world |
| [rent_read_file](#rent_read_file) | Read a file | `sandbox` | read-only, idempotent, open world |
| [rent_write_file](#rent_write_file) | Write a file | `sandbox` | destructive, idempotent, open world |
| [rent_get_port_url](#rent_get_port_url) | Get a port URL | `sandbox` | read-only, idempotent |
| [rent_extend_sandbox](#rent_extend_sandbox) | Extend a sandbox | `sandbox` | idempotent, open world |
| [rent_pause_sandbox](#rent_pause_sandbox) | Pause a sandbox | `sandbox` | idempotent, open world |
| [rent_resume_sandbox](#rent_resume_sandbox) | Resume a sandbox | `sandbox` | idempotent, open world |
| [rent_terminate_sandbox](#rent_terminate_sandbox) | Terminate a sandbox | `sandbox` | destructive, idempotent, open world |
| [rent_create_gpu_pod](#rent_create_gpu_pod) | Create a GPU pod | `gpu` | open world |
| [rent_list_gpu_pods](#rent_list_gpu_pods) | List GPU pods | `gpu` | read-only, idempotent, open world |
| [rent_terminate_gpu_pod](#rent_terminate_gpu_pod) | Terminate a GPU pod | `gpu` | destructive, idempotent, open world |
| [rent_set_byok_credential](#rent_set_byok_credential) | Store a vendor key | `keys` | open world |
| [rent_get_usage](#rent_get_usage) | Get usage | `billing` | read-only, idempotent, open world |
| [rent_get_operation](#rent_get_operation) | Get an operation | none | read-only, idempotent |
| [rent_supply_guide](#rent_supply_guide) | Read the provider guide | none | read-only, idempotent |
| [rent_supply_enroll](#rent_supply_enroll) | Enrol nodes | `supply` | none |
| [rent_supply_nodes](#rent_supply_nodes) | List your nodes | `supply` | read-only, idempotent |
| [rent_supply_node](#rent_supply_node) | Get a node | `supply` | read-only, idempotent |
| [rent_supply_update_node](#rent_supply_update_node) | Change a node | `supply` | idempotent |
| [rent_supply_probe_node](#rent_supply_probe_node) | Probe a node now | `supply` | open world |
| [rent_supply_revoke_node](#rent_supply_revoke_node) | Revoke a node | `supply` | destructive, idempotent, open world |
| [rent_supply_earnings](#rent_supply_earnings) | Get your earnings | `supply` | read-only, idempotent |

### rent_list_providers

Lists rent.dev's provider catalog as compact summaries: each provider's capabilities (offerings, pricing types, interfaces, auth and product capabilities), display-only `summary_verdict`, `agentic` flag, the provenance of its cheapest matched offering and that offering's price row. Read-only and anonymous on /mcp/public. A listed provider may be unpriced: its terms may permit display but not comparison (`clause` names that denial and its prices are withheld), no price row may match the filters, or its rows may carry no hourly figure (a GPU row then gives `unpriced_reason`: monthly_rate, no_time_unit or unknown_gpu_count; a per-unit row has `usd` and `unit` but no hourly figure). `include_offerings` carries every matching offering, not just the cheapest. `use` asserts a rights use on providers and figures (default display). `limit` caps providers only. Every offer carries `covers`, `gpu.count`, `region_id` and `comparison_group`: the group names the purchasable unit (GPU model, count, market, pricing type, canonical region and commitment), so two offers are price-comparable exactly when their groups match. `region` accepts the vendor's label or the canonical id; `gpu_count`, `covers` and `figure` narrow the grain.

### rent_quote

Dollars per completed task for rent.dev's own sandboxes beside every published-rate vendor, by workload profile, plus hourly GPU rows. Defaults: all three profiles, 2 vCPU, 4096 MiB. A quote is a price list, not a reservation; it holds nothing.

### rent_get_balance

Your prepaid balance in micro-USD (1 USD = 1,000,000): total, held by running sandboxes, and available, and whether the account is frozen.

### rent_topup

method "card": answers a pay link for a person (valid 24 hours; Stripe Checkout, no API key needed). On clients that take URL elicitation the link is shown to the person first, then the tool answers it. method "x402": charges USDC over MCP with x402. The first call answers isError with the PaymentRequired object (amount = credits + 5.5%; sign within its maxTimeoutSeconds, 120 s); retry the same call with the PaymentPayload in _meta["x402/payment"]. The paid call settles, credits, and answers the SettleResponse in _meta["x402/payment-response"]. The balance shows card payments once Stripe confirms them; check with rent_get_balance.

### rent_create_sandbox

Starts a Linux sandbox (a Firecracker microVM) and holds its cost from your balance; it is charged by the second while it runs. Waits up to wait_s for placement and answers the sandbox; past that it answers an operation id (`op_…`) to poll with rent_get_operation, whose response is the sandbox. The sandbox id (`lse_…`) lives until rent_terminate_sandbox or the timeout (default 300 s). A short balance answers 402 `insufficient_balance` with a card pay link (valid 24 hours) and an x402 top-up URL. Arguments not listed here go to the REST route as they are, which refuses them by name.

### rent_get_sandbox

One sandbox by id: state (requested, placing, running, paused, settling, terminated, failed), template, size, timeout_at, port-URL domain, and what it has been charged and holds.

### rent_list_sandboxes

Your sandboxes, newest first. By default every state but terminated and failed; `state` picks one. `next_cursor` is opaque and only for the next call of this tool.

### rent_exec

Runs a shell command (`/bin/bash -l -c`) in a running sandbox and answers when it exits: stdout, stderr, exit_code (null when killed by the timeout), timed_out. timeout_s defaults to 50, so the answer comes back inside common client tool timeouts. For work that runs longer (servers, builds, training), start it with rent_start_process and read its output with rent_get_process_output. Arguments not listed here go to the REST route as they are, which refuses them by name.

### rent_start_process

Starts a command in the background and answers at once with its process id. The process id is valid while the sandbox runs; read its output with rent_get_process_output. The process ends with the sandbox. Arguments not listed here go to the REST route as they are, which refuses them by name.

### rent_get_process_output

Output of a background process after a cursor, whether it still runs, and its exit code. Omit cursor to read from the start; pass the answer's cursor back unchanged to read only newer output. Cursors and process ids are valid while the sandbox runs.

### rent_run_code

Runs code on the Jupyter kernel of a `code-interpreter-v1` sandbox and answers its rich results (text, images as base64, …), stdout, stderr and error. State persists between calls in one context_id; a context lives as long as the sandbox. timeout_s defaults to 50. Arguments not listed here go to the REST route as they are, which refuses them by name.

### rent_read_file

Reads a file from a running sandbox. Without `encoding` the content comes back as UTF-8 text when it is valid UTF-8, else as base64.

### rent_write_file

Writes a file in a running sandbox, creating parent directories and replacing what is there. `content` is UTF-8 text, or base64 with encoding base64.

### rent_get_port_url

The public HTTPS URL of a port in the sandbox (`https://{port}-{sandbox}.{domain}`). It serves while the sandbox runs and something listens on the port.

### rent_extend_sandbox

Sets a running sandbox to stop timeout_s seconds from now (at most 86400), holding the added cost first on the key that started it; a short balance answers 402 with top-up links. An API key with a spend limit cannot extend a sandbox another key started (403 forbidden).

### rent_pause_sandbox

Pauses a running sandbox, keeping its memory and files; the id stays valid and rent_resume_sandbox continues it.

### rent_resume_sandbox

Resumes a paused sandbox with a new timeout (default 300 s), holding its cost first on the key that started it; a short balance answers 402 with top-up links. An API key with a spend limit cannot resume a sandbox another key started (403 forbidden).

### rent_terminate_sandbox

Ends a sandbox now, deleting its memory and files, and settles its charge. Terminating an ended sandbox answers it unchanged. The id keeps answering rent_get_sandbox.

### rent_create_gpu_pod

Starts a GPU pod on RunPod with your own RunPod key (store it with rent_set_byok_credential, vendor runpod). RunPod bills your key by the hour until rent_terminate_gpu_pod; no rent.dev balance is used. The pod id (`lse_…`) lives until it is terminated. Arguments not listed are refused by name.

### rent_list_gpu_pods

Your GPU pods, newest first, every state. `next_cursor` is opaque and only for the next call of this tool.

### rent_terminate_gpu_pod

Ends a GPU pod on RunPod, deleting its container disk, and stops RunPod's billing. Terminating an ended pod answers it unchanged.

### rent_set_byok_credential

Stores your own vendor key (BYOK) for e2b and vercel sandboxes or runpod GPU pods, after one read-only call to the vendor checks it. The secret is an argument of this call; it is stored sealed and never returned. fields: e2b {api_key}, vercel {token, team_id, project_id}, runpod {api_key}. The credential id (`cred_…`) stays valid until you delete it (DELETE /v1/byok/credentials/{id}). An API key with a spend limit cannot store one (403 forbidden): use an uncapped key or a signed-in person.

### rent_get_usage

Usage over a period (default the last 30 days): what rent.dev billed for its own sandboxes, beside showback of BYOK usage your vendors bill you for.

### rent_get_operation

A long-running operation by id (`op_…` or `operations/op_…`), as tools answer them for work that outlasts a call (sandbox placement, x402 settlement). Poll about every 2 s until done; then exactly one of `response` (the finished resource) or `error` (a Problem). The id stays readable after it is done.

### rent_supply_guide

The guide for providers who run rent.dev nodes, as Markdown: the two ways to run a node, every step with its exact command, each node state and what to do next, every check with its fix, every problem, rates and the holdback. Read it before rent_supply_enroll. Needs no scope, and the public endpoint serves it too.

### rent_supply_enroll

Makes claim tokens for new nodes, one per node, each valid for 10 minutes and shown only in this answer. `lines` holds the exact shell line for each token: run it on the machine and the node kit sets the machine up, claims the node, links it and reports each step. The token reaches the kit on stdin, never as a process argument. On a host the claim comes after the install, and a first install can outlast the token: when the line ends with `claim_invalid`, call this tool again and run the same line with the new token. That run is quick, because the host is already set up. A refusal carries `retryable`, `owner_action_required` and `next`; run `next.command` after `next.after_s` seconds.

### rent_supply_nodes

Your nodes, newest claim first, each with its state, checks and `next` step. `next_cursor` is only for the next call of this tool.

### rent_supply_node

One of your nodes: state, wire, region, ask, capacity, templates, checks with their fixes, suspension reasons, placement and setup timings. `next` names the one command to run and when; poll this until `next.action` is `none`.

### rent_supply_update_node

Changes a node's ask, capacity, region or payout. A new ask applies to leases placed from now on; running leases keep their rates. More slots or a new region sends the node back to probing. Changing payout needs `signed_nonce`: a nonce from GET /v1/accounts/nonce, signed with your account's agent key over the message `rent.dev payout v1\n<node_id>\n<nonce>\n<public_key>` (four lines joined by newlines, no newline at the end). A refusal carries `retryable`, `owner_action_required` and `next`; run `next.command` after `next.after_s` seconds.

### rent_supply_probe_node

Makes the node's next probe due now, and takes a suspended node back to probing. The probe starts within about a minute. Read the outcome with rent_supply_node. A probe already running is refused with `probe_in_progress`. A refusal carries `retryable`, `owner_action_required` and `next`; run `next.command` after `next.after_s` seconds.

### rent_supply_revoke_node

Takes a node out for good: it gets no new leases and its link is removed. A revoked node cannot come back; enrol the machine again for a new node. Revoking a revoked node answers it unchanged. A refusal carries `retryable`, `owner_action_required` and `next`; run `next.command` after `next.after_s` seconds.

### rent_supply_earnings

What your nodes have earned: pending, releasable, frozen and paid-out totals in micro-USD, and one page of entries, newest first. A lease shows once it settles, never while it runs. An entry becomes releasable after the holdback, which is longer when the customer paid by card. `next_cursor` is only for the next call of this tool.

## Tools on /mcp/public

| Tool | What it does | Scope | Hints |
| --- | --- | --- | --- |
| [rent_quote](#rent_quote) | Quote sandbox prices | none | read-only, idempotent |
| [rent_list_providers](#rent_list_providers) | List providers | none | read-only, idempotent |
| [rent_search_docs](#rent_search_docs) | Search the rent.dev docs | none | read-only, idempotent |
| [rent_signup](#rent_signup) | Sign up | none | none |
| [rent_supply_guide](#rent_supply_guide) | Read the provider guide | none | read-only, idempotent |

### rent_search_docs

Keyword search over the sections of rent.dev's full docs (llms-full.txt): each hit is one section, best first, with its heading path and Markdown. The docs are re-read every five minutes.

### rent_signup

Creates a rent.dev account and answers its API key, shown this once (store it; it stays valid until revoked), with the authenticated MCP URL and the Authorization header to connect with, and a three-line REST quickstart. To be able to retry, pass idempotency_key, a random value you keep (a UUID will do): if the answer is lost, call again with the same value within 24 hours and you get the same account with a new API key that replaces the first, not a second account. That holds while the account is untouched (its key never used, nothing bound to it, no money in); after that the repeat is refused with idempotency_key_reused and answers no key. Without idempotency_key every call creates a new account. The account starts with a zero balance: a call that needs credit answers 402 with a card pay link and an x402 top-up, or use rent_topup on the authenticated server.

## Results and errors

A tool answers its value as `structuredContent`, and as the same JSON in a text block. A refusal is an `isError` result whose content is the Problem the REST API would have answered, with its `code`. The codes are listed on [the errors page](/docs/errors).

## Paying from inside a tool call

When a call needs more credit, the Problem is a 402 with `topup.checkout_url` and `topup.x402_url`. A 2026-07-28 client that takes URL elicitation is handed the pay link as an input request instead, and the call runs again once the person has answered.

`rent_topup` with `method: "card"` answers a pay link for a person, valid 24 hours. With `method: "x402"` it charges USDC over MCP:

1. The first call answers `isError` with the x402 PaymentRequired object as its structured content.
2. Sign it within 120 seconds and call again with the same arguments and the PaymentPayload in `_meta["x402/payment"]`.
3. The paid call settles, credits the balance and answers the settlement in `_meta["x402/payment-response"]`.

A payment that is broadcast but not yet confirmed answers an operation instead. Poll it with `rent_get_operation`.

```json
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "rent_topup",
    "arguments": {
      "method": "x402",
      "amount_usd": "5.00"
    },
    "_meta": {
      "x402/payment": "<PaymentPayload>"
    }
  }
}
```
