Client libraries
MCP server
Give an agent the API through the Model Context Protocol — one URL, your key, sixteen tools, nothing to install.
/mcpon https://mcp.auray.aiThe 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 mcp add --transport http auray https://mcp.auray.ai/mcp \
--header "Authorization: Bearer $AURAY_API_KEY"{
"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
| Tool | What it does | The call it makes |
|---|---|---|
search_models | The 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_schema | What an address takes: its input schema, the scope a key needs, the hosts to call. | the address's openapi.json |
get_pricing | The 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_docs | Search this documentation; titles, URLs and the matching passage. | the documentation's search |
run_model | Run 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_job | Submit 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_job | Where a request is: IN_QUEUE, IN_PROGRESS or COMPLETED, with logs. | GET …/requests/{id}/status |
get_job_result | What a completed request produced; with assets: true, the signed download URLs too. | GET …/requests/{id}, then GET /v1/jobs/{id}/assets |
cancel_job | Ask a request to stop. | PUT …/requests/{id}/cancel |
upload_file | Put 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_model | Rank 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 |
chat | One 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_chat | Reads 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 |
embed | Vectors 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_account | The 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_community | What 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_modelanswers the result, or the job still running — itsrequest_id, its URLs,charged: "stands"and theidempotency_keyit ran under — forcheck_jobandget_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, andcancel_jobbefore 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 istool_failedwithretry: "same_key",charged: "unknown"and that key, andrun_modelagain with the same key and input is handed the same request — never a second charge.chatanswers the turn, or — when it is still being written —status: "IN_PROGRESS"with the turn'sjob_id, itsconversation_idand theidempotency_keyit was sent under. The turn is written to the end and charged for what it writes, andread_chatreads it: byjob_idunderjobs:read, or the whole thread byconversation_idunderchat:read. Sending the turn again would pay for it twice, and under the sameidempotency_keyis 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":{}}}}'{
"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"}}}'{
"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.