# Liquid MCP server

The exchange runs a Model Context Protocol (MCP) server. Add it to an agent, and the agent can look up models and prices, preview a purchase, read your balance and buy answers.

To add it, run `liquid mcp claude` or follow [Connect](#connect). Tools that buy charge your balance and need a `spend` key.

Use the server while you build. In an application, call the [REST API](https://inference.ai.exchange/docs/connect-openai-sdk.md).

## Connect

The server is at `https://router.inference.ai.exchange/mcp`. It uses the Streamable HTTP transport and keeps no session.

There are two ways to sign in:

- **Sign in with your browser.** You choose the workspace, whether the app can buy answers, and the spend limit. Use this when the client supports OAuth.
- **Use a key.** Send an API key as a bearer token, for a client that cannot open a browser.

### Sign in with your browser

Only an owner or admin of the workspace can approve a key. The page shows where the browser goes next. Approve only if you started the request from that app.

The key that the client gets:

- Is named `Liquid MCP: <app name>`. Revoke it on the [API keys](https://inference.ai.exchange/docs/keys.md) page.
- Expires in 7 days. The client then asks you to sign in again.
- Has the spend limit that you set. The default is $10.
- Cannot buy answers unless you tick the box on the consent page. It can still look up models and read the balance and receipts.

The box is unticked for a client that asks for `mcp:read`, and ticked for `mcp:spend`. You decide either way.

#### The Liquid CLI

The [Liquid CLI](https://inference.ai.exchange/docs/cli.md) adds the server in one step. Use `claude`, `codex`, `cursor` or `opencode`. A `liquid claude` or `liquid opencode` session has it already.

```bash
liquid mcp claude
```

#### Claude Code

```bash
claude mcp add --transport http liquid https://router.inference.ai.exchange/mcp
claude mcp login liquid
```

You can also run `/mcp` in a session, select **liquid**, and choose **Authenticate**.

#### Codex CLI

```bash
codex mcp add liquid --url https://router.inference.ai.exchange/mcp
codex mcp login liquid
```

Run the login on the machine where Codex runs, because Codex receives the code on `http://127.0.0.1`.

#### Cursor

Add this to `~/.cursor/mcp.json`, then sign in from the MCP settings.

```json
{
  "mcpServers": {
    "liquid": { "url": "https://router.inference.ai.exchange/mcp" }
  }
}
```

#### OpenCode

Add this to `~/.config/opencode/opencode.json`. OpenCode starts the sign-in when the server answers 401, or when you run `opencode mcp auth liquid`.

```json
{
  "mcp": {
    "liquid": {
      "type": "remote",
      "url": "https://router.inference.ai.exchange/mcp",
      "enabled": true
    }
  }
}
```

#### Claude Desktop and Claude on the web

The exchange is not in the connector directory, so add a custom connector.

1. Open **Customize → Connectors**, and choose **Add custom connector**. On a Team or Enterprise plan, an owner first adds it in **Organization settings → Connectors**, and each member then chooses **Connect**.
2. Set the name to `Liquid Inference` and the server URL to `https://router.inference.ai.exchange/mcp`. Leave the OAuth client ID and secret empty.
3. Choose **Add**, then **Connect**, and approve the request in your browser.

### Use a key

A key with the `spend` scope can buy answers. A key that can only view the market can use the other tools. See [API keys](https://inference.ai.exchange/docs/keys.md). Export the key once:

```bash
export LIQUID_API_KEY=sk-liquid-...
```

#### Claude Code

```bash
claude mcp add --transport http liquid https://router.inference.ai.exchange/mcp \
  --header "Authorization: Bearer $LIQUID_API_KEY"
```

#### Codex CLI

Add this to `~/.codex/config.toml`.

```toml
[mcp_servers.liquid]
url = "https://router.inference.ai.exchange/mcp"
bearer_token_env_var = "LIQUID_API_KEY"
```

#### Cursor and OpenCode

Use the config from above and add the key:

- Cursor: add `"headers": { "Authorization": "Bearer ${env:LIQUID_API_KEY}" }` to the `liquid` entry.
- OpenCode: add `"oauth": false`, which makes OpenCode use your key, and `"headers": { "Authorization": "Bearer {env:LIQUID_API_KEY}" }`.

## Tools

Each tool calls the REST endpoint of the same name in the [API reference](https://inference.ai.exchange/docs/api.md), with the same key rules, rate limits and billing.

### Tools that read

These tools need any key, unless the table says `read`.

| Tool             | What it does                                                                         | Key  |
| ---------------- | ------------------------------------------------------------------------------------ | ---- |
| `list_models`    | Lists the models that the exchange serves now.                                       | any  |
| `get_model`      | Gets one model card: context window, features, prices.                               | any  |
| `compare_models` | Compares two to five models. Takes `models`.                                         | any  |
| `get_book`       | Lists the resting offers.                                                            | any  |
| `get_markets`    | Gets the providers and offers for each model.                                        | any  |
| `get_routers`    | Lists router models, such as `liquid.auto`.                                          | any  |
| `get_fills`      | Lists the newest settled jobs of a model. It never names a buyer.                    | any  |
| `get_volume`     | Gets the tokens and jobs per model, router and preset.                               | any  |
| `get_throughput` | Gets the measured speed of each provider and model.                                  | any  |
| `get_lifetime`   | Gets the jobs settled and tokens processed in all.                                   | any  |
| `count_tokens`   | Estimates input tokens. Takes `prompt`, `system`.                                    | any  |
| `dry_run`        | Previews a purchase and buys nothing. See [Preview a purchase](#preview-a-purchase). | any  |
| `get_video`      | Gets the status of a video job. See [Video](#video).                                 | any  |
| `get_balance`    | Gets the workspace balance.                                                          | read |
| `get_limits`     | Gets what the key can spend: grade, balance, spend limit and the part used.          | read |
| `get_job`        | Gets the receipt of one job.                                                         | read |
| `list_receipts`  | Lists the jobs of the workspace, newest first.                                       | read |

`list_models` and `get_book` take `kind`, `max_price_usd`, `limit` and `offset` (see [Find a model](#find-a-model)). `get_volume` and `get_throughput` take `days` (1 to 90). `list_receipts` takes `limit`, `cursor`, and `model` and `outcome` as comma-separated lists.

### Tools that buy

These tools charge the balance. They need a `spend` key.

| Tool              | What it buys                                     | Arguments                                                                                                                   |
| ----------------- | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| `send_message`    | One answer. See [Buy an answer](#buy-an-answer). | See that section.                                                                                                           |
| `generate_image`  | One to ten images.                               | `model`, `prompt`; optionally `n`, `size`, `quality`, `output_format`, references                                           |
| `generate_speech` | Spoken audio.                                    | `model`, `input`, `voice`; optionally `response_format` (`mp3`, `opus`, `aac`, `flac`, `wav` or `pcm`), `speed`, references |
| `create_video`    | A video job. It returns the job ID.              | `model`, `prompt`; optionally `seconds` (at most 20), `size`, `callback_url`, references                                    |
| `transcribe`      | A transcript of an audio file.                   | `model`, `audio_base64`; optionally `language`, `prompt`, `response_format`, `max_usd_per_second`                           |
| `decide`          | Answers to typed questions.                      | `state`, `questions`; optionally `model`, `orderings`                                                                       |
| `embed`           | Vectors for one text or up to 2,048 texts.       | `model`, `input`; optionally `dimensions`, `encoding_format`                                                                |

Every tool that buys also takes `cap_usd` and the [purchasing controls](#purchasing-controls).

### Preview a purchase

Call `dry_run` before `send_message`. It shows the estimated charge and the winning offer. It places no job and holds no funds.

| Argument                   | Meaning                                    |
| -------------------------- | ------------------------------------------ |
| `model`                    | The model to preview.                      |
| `input_tokens`             | Default 4,000.                             |
| `max_output_tokens`        | Default 1,000.                             |
| `cap_usd`, `task`, `tools` | Optional. `task` is `general` or `coding`. |

`kind` previews a job that is not chat. Send the size of the job with it:

| `kind`                | Size                                                                                 |
| --------------------- | ------------------------------------------------------------------------------------ |
| `image_generation`    | `n` (default 1), optionally `size`.                                                  |
| `audio_speech`        | `characters`, optionally `voice` (default `alloy`).                                  |
| `video_generation`    | Optionally `seconds` and `size`.                                                     |
| `audio_transcription` | `seconds` of audio.                                                                  |
| `embedding`           | Optionally `inputs` (default 1) and `dimensions`. `input_tokens` sizes each text.    |
| `decision`            | `questions`, optionally `orderings` (default 4). `input_tokens` sizes the situation. |

### Find a model

- `kind` takes one kind or a comma-separated list: `text`, `image`, `video`, `speech`, `transcription`, `embedding` or `decision`.
- `supports` (`list_models` only) keeps models that support a parameter, such as `input.image`.
- `max_price_usd` keeps the offers whose rate is at most that price. It needs exactly one `kind`. It leaves out offers priced in another unit, and router models.

| Kind                    | Unit of `max_price_usd`        |
| ----------------------- | ------------------------------ |
| `text`                  | A million output tokens        |
| `embedding`, `decision` | A million input tokens         |
| Other kinds             | One image, character or second |

### Buy an answer

`send_message` charges the balance by the tokens used. `cap_usd` is the most that the job can cost. An offer with a higher worst case does not compete.

The reply has the answer, the usage and the `exchange` block with the job ID and charge.

`send_message` takes `model` and a `prompt`, `messages` or both. Optional arguments:

| Group          | Arguments                                                                                                                             |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Input          | `system`, `image_urls` (`https` or `data:image` URLs), `attachments`                                                                  |
| Sampling       | `temperature`, `top_p`, `top_k`, `min_p`, `repetition_penalty`, `frequency_penalty`, `presence_penalty`, `seed`, `logit_bias`, `stop` |
| Output         | `logprobs`, `top_logprobs`, `reasoning_effort`, `response_format` (`text`, `json_object` or a JSON schema object), `max_tokens`       |
| Tools          | `tools`, `tool_choice`                                                                                                                |
| Cost and retry | `cap_usd`, `idempotency_key`                                                                                                          |

- `messages` holds earlier turns as `{role, content}`. The role is `user`, `assistant` or `tool`. The `prompt` follows them.
- An `assistant` turn can hold `tool_calls` and no content. A `tool` turn needs a `tool_call_id`.
- An `attachment` is a reference (see [Media and references](#media-and-references)), with inline data for audio, video and files.
- `max_tokens` defaults to 1024, which keeps the hold small.
- An argument that no offer supports gets the refusal of the endpoint, and nothing is charged.

When the model calls a tool, the reply has `tool_calls`. The server does not run them. To continue, send the `assistant` turn with its `tool_calls`, then a `tool` turn with the result.

To retry a call that timed out without a second purchase, send the same `idempotency_key`. Every tool that buys takes it. The job runs to its end even when the client leaves, so a retry without a key can buy twice.

### Other buying tools

#### decide

`decide` calls `POST /v1/systemone`. See the [Decisions API](https://inference.ai.exchange/docs/api-systemone.md).

- `state` is the situation, as text or data. `questions` holds typed questions by ID.
- `model` is a decision model from `list_models`. The default is `liquid.decision`. Liquid Auto cannot answer decisions.
- `orderings` sets how many orderings of the options each question gets. The cap holds one read of the prompt for each ordering.

#### embed

`embed` calls `POST /v1/embeddings` and charges per input token. Vectors are floats, so keep the list short.

#### transcribe

`transcribe` charges for each second of audio.

- Send the file as base64 or as a `data:audio/` URL in `audio_base64`. The limit is 1,000,000 characters, about 750 KB of audio. The tool does not fetch a URL. For a larger file, call `POST /v1/audio/transcriptions`.

#### Images and speech

`generate_image` and `generate_speech` return the media and the `exchange` block.

### Media and references

`create_video`, `generate_image` and `generate_speech` take `references`. `generate_image` and `create_video` also take `reference_image_urls`.

| Argument               | Meaning                                                                                                                                             |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `image_url`            | `create_video` only. An `https` or `data:image` address. The video starts from this frame.                                                          |
| `reference_image_urls` | Images that the output should keep.                                                                                                                 |
| `references`           | The full form: a list of `{type: "image", url}`, `{type: "audio", data, format}`, `{type: "video", url}` and `{type: "file", file_data, filename}`. |
| `callback_url`         | `create_video` only. An `https` address that the exchange calls when the job ends.                                                                  |

- A call has at most 16 references. The model must accept image input.
- A request to `/mcp` is limited to 20 MB. Base64 is a third larger than the file. For a large file, use an `https` address.

### Video

`get_video` takes `video_id` and optionally `wait_seconds` (up to 300), `include_file` and `chunk`. A completed job also returns:

- `download_url`, a link that works without a key for 15 minutes.
- The video file as an embedded resource, up to 4 MB. A client drops a result that is too big, with the link in it.

For a larger file, open `download_url`. If there is no link, send `chunk` (0, 1, and so on). Each chunk is 1 MB as base64, and the reply states `chunk_count`. A file can be at most 64 MB this way. Send `include_file` as `false` to get the status and the link only.

### Purchasing controls

The tools that buy, and `dry_run`, take the `x-liquid-*` header controls as arguments. A value that cannot be read is a tool error, and nothing is charged.

| Argument    | Header               | Value                                                                                                                                                                 |
| ----------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `retention` | `x-liquid-retention` | `none` keeps only offers whose provider does not retain the content. `allowed` is the default.                                                                        |
| `ttft_ms`   | `x-liquid-ttft-ms`   | The most time to the first token, in milliseconds.                                                                                                                    |
| `tag`       | `x-liquid-tag`       | A label for the spend. At most 63 letters, digits, dots, dashes and underscores.                                                                                      |
| `rules`     | `x-liquid-rules`     | An object with other rules of the `exchange` block, such as `exchange.privacy.regions`. It cannot set `exchange.candidates.models.allow` or `exchange.rank.strategy`. |

## Reference

### Resources and prompts

A resource read needs the same key as its tool. `liquid://models`, `book`, `markets`, `routers`, `balance` and `lifetime` hold the data of the tools with those names. `liquid://models/{model}`, `liquid://book/{model}`, `liquid://markets/{model}` and `liquid://fills/{model}` hold the data for one model. `liquid://jobs/{job_id}` holds one receipt.

`completion/complete` gives model IDs for `model` and `models`, and recent job IDs for `job_id`.

Three prompts write a request and buy nothing: `find_cheapest` (`task`, optionally `max_usd`), `audit_spend` (optionally `days`) and `compare_models` (`models`).

### Widgets

A client that supports MCP Apps shows a widget under some results. The tool names it in `_meta.ui.resourceUri`. A widget has no button that buys.

Each widget is `ui://liquid/<name>`: `models`, `model`, `book`, `receipts`, `job`, `markets`, `fills`, `account` (`get_balance`, `get_limits`), `image`, `speech`, `video` (`create_video`, `get_video`) and `dry-run`. The name follows the tool, such as `book` for `get_book`.

The widgets are resources with the media type `text/html;profile=mcp-app`. The server offers the `io.modelcontextprotocol/ui` extension.

### Results and progress

A result holds the data as text and as `structuredContent`. A list is the object `{ "items": [...] }`. A failed tool has no `structuredContent`.

`get_video` with `wait_seconds` checks the job until it completes, fails or the time ends. For a `notifications/progress` message at each check, the client must send `_meta.progressToken` and accept `text/event-stream`. A refusal for the grade of the key then arrives as a tool error, not a 403.

The server does not use elicitation. When a purchase is refused with a `proposed_retry`, the result says to ask the person.

### Tool errors

A failed tool returns a result with `isError` set. It holds the HTTP status and the error body of the endpoint. See [Errors](https://inference.ai.exchange/docs/errors.md).

### How the sign-in works

This section is for people who build a client.

1. A request to `/mcp` without a key returns `401` with a `WWW-Authenticate` header. It names the resource metadata (RFC 9728) at `/.well-known/oauth-protected-resource/mcp`.
2. That metadata names the authorization server (RFC 8414, `/.well-known/oauth-authorization-server`).
3. The client identifies itself in one of two ways.
   - **Metadata document.** The `client_id` is the `https` address of a JSON document, and the server metadata sets `client_id_metadata_document_supported` to `true`.
     - The address must have a path, no query or fragment, no port, and a public host name.
     - The document must name its own address as `client_id`, list `redirect_uris`, and use `token_endpoint_auth_method` `none`. It must be at most 8 KB with a JSON content type.
     - The exchange reads it at sign-in. It follows no redirect and refuses a host in its own network.
   - **Registration.** The client registers at the `registration_endpoint` (RFC 7591). All clients are public: `token_endpoint_auth_method` is `none`.
     - Each `redirect_uri` must use `https`, `http` on `localhost`, `127.0.0.1` or `[::1]`, or an app scheme.
     - An app scheme is a reverse-DNS name (it has a `.`) or one of `claude`, `cursor`, `goose`, `vscode`, `vscode-insiders`, `windsurf` and `zed`.
4. The client opens the `authorization_endpoint` with `response_type=code`, `code_challenge_method=S256` and a `state`. PKCE is required. A `resource` (RFC 8707) must be the address of the MCP server or the router origin, or the exchange returns `invalid_target`.
5. The `scope` is `mcp:read`, `mcp:spend`, or both. `mcp:spend` includes reading. Any other scope gets `invalid_scope`. With no scope, the client asks for `mcp:read`. The token response names the scope that the person granted, which can be less.
6. Each redirect to the client, success or error, carries `iss` (RFC 9207). The client must compare it with the issuer that it recorded and refuse a code on a mismatch.
7. The client sends the code to the `token_endpoint` with the `code_verifier`. The response has the key as `access_token`, with `token_type` `Bearer` and `expires_in` in seconds. There are no refresh tokens.

The `/mcp` route refuses a browser request with an `Origin` that is not the exchange or a loopback address.

A code works one time and ends after 5 minutes. If a client sends a code twice, the exchange revokes the key that the first request got. A wrong `code_verifier` uses up the code.

#### Asking for more access

A `401` carries `scope="mcp:read"` in `WWW-Authenticate`. If a key cannot do what a tool needs, the server answers `403` with `error="insufficient_scope"` and the needed scope, such as `scope="mcp:spend"`. The JSON body still has the tool error.
