# MCP server

kenari runs a Model Context Protocol server that gives an agent the kenari documentation, the live model list, your account figures, web search, page fetch, X search and every kenari chat model. Connect it to claude.ai, ChatGPT, Claude Code, Cursor or another MCP client, and the agent can look things up, check your balance and ask another model for a second opinion without leaving its session.

The server is at `https://kenari.id/mcp`. It uses the Streamable HTTP transport: the client sends a JSON-RPC 2.0 message with `POST`. A tool that calls a model or the web answers with a server-sent event stream when the client accepts `text/event-stream`, as MCP clients do by default. The stream sends a heartbeat every 15 seconds, or a progress notification when the client sent a progress token, and then the result, so a long call is not cut off. Every other answer is one JSON response. There is no server-initiated stream, so `GET /mcp` returns `405`.

## Connect a client

The documentation tools and the model list need no account. For the rest, connect your kenari account in one of two ways: sign in from the client, or put an API key in its config.

### Sign in from the client

A client that supports MCP sign-in (OAuth) needs only the URL `https://kenari.id/mcp`, with no key. When the agent first calls a tool that needs an account, the client sends you to a kenari page in your browser:

1. Log in to kenari, if you are not logged in yet.
2. Check which app is asking. claude.ai and chatgpt.com show **Verified** next to their address. An app that registers itself, as most coding tools do, or one that runs on your computer, shows **Not verified**. That is normal for those apps: continue only if you started the connection yourself.
3. Check the account under **Signed in as**. To connect a different kenari account, select **Switch account**.
4. Under **Billed to**, pick the workspace that pays.
5. Select **Allow**.

The client then calls the tool again, signed in. It renews its sign-in on its own while you use it. After 90 days without use, it asks you to sign in again.

The connection gets its own API key, listed on **API Keys** with the **MCP connection** badge. Usage is billed to the workspace you picked, like any other key. Revoke that key to disconnect the client. If you set an expiry on it, the connection ends on that date.

### claude.ai and ChatGPT

Add kenari as a custom connector with the URL `https://kenari.id/mcp` and no key, then sign in when the connector asks.

### Claude Code

Add the server with the URL only:

```bash
claude mcp add --transport http kenari https://kenari.id/mcp
```

Then sign in: run `/mcp` in Claude Code, select kenari and choose **Authenticate**, or run `claude mcp login kenari` from your shell. Until you sign in, the public tools work, and a tool that needs an account marks kenari as needing sign-in in `/mcp`.

To use an API key instead, add it as a header:

```bash
claude mcp add --transport http kenari https://kenari.id/mcp \
  --header "Authorization: Bearer $KENARI_API_KEY"
```

Your shell expands `$KENARI_API_KEY` when you run the command, so the key is stored in Claude Code's MCP config.

### Cursor

Add the server to `~/.cursor/mcp.json`, or to `.cursor/mcp.json` in a project:

```json
{
  "mcpServers": {
    "kenari": {
      "url": "https://kenari.id/mcp",
      "headers": { "Authorization": "Bearer kn-..." }
    }
  }
}
```

### Other clients

Clients that document the `mcpServers` schema, such as Cursor, accept the entry above. For any other client that supports remote MCP servers over HTTP, set the URL `https://kenari.id/mcp`, then either sign in, if the client supports it, or add an `Authorization: Bearer $KENARI_API_KEY` header in that client's own settings.

## Send your key

With an API key, set it once, in the headers of the server entry. It is never a tool argument. kenari reads it from the `Authorization` header, with or without the `Bearer ` prefix, or from `x-api-key` if your client sends that style. When both are present, `Authorization` wins.

A request with no key and no sign-in can still call the public tools. A tool that needs an account answers it with HTTP `401`, which tells a client that supports sign-in to start it. A key that is wrong or revoked gets an error result telling you to check the header instead.

## Tools

| Tool | Needs an account | Billed | What it does |
| --- | --- | --- | --- |
| `kenari_search_docs` | No | No | Searches this documentation and returns the most relevant sections. |
| `kenari_list_models` | No | No | Lists models with live prices in Rupiah per 1,000,000 tokens. |
| `kenari_model_get` | No | No | Returns the full catalog record of one model, as `GET /v1/models` lists it: prices, context length, modalities and endpoints. |
| `kenari_model_changes` | No | No | Lists recent changes to the models: new and removed models, price changes and scheduled retirements, newest first. The HTTP equivalent is [Model changes](/en/docs/model-changes). |
| `kenari_ask` | Yes | Per token | Sends one prompt to a kenari chat model and returns its answer. |
| `kenari_balance` | Yes | No | Returns your balance in Rupiah. Refused for a restricted key. The HTTP equivalent is [Account balance](/en/docs/account-balance). |
| `kenari_usage` | Yes | No | Returns your usage for the last 30 days: requests, tokens and cost per model. For a restricted key, it covers only that key. The HTTP equivalent is [Usage](/en/docs/usage). |
| `kenari_quota` | Yes | No | Returns your plan and coupon quota. Refused for a restricted key. The HTTP equivalent is [Account quota](/en/docs/account-quota). |
| `kenari_usage_daily` | Yes | No | Returns your requests and cost per day over a window, the last 30 days by default. For a restricted key, it covers only that key. The HTTP equivalent is `GET /v1/usage/daily` in [Usage](/en/docs/usage). |
| `kenari_usage_log` | Yes | No | Lists your calls one by one, newest first, a page at a time: the model, its maker, how the call was paid, the tokens and the cost. For a restricted key, it covers only that key. The HTTP equivalent is `GET /v1/usage/log` in [Usage](/en/docs/usage). |
| `kenari_workspace_members` | Yes | No | Lists the members of your team workspace with their roles, seats and PAYG limits. Needs a key with workspace access. The HTTP equivalent is `GET /v1/workspace/members` in the [Workspace API](/en/docs/workspace-api). |
| `kenari_workspace_seats` | Yes | No | Lists the seats of your team workspace and who holds each one. Needs a key with workspace access. The HTTP equivalent is `GET /v1/workspace/seats`. |
| `kenari_workspace_invites` | Yes | No | Lists the pending invites of your team workspace. Needs a key with workspace access, held by an Owner or Admin. The HTTP equivalent is `GET /v1/workspace/invites`. |
| `kenari_web_search` | Yes | Per call | Searches the web and returns titles, URLs and snippets. |
| `kenari_web_fetch` | Yes | Per call | Fetches one URL and returns its text. |
| `kenari_x_search` | Yes | Per call | Searches live posts on X and returns a short answer with sources. |
| `kenari_ocr` | Yes | Per page | Reads a PDF or an image and returns its text. The HTTP equivalent is [OCR](/en/docs/ocr). |
| `kenari_transcribe` | Yes | Per second | Turns audio or a video's sound into text, plain or as subtitles. The HTTP equivalent is [Transcriptions](/en/docs/audio-transcriptions). |
| `kenari_translate_audio` | Yes | Per second | Turns speech in any language into English text, plain or as subtitles. |
| `kenari_image_generate` | Yes | Per image | Makes 1 to 4 images from a prompt and returns a preview and a download link for each. The HTTP equivalent is [Images](/en/docs/images). |
| `kenari_image_edit` | Yes | Per image | Edits or combines 1 to 4 images with a prompt. The HTTP equivalent is [Images](/en/docs/images). |
| `kenari_speech` | Yes | Per 1,000 characters | Turns text into speech and returns a download link. The HTTP equivalent is [Text to speech](/en/docs/audio-speech). |
| `kenari_music` | Yes | Per song | Makes a song from a prompt, lyrics or both, and returns a download link. The HTTP equivalent is [Music](/en/docs/music). |
| `kenari_video_generate` | Yes | Per second | Starts a video and waits up to 2 minutes for it. The HTTP equivalent is [Videos](/en/docs/videos). |
| `kenari_video_extend` | Yes | Per second | Continues a finished video from its last frame. |
| `kenari_video_status` | Yes | No | Checks a video by its id, and returns a fresh download link when it is done. |
| `kenari_embed` | Yes | Per token | Turns up to 64 texts into embedding vectors. The HTTP equivalent is [Embeddings](/en/docs/embeddings). |
| `kenari_rerank` | Yes | Per token | Ranks up to 100 documents by how well each one answers a query, with a score for each. The HTTP equivalent is [Rerank](/en/docs/rerank). |
| `kenari_systemone` | Yes | Per token | Answers typed questions about a state, or extracts typed fields from it, with a System One model. The HTTP equivalent is [System One](/en/docs/systemone). |

The search tools report the fee they charged at the end of the result. `kenari_ask` is billed by the tokens it uses, see [Billing](#billing).

Every result from a tool that runs a kenari API request, such as `kenari_ask`, `kenari_model_get`, the search tools and every billed tool, carries the request id in `_meta`, under `kenari.id/request_id`. Quote it when you contact support.

The three `kenari_workspace_*` tools are read-only. The key needs workspace access **Read** or higher, and the answer is the same JSON as the matching `GET` route of the Workspace API. A key without enough access, a shared key and a restricted key get an error result with the same code as the HTTP route, such as `workspace_access_required`. Changes are made only through the [Workspace API](/en/docs/workspace-api). The three tools do not work for a client connected through [Sign in from the client](#sign-in-from-the-client): its **MCP connection** key gets an error result that says so. Put a kenari API key created with workspace access in the client's config instead.

### Arguments

| Tool | Arguments |
| --- | --- |
| `kenari_search_docs` | `query` (string, required). |
| `kenari_list_models` | `search` (string, optional). Filters by model id or provider. |
| `kenari_model_get` | `model` (string, required). A model id from `kenari_list_models`. |
| `kenari_model_changes` | `since` (integer, unix seconds, optional): only changes confirmed at or after that time. `limit` (integer, 1 to 200, default 50). |
| `kenari_ask` | `model` (string, required), `prompt` (string, required), `system` (string, optional) and `max_tokens` (integer, optional, default 4,096, at most 32,768, and a larger value is lowered to 32,768). |
| `kenari_web_search` | `query` (string, required) and `max_results` (integer, 1 to 10, default 5, and a value outside that range is moved to the nearest limit). |
| `kenari_web_fetch` | `url` (string, required). |
| `kenari_x_search` | `query` (string, required) and `x_search_filter` (object, optional). |
| `kenari_ocr` | One input: `url` (string), `data` (string, base64) or `reuse_id` (string). With `data`, `mime_type` (string, optional). |
| `kenari_transcribe` | One input: `url` (string) or `data` (string, base64). With `data`, `mime_type` (string, optional). `model` (string, optional, the cheapest transcription model when left out), `language` (string, optional) and `format` (string, optional: `text` (default), `srt`, `vtt` or `verbose_json`). |
| `kenari_translate_audio` | One input: `url` (string) or `data` (string, base64). With `data`, `mime_type` (string, optional). `model` (string, optional: when left out, the transcription model kenari Studio uses if it translates, else the lowest-priced model that does) and `format` (string, optional: `text` (default), `srt`, `vtt` or `verbose_json`). The output is always English, so it takes no `language`. |
| `kenari_image_generate` | `prompt` (string, required), `model` (string, optional, the image model kenari Studio uses when left out), `size` and `quality` (strings, optional), `n` (integer, 1 to 4, default 1) and `preview` (boolean, default true). |
| `kenari_image_edit` | `prompt` (string, required) and `images` (1 to 4 images, required), plus an optional `mask`. Each image is an object with one `url` (a public link, or a kenari link from an earlier result) or `data` (base64) with an optional `mime_type`. `model`, `size`, `quality`, `n` and `preview` work as in `kenari_image_generate`. |
| `kenari_speech` | `text` (string, required, up to 4,000 characters), `model` (string, optional), `voice` (string, optional), `format` (string, optional: `mp3` by default, or `wav`, `pcm`, `opus`, `aac` or `flac` where the model makes it, and `wav` and `pcm` take up to 2,000 characters), `speed` (number, 0.25 to 4, optional) and `language` (string, optional). |
| `kenari_music` | `prompt` and `lyrics` (strings, give one or both), `instrumental` (boolean, optional), `model` (string, optional) and `format` (string, optional: `mp3` or `m4a`). |
| `kenari_video_generate` | `prompt` and `model` (strings, required), `duration` (integer, seconds, optional), `aspect_ratio` and `resolution` (strings, optional), and the images `image` (the first frame), `end_image` (the last frame) and `reference_images` (up to 4), each an object with one `url` or `data`. |
| `kenari_video_extend` | `id` and `model` (strings, required), `prompt` (string, optional) and `duration` (integer, seconds, optional). |
| `kenari_video_status` | `id` (string, required). |
| `kenari_embed` | `model` (string, required) and `input` (an array of 1 to 64 strings, required). Vectors from two models do not compare, so the model has no default. |
| `kenari_rerank` | `query` (string, required), `documents` (an array of 1 to 100 strings, each up to 8 KB and 512 KB together, required), `top_n` (integer, optional, default all) and `model` (string, optional, the lowest-priced rerank model when left out). |
| `kenari_systemone` | `model` (string, required), plus the body of `POST /v1/systemone` as fields: `state`, then `questions` or `fields`, and optionally `assets`, `images`, `audio`, `videos`, `reference_date` and `options`. kenari checks them the same way as the endpoint. |
| `kenari_usage_daily` | `range` (string, optional: `today`, `24h`, `7d` or `30d`, default `30d`), `start` and `end` (integers, unix seconds, optional, and they win over `range`) and `model` (string, optional). |
| `kenari_usage_log` | The same as `kenari_usage_daily`, plus `page` (integer, from 0, default 0), `limit` (integer, 1 to 200, default 50) and `billing_kind` (string, optional: `payg`, `plan`, `coupon`, `token_alloc`, `free` or `byok`). |
| `kenari_balance`, `kenari_usage`, `kenari_quota`, `kenari_workspace_members`, `kenari_workspace_seats`, `kenari_workspace_invites` | None. |

`x_search_filter` takes the same fields as `POST /v1/x/search`. `allowed_x_handles` and `excluded_x_handles` each hold at most 20 handles without the `@` and cannot be used together. `from_date` and `to_date` are `YYYY-MM-DD`. `enable_image_understanding` and `enable_video_understanding` read the media inside posts. See [Web and X search](/en/docs/web-search) for the full table.

### Files for OCR and transcription

`kenari_ocr`, `kenari_transcribe` and `kenari_translate_audio` take their file in one of two ways. Send exactly one.

- **`url`**: a public `http` or `https` link that kenari downloads. It must answer `200` with the file itself, on the standard port. kenari follows up to 3 redirects, so share and download links work. A link to a private network address, or one that needs a login, is refused. Up to 15 MiB for OCR and 25 MiB for audio.
- **`data`**: the file itself, base64-encoded, up to 10 MiB. This suits scripts and local tools that already have the file. A model cannot write a large file into a tool call, so for anything but a small file, use a link.

kenari checks the file's content, not only its name or declared type. A link that answers a web page instead of a PDF is refused before anything is read or charged. OCR accepts PDF, PNG, JPEG, WebP, GIF and TIFF. Transcription and translation accept MP3, M4A, MP4, WAV, WebM, OGG and FLAC.

`reuse_id`, from a previous `kenari_ocr` result, reads the same document again at no charge.

### Files kenari makes

The generation tools answer with download links on `kenari.id`. A link works without a key for anyone who has it, until it expires, so share it only where you mean to.

- Images, speech and songs are kept for 24 hours.
- A video's link works for 6 hours. `kenari_video_status` gives a fresh link while the video is still there, about 6 hours after it finishes. Download what you want to keep.
- `kenari_image_generate` and `kenari_image_edit` also show a small JPEG preview of each image in the result, so the model can see what it made. Set `preview` to `false` to leave it out.
- kenari keeps the most recent 1 GiB of files per account. Past that, the oldest links stop working early.
- A kenari link from an earlier result can be the input of `kenari_image_edit` or a video tool. It must come from your own account.
- `kenari_embed` shows the vectors in the result when they fit in 60,000 characters. A larger answer is kept as a JSON file for 24 hours, and the result gives its link, the number of vectors and their dimensions.

## Billing

The three search tools are billed per call from your account, the same as the endpoints in [Web and X search](/en/docs/web-search). Web search and page fetch share one daily plan allowance, and `kenari_x_search` is billed from your balance only.

`kenari_ask` is billed exactly like the same request to [Chat completions](/en/docs/chat): at the model's price per token, from your plan, coupon or balance by the same rules. Its answer stops at `max_tokens`, 4,096 tokens unless you ask for more, so one call costs no more than you allow.

`kenari_ocr` is billed per page read, from your balance only, and `kenari_transcribe` and `kenari_translate_audio` per second of audio at the model's price, exactly like [OCR](/en/docs/ocr) and [Transcriptions](/en/docs/audio-transcriptions). A file that is refused is not charged.

The generation tools are billed exactly like the same request to [Images](/en/docs/images), [Text to speech](/en/docs/audio-speech), [Music](/en/docs/music) and [Videos](/en/docs/videos): per image, per 1,000 characters, per song and per second of video. A video is charged when it starts and refunded if it fails. Each result states what it cost. At most 2 image, speech or music calls run at once per account.

`kenari_embed`, `kenari_rerank` and `kenari_systemone` are billed per input token, exactly like [Embeddings](/en/docs/embeddings), [Rerank](/en/docs/rerank) and [System One](/en/docs/systemone). Each result states what it cost. `kenari_usage_daily`, `kenari_usage_log` and `kenari_model_changes` are free.

A call that fails is not charged. See [How billing works](/en/docs/billing).

## Tool list for clients

`GET https://kenari.id/v1/tools` returns the same tools with no key: each tool's input schema, its MCP annotations and how it is billed (`free`, `per_call`, `per_token`, `per_page`, `per_second`, `per_image` or `per_1k_chars`). A client can read it to build its own commands. See [Docs for AI agents](/en/docs/agent-docs) for the OpenAPI spec.

## Shared API keys

A shared API key cannot access the owner's financial information. `kenari_balance`, `kenari_usage`, `kenari_usage_daily`, `kenari_usage_log` and `kenari_quota` are refused for a shared API key, because they would reveal the owner's balance, usage and quota. The search tools and `kenari_ask` still work with a shared API key, within what the share allows. `kenari_ask` follows the key's model restriction and caps, like the chat endpoint. Use a key from your own account to read those figures.

A restricted key, meaning one with a model restriction, a spend cap or a token cap, is refused by `kenari_balance` and `kenari_quota`. `kenari_usage`, `kenari_usage_daily` and `kenari_usage_log` work but show only that key's own usage. See [Account overview](/en/docs/account).

## See also

- [Docs for AI agents](/en/docs/agent-docs)
- [Web and X search](/en/docs/web-search)
- [Authentication and API keys](/en/docs/authentication)
- [Workspace API](/en/docs/workspace-api)
- [Claude Code](/en/docs/claude-code)
