Docs

Client libraries

MCP server

Give an agent the API through the Model Context Protocol — one URL, your key, sixteen tools, nothing to install.

POST/mcpon https://mcp.auray.ai

The API is itself an MCP server. Point an agent at it with your key and it can find a model, read what the model takes, run it, follow the request, fetch the result, upload a file and search this documentation — through eleven tools that follow a common MCP tool set by name (Compatibility), so a configuration written for it needs a new URL and nothing else. Four more go further: chat talks to one of our chat models and read_chat reads its thread back, embed turns text into vectors, and get_account says what the key's account holds and where its credits went.

The address is https://mcp.auray.ai/mcp. Until 28 September 2026 it was https://api.auray.ai/v1/mcp, and that address still answers — the same server, not a redirect — so an agent already configured with it keeps working without a change.

#Connecting

The transport is Streamable HTTP with one message per request. There is no session to open and nothing to install; the key goes in the Authorization header the way it does for every other call.

Claude Code
claude mcp add --transport http auray https://mcp.auray.ai/mcp \
  --header "Authorization: Bearer $AURAY_API_KEY"
Any client that reads an mcpServers block
{
  "mcpServers": {
    "auray": {
      "url": "https://mcp.auray.ai/mcp",
      "headers": { "Authorization": "Bearer auray_sk_…" }
    }
  }
}

The key you connect with is the key the agent spends. A tool has exactly the authority the key has — its scopes, its ceiling, its rate buckets — and no more, so mint a key for the agent with the scopes it needs and a ceiling you are comfortable with. API keys shows where.

#The sixteen tools

ToolWhat it doesThe call it makes
search_modelsThe catalogue, filtered by a word, a category, a status or a kind — kind chat lists the models chat takes and embedding the model embed takes, each by the name that tool takes. A catalogue language model (category llm) is an address like any other.GET /v1/models
get_model_schemaWhat an address takes: its input schema, the scope a key needs, the hosts to call.the address's openapi.json
get_pricingThe price rule an address publishes, in credits — and covered_by_your_pass when a pass you hold runs that model without spending credits: until when, whether it works through MCP, and what it covers.the address's openapi.json, and GET /v1/me
search_docsSearch this documentation; titles, URLs and the matching passage.the documentation's search
run_modelRun an address and wait for the answer, up to 40 seconds. A job still running then comes back as the job — its request_id, status_url, charged: "stands" and the idempotency_key it ran under — to follow with check_job, not to run again. A job a pass or a promotion pays for costs nothing; the answer is its output and does not say what paid — submit_job's does, in coverage. A pass takes a job at a time, and another sent meanwhile is 429 pass_busy, uncharged.POST on the synchronous host
submit_jobSubmit without waiting; answers the request id and its three URLs. priority, max_queue_length and request_timeout_seconds shape the queue of a model that queues (lifecycle.queue); sent to one that refuses when full instead, priority or max_queue_length is 422 queue_not_offered before anything is charged, and request_timeout_seconds, a time to start, is accepted. A job a pass or a promotion pays for costs nothing and says so in coverage.POST on the queue host
check_jobWhere a request is: IN_QUEUE, IN_PROGRESS or COMPLETED, with logs.GET …/requests/{id}/status
get_job_resultWhat a completed request produced; with assets: true, the signed download URLs too.GET …/requests/{id}, then GET /v1/jobs/{id}/assets
cancel_jobAsk a request to stop.PUT …/requests/{id}/cancel
upload_filePut a file where a request can reach it — up to 3 MB inline as base64 — and get the signed URL for the field that takes it.POST /v1/uploads and the two calls after it
recommend_modelRank addresses for a task in plain words: connected first, then described, then by what each address makes and takes — a word its category makes counts three, one it takes two, a word of the address itself one, and photo, clip, song or mesh count as image, video, audio and 3d. Arithmetic, not judgement.GET /v1/models
chatOne turn with one of our three chat models (glm-5-3-flash, deepseek-v4-1-flash, kimi-k3, or a slot's address auray-ai/<slot>), optionally inside a thread (conversation_id), with max_output_tokens, effort and idempotency_key. A turn still being written after 40 seconds comes back as status: "IN_PROGRESS" with its job_id, its conversation_id and the idempotency_key it was sent under, and is written to the end; read_chat reads it. A catalogue language model is not taken here: it is an address, for run_model or submit_job. Charges credits; needs text:write (or chat:write on an older key).POST /v1/chat
read_chatReads back what chat wrote — how a turn it answered as still being written is read. By job_id, the turn itself, running and then succeeded with its content (needs jobs:read); by conversation_id, the whole thread in order, a page at a time (needs chat:read, which text:write and chat:write do not include). Free.GET /v1/jobs/{id} or GET /v1/chat/conversations/{id}/messages
embedVectors for a string or a list of strings from Qwen/Qwen3-Embedding-8B. Charges credits by the token; needs embeddings:write, which text:write does not include. The first one after a quiet spell can take longer than an agent waits by default — see below.POST /openai/v1/embeddings
get_accountThe key's plan, credit balance, scopes and ceiling, and the passes held; with usage: true, where the credits went (needs jobs:read). Free.GET /v1/me, then GET /v1/usage
search_communityWhat people published to the Community: works by words (every word a prefix of a title, description, model name or a shown prompt), or without words a feed — trending, new, or top over a window, by kind or model — or public profiles by handle or name. Each work comes with its page, its model, that it is AI-generated, its licence text and its files. Reads only: publishing, liking and commenting stay on the API, behind community:write. Free; needs no scope.GET /v1/community/search or GET /v1/community/feed

Each tool is one call to this API's public origins under the key you connected with. Every tool also says what it does to a client that asks: the ten that only read carry readOnlyHint: true, and the six that spend, upload or cancel say they do not, so a client that runs read-only tools without asking still asks before one that charges. Nothing is cached on the server and nothing decides on the agent's behalf: when the API refuses — not enough credits, a scope the key lacks, an address that is described but not connected — the refusal comes back as the tool's answer with isError: true and the same error, error_type, message, docs and request_id a curl would get, beside the API's own advice: retry says whether sending it again can help (never, same_key, new_key, poll_first, after_retry_after) and charged what happened to the money (no, refunded, stands, unknown). The agent reads it the way you would. A refusal that was charged also keeps the job it names — queue_request_id and its URLs, with charged: "stands" — so the agent follows the job it paid for rather than paying for it again, and a chat turn resent under a key that already ran keeps the message_id it names and the conversation_id it was sent to, beside the turn's job_id — the one read_chat takes — to read rather than pay for twice. A failure of run_model or chat names the idempotency_key the call went out under, so the agent's resend is the same request, never a second one under a fresh key. An argument a tool cannot use comes back the same way, as invalid_arguments with retry: "never" and charged: "no", so the agent can correct the call — and an argument the money rides on is never dropped: an idempotency_key that is not a string, or an input that is not an object, is refused before anything is sent. A refusal that says to wait (after_retry_after) is handed back at once with its retry_after_seconds, never waited out inside the tool.

#Long work comes back as something to follow

run_model and chat never hold a call past an agent's own timeout. The MCP TypeScript SDK gives a request a minute by default, and a job can take far longer, so these two tools answer 40 seconds after the request reaches the server at the latest, with what to follow:

  • run_model answers the result, or the job still running — its request_id, its URLs, charged: "stands" and the idempotency_key it ran under — for check_job and get_job_result. A run still waiting in the queue to start is answered as the API answers it, charged: "no": it is charged when it starts, and cancel_job before then costs nothing. Every run goes out under a key: yours, or one the server names for you, and every failure names it. In the rare case the synchronous host has not answered at all 50 seconds after the request arrived, the answer is tool_failed with retry: "same_key", charged: "unknown" and that key, and run_model again with the same key and input is handed the same request — never a second charge.
  • chat answers the turn, or — when it is still being written — status: "IN_PROGRESS" with the turn's job_id, its conversation_id and the idempotency_key it was sent under. The turn is written to the end and charged for what it writes, and read_chat reads it: by job_id under jobs:read, or the whole thread by conversation_id under chat:read. Sending the turn again would pay for it twice, and under the same idempotency_key is refused.

embed is not bounded this way, and cannot be from here. The first embed after a quiet spell can take up to 3 minutes — longer than an MCP client waits by default, and the embeddings route offers no way to ask it to answer sooner. So give embed a longer timeout than that. A batch your client gave up on is still embedded and charged, and vectors are not stored: send an idempotency_key, and a resend under it is refused (in_flight, key_settled) rather than charged twice. Past that wait it is refused 503 embeddings_warming, with retry_after_seconds and nothing charged.

#The protocol, by hand

The server speaks JSON-RPC 2.0, one message per POST, in both eras of the protocol, and keeps no session in either.

Revision 2026-07-28 has no handshake. Every request carries its revision and the client's capabilities in _meta, and three headers mirror the body: MCP-Protocol-Version, Mcp-Method and, on tools/call, Mcp-Name. Its methods are server/discover, tools/list and tools/call, and every result says resultType: "complete".

curl -X POST https://mcp.auray.ai/mcp \
  -H "Authorization: Bearer $AURAY_API_KEY" \
  -H "content-type: application/json" \
  -H "MCP-Protocol-Version: 2026-07-28" \
  -H "Mcp-Method: server/discover" \
  -d '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'
200 OK
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "resultType": "complete",
    "supportedVersions": ["2026-07-28", "2025-11-25", "2025-06-18", "2025-03-26"],
    "capabilities": { "tools": { "listChanged": false } },
    "instructions": "Every model is an address (`auray-ai/krea-2-turbo/text-to-image`). …",
    "ttlMs": 3600000,
    "cacheScope": "public",
    "_meta": { "io.modelcontextprotocol/serverInfo": { "name": "auray", "version": "2" } }
  }
}

tools/list says how long its answer may be kept (ttlMs, cacheScope: "public": the list is the same for every key). A request the revision calls malformed is refused 400 with a JSON-RPC error and nothing runs: -32020 for a header that is missing or does not match the body, -32602 for _meta without its two fields, and -32022 for a revision this server does not serve, with the ones it does in data.supported. A method the revision does not have — ping and initialize among them — is 404 with -32601; so is subscriptions/listen, since this server has nothing to notify (listChanged: false). supportedVersions names all four revisions the address serves, but the three from 2025 have a handshake: a request in this shape that names one of them is -32022 naming 2026-07-28 alone, and those revisions are reached through initialize.

Revisions 2025-03-26, 2025-06-18 and 2025-11-25 open with initialize, which agrees to the revision asked for (or 2025-11-25 when it is none of the three), then tools/list and tools/call under the MCP-Protocol-Version it agreed; ping is answered too. A request with no MCP-Protocol-Version header is served as 2025-03-26. An initialize is a handshake whatever header comes with it. This is how Claude Code, Cursor and the official SDKs connect today.

curl -X POST https://mcp.auray.ai/mcp \
  -H "Authorization: Bearer $AURAY_API_KEY" \
  -H "content-type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'
200 OK
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": { "tools": { "listChanged": false } },
    "serverInfo": { "name": "auray", "title": "Auray", "version": "2" },
    "instructions": "Every model is an address (`auray-ai/krea-2-turbo/text-to-image`). …",
    "_meta": { "io.modelcontextprotocol/serverInfo": { "name": "auray", "version": "2" } }
  }
}

In both eras a notification is answered 202 with no body, and a request without an id is refused 400 and never run — a tools/call without one does not call the tool. A response posted by the client is refused 400 with no id: this server asks nothing. A batch (an array of up to 20 messages) is answered under 2025-03-26, the one revision that has them — with no MCP-Protocol-Version header, or that one — as an array of answers (202 if it held only notifications), each message taking a read from the key's bucket as its own request would; under every other revision it is refused 400. A body that is not JSON is -32700. GET and DELETE, which older revisions used to open a server-initiated stream and to end a session, answer 405 with Allow: POST, because there is neither.

The response carries the revision served in the MCP-Protocol-Version header, and the X-Request-Id and rate-limit headers every platform call carries. Without a key the answer is the API's own 401, in its own envelope, before any protocol is spoken. A request carrying an Origin header — a browser's — is 403 browser_origin_refused before even that, with a key or without: the server has no browser caller, and a key in a browser is a key that has been published.

The reference page for the operation is POST /v1/mcp, the platform address the server answers at as well.

Start typing to search every page of the documentation.

Up arrow Down arrow to moveEnter to openEscape to close