Cluvia API — MCP server
Drive your Cluvia companies from Claude Code, Cursor, VS Code or any other Model Context Protocol (MCP) client: list your companies, give agents tasks, read what they produced, decide approvals, schedule recurring work and grow the team.
https://mcp.usecluvia.com/mcpAuthorization: Bearer cluvia_pat_…1. Overview #
Cluvia runs AI companies: each company has a team of AI agents (a CEO, marketers, writers, website builders and more) that do real work — research, writing, websites — on Cluvia's servers. The Cluvia MCP server is the programmatic door to the same account you use in the app. Anything that speaks MCP can connect to it with a personal access token and act on your behalf.
With it you can:
- See your companies — their teams, open tasks, recent outputs and how many approvals are waiting (
list_companies,get_company). - Give an agent work and wait for the result (
assign_task,wait_for_run,run_status). A task whose title asks for a website builds one. - Read outputs — the deliverables and websites your agents produced (
list_outputs,get_output). - Decide approvals — publishing a site, sending an email reply, starting or finishing an A/B test, a connected-tool call (
list_approvals,approve,deny). - Schedule recurring work with a cron expression in your time zone (
list_schedules,create_schedule,delete_schedule). - Shape the team — hire from the role library and set each agent's standing policy for outside actions (
hire_agent,set_policy).
The server exposes 15 tools. It does not expose prompts or resources. A typical session: list_companies for a companyKey, get_company for the team, assign_task to give an agent work, then wait_for_run for the result. Runs start on Cluvia's servers within seconds when there is room, use your plan's credits, and appear on the app's task board when they finish.
The server also sends these instructions to your client when it connects, so your AI assistant knows how to start:
Cluvia runs AI companies whose agents do real work. Start with list_companies for a companyKey and get_company for the team, then assign_task to give an agent work and wait_for_run for the result. Runs start on Cluvia's servers within seconds when there is room. hire_agent is queued and applies within about a minute (by the Cluvia app when it has that company open, or by Cluvia when nobody does); set_policy applies at once.
2. Quick start #
Step 1: create an access token
- Sign in to Cluvia at www.usecluvia.com.
- Open Connections in the sidebar.
- In the panel Connect Claude Code, Cursor or Claude Desktop, give the token a name (for example “My laptop”).
- Tick the Permissions it needs. The default, Read and Run tasks, is enough for this quick start. Add Approve and perform to decide approvals and Administer to hire agents or set policies (see permissions).
- Leave Company on All companies, or pick one company to limit the token to it.
- Click Create token. The token (
cluvia_pat_followed by 32 letters and digits) is shown once, with ready-made setup snippets. Copy it now.
Step 2: add the server to Claude Code
claude mcp add --transport http cluvia https://mcp.usecluvia.com/mcp --header "Authorization: Bearer cluvia_pat_YOUR_TOKEN"
Replace cluvia_pat_YOUR_TOKEN with your token. Check the connection with claude mcp list, or with /mcp inside a Claude Code session. Other clients are covered in Connecting your client.
Step 3: make a first call
Ask your assistant in plain words. It picks the tools.
| You say | What happens |
|---|---|
| “List my Cluvia companies.” | The assistant calls list_companies and shows each company's name, industry, team size and open tasks, with its companyKey. |
| “Who is on the team at Sunrise Bakery?” | get_company: every agent's id, name, role, department and status, plus open tasks, recent outputs and pending approvals. |
| “Ask the Content Writer at Sunrise Bakery to draft a launch email for the spring menu, and show me the result.” | assign_task queues a run and returns a runId; wait_for_run waits (up to 50 seconds by default, longer if asked) and returns the finished text. The task also appears on the app's task board. |
| “Is anything waiting for my approval?” | list_approvals lists pending actions, such as publishing a site. With the Approve and perform permission, the assistant can approve or deny them when you tell it to. |
Prefer raw HTTP? The same first call with curl:
curl -sS https://mcp.usecluvia.com/mcp \
-H "Authorization: Bearer $CLUVIA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_companies","arguments":{}}}'
3. Endpoint and transport #
Endpoint
POST https://mcp.usecluvia.com/mcp
The path is exactly /mcp. The earlier address, https://companyos-ai-proxy.adamidrissi177.workers.dev/mcp, is the same server and keeps working, so existing client configurations do not need to change. New setups should use https://mcp.usecluvia.com/mcp.
The host mcp.usecluvia.com serves only the MCP endpoint. Opening https://mcp.usecluvia.com/ (or any other path) in a browser redirects to this page; paths under /.well-known/ and requests other than GET or HEAD to any path but /mcp answer 404.
Transport
- Streamable HTTP, request–response only. Send one JSON-RPC 2.0 message per
POST. Every answer is a single JSON object withContent-Type: application/jsonandCache-Control: no-store. The server never opens a Server-Sent Events stream, so theAcceptheader does not change the answer. - No sessions. The server issues no
Mcp-Session-Id; every request stands alone and must carry theAuthorizationheader. - No batches. A JSON array is refused with HTTP 400.
- Request body limit: 64 KB. Larger bodies get HTTP 413.
GET,DELETE,OPTIONSand every method other thanPOSTget HTTP 405 withAllow: POST. There is no stream to open withGETand no session to end withDELETE.- Notifications (messages without an
id, such asnotifications/initializedornotifications/cancelled) are accepted with HTTP 202 and no body. Responses a client sends (messages withresultorerrorand nomethod) also get 202. The server never sends requests or notifications to the client. - Headers
MCP-Protocol-Version,Mcp-MethodandMcp-Nameare informational. A request is never refused because of them; the protocol version is read from the message body (below). - Browsers: a request carrying an
Originheader from another website is refused with HTTP 403 (a guard against DNS rebinding). The endpoint sends no CORS headers. Call it from an MCP client, a script or a server, not from a web page.
Protocol versions
Supported MCP protocol versions, newest first:
| Version | How the server answers |
|---|---|
2026-07-28 | Current shape: every successful result also carries "resultType": "complete" and _meta["io.modelcontextprotocol/serverInfo"]. An unknown method gets HTTP 404. |
2025-11-25 | Earlier shape: the client opens with initialize; results are plain JSON-RPC results. An unknown method gets HTTP 200 with a JSON-RPC error. |
2025-06-18 | |
2025-03-26 |
How the version is chosen:
- Only
params._meta["io.modelcontextprotocol/protocolVersion"]in the request body decides. If it is missing, the request is served in the earlier shape and is never refused for that. - If it is
2026-07-28, the request is served in the current shape. Any other supported version gets the earlier shape. - If it is present but not a supported version, the server answers HTTP 400 with JSON-RPC error
-32022"Unsupported protocol version"anddata: { "supported": [...], "requested": ... }. initializeanswers with theprotocolVersionthe client asked for when it is2025-11-25,2025-06-18or2025-03-26, and with2025-11-25otherwise.
Methods
| Method | Result |
|---|---|
initialize | { protocolVersion, capabilities: { tools: { listChanged: false } }, serverInfo, instructions } |
ping | {} |
server/discover | { supportedVersions, capabilities: { tools: {} }, instructions } |
tools/list | { tools } — only the tools your token's permissions allow. The list does not change during a connection (listChanged: false) and is not paginated. |
tools/call | Runs one tool. See Tool results. |
| anything else | JSON-RPC error -32601 "Method not found: <method>". |
serverInfo is { "name": "cluvia", "title": "Cluvia", "version": "1.0.0", "websiteUrl": "https://www.usecluvia.com/developers" }. websiteUrl is left out for 2025-06-18 and 2025-03-26, whose schemas do not define it. Authentication is checked before the body is read, so even initialize needs a valid token.
Example: initialize
Request:
POST /mcp HTTP/1.1
Host: mcp.usecluvia.com
Authorization: Bearer cluvia_pat_YOUR_TOKEN
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": { "name": "example-client", "version": "1.0.0" }
}
}
Response (HTTP 200):
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": { "tools": { "listChanged": false } },
"serverInfo": { "name": "cluvia", "title": "Cluvia", "version": "1.0.0" },
"instructions": "Cluvia runs AI companies whose agents do real work. Start with list_companies for a companyKey and get_company for the team, then assign_task to give an agent work and wait_for_run for the result. ..."
}
}
(A client asking for 2025-11-25 also gets "websiteUrl": "https://www.usecluvia.com/developers" in serverInfo.) Then send the notifications/initialized notification (answered with 202) and call tools/list.
Example: a 2026-07-28 request
Clients on 2026-07-28 name the version in _meta on each request:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": { "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28" } }
}
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"resultType": "complete",
"tools": [
{
"name": "list_companies",
"title": "List companies",
"description": "List the companies in this Cluvia account: name, industry, team size and open tasks. Each has a companyKey the other tools take.",
"inputSchema": { "type": "object", "properties": {}, "additionalProperties": false },
"annotations": { "readOnlyHint": true, "openWorldHint": false }
}
],
"_meta": {
"io.modelcontextprotocol/serverInfo": { "name": "cluvia", "title": "Cluvia", "version": "1.0.0", "websiteUrl": "https://www.usecluvia.com/developers" }
}
}
}
(The real answer lists every tool your token may call; one is shown.)
Tool results
A successful tools/call returns the data twice: as pretty-printed JSON text in content[0].text, and as an object in structuredContent.
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [{ "type": "text", "text": "{\n \"companies\": [ ... ]\n}" }],
"structuredContent": { "companies": [ ... ] },
"isError": false
}
}
A tool that cannot do what was asked returns a normal result with isError: true and a readable sentence, and no structuredContent. The HTTP status is 200. Your assistant can read the sentence and correct itself.
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [{ "type": "text", "text": "No company has the companyKey \"company-x:2026-01-01T00:00:00.000Z\" — call list_companies for the keys." }],
"isError": true
}
}
Every tool's input schema sets additionalProperties: false. Read-only tools carry the annotations { "readOnlyHint": true, "openWorldHint": false }; the others list their own annotations below.
4. Authentication #
Every request needs a personal access token in the Authorization header:
Authorization: Bearer cluvia_pat_YOUR_TOKEN
The word Bearer followed by one space is required, with that capitalisation.
Personal access tokens
- Create: in the app, Connections → Connect Claude Code, Cursor or Claude Desktop → Create token (steps in Quick start). Token management needs a signed-in app session: a token cannot create, list or revoke tokens.
- Format:
cluvia_pat_followed by 32 base62 characters (0-9,A-Z,a-z), for examplecluvia_pat_Ab12Cd34Ef56Gh78Ij90Kl12Mn34Op56(not a real token). - Shown once: Cluvia stores only a SHA-256 hash of the token, so it cannot show it again. The token list shows its name, the first characters (
cluvia_pat_XXXX…), its permissions and when it was last used. - Limit: up to 10 live tokens per account.
- Expiry: tokens do not expire. Revoke a token you no longer use.
- Revoke: click the trash icon next to the token on the Connections page. It stops working on the next request.
- No plan requirement: every plan, Free included, can create tokens and use the MCP server. What a call may spend is governed by your plan (see Plans and credits).
Permissions
Each token carries one or more permissions. They are independent: Administer does not include Read. The app ticks Read and Run tasks for a new token; change the ticks before you create it. tools/list shows only the tools a token may call.
| In the app | Name | Tools | Note |
|---|---|---|---|
| Read | read | list_companies, get_company, list_outputs, get_output, run_status, wait_for_run, list_approvals, list_schedules | Reads everything in the account the token can reach. |
| Run tasks | tasks | assign_task, create_schedule, delete_schedule | Spends your credits. There is no separate spending cap on a token. |
| Approve and perform | approve | approve, deny | Approving carries the action out: publish a site, send an email. |
| Administer | admin | hire_agent, set_policy | Changes the team and the agents' standing policies. |
Calling a tool without its permission returns an isError result and does nothing (it does not count toward the rate limit):
This access token doesn't have the "admin" permission this needs. Create a token that has it on the Connections page in Cluvia.
Tokens created before permissions existed act as the whole account and appear in the app as Full access (legacy). Replace them with a token that has only the permissions you need.
Company-limited tokens
- A token limited to one company reaches only that company.
list_companiesreturns just that one. - Any other company answers exactly like a company that does not exist:
No company has the companyKey "…" — call list_companies for the keys. - Runs, approvals and schedules are judged by the company they belong to: another company's run answers
No run "…" in this account., its approvalApproval not found., its scheduleSchedule not found. - Cluvia's site-management routes, which the app uses, refuse a one-company token any site that is not bound to its company (the app warns: “One-company tokens can't publish or manage sites.”). Over MCP, such a token with Approve and perform can still
approveapublish_siteapproval of its own company. - Deleting the company revokes every token limited to it. Account-wide tokens are not affected.
- Deleting your account deletes all of its tokens.
What a refused request looks like
Authentication failures are answered at the HTTP level, before the message is read, with a JSON-RPC error body (code -32600, no id). A 401 also carries WWW-Authenticate: Bearer realm="cluvia".
HTTP/1.1 401 Unauthorized
Content-Type: application/json
WWW-Authenticate: Bearer realm="cluvia"
{"jsonrpc":"2.0","error":{"code":-32600,"message":"This access token is not valid — it may have been revoked. Create a new one on the Connections page in Cluvia."}}
| HTTP | Message | What to do |
|---|---|---|
| 401 | Connect with a Cluvia access token: send Authorization: Bearer cluvia_pat_… (create one on the Connections page in Cluvia). | Add the header, spelled exactly Authorization: Bearer <token>. |
| 401 | This access token is not valid — it may have been revoked. Create a new one on the Connections page in Cluvia. | The token is mistyped or revoked. Create a new one. |
| 401 | This access token's permissions could not be read, so it was refused. Revoke it and create a new one on the Connections page in Cluvia. | Revoke it and create a new one. |
| 401 | The account this access token belongs to can no longer use Cluvia. | The account was deleted or suspended. Contact support if this is unexpected. |
| 401 | Guest sessions cannot use Cluvia tools — create an account or sign in. | Only for an app guest session used as a bearer. Create an account and a token. |
| 503 | Could not check your access token right now — try again in a moment. | Retry after a short wait. |
There is no OAuth: the server publishes no OAuth or protected-resource metadata, and a token is the only way for an outside tool to sign in. (The app's own sign-in session also works as a bearer, with every permission, but outside tools should use a token.)
5. Connecting your client #
Any MCP client that supports the Streamable HTTP transport and lets you set a request header can connect. Each client writes its configuration a little differently; the snippets below follow each client's own documentation. In every snippet, replace the token placeholder with yours, or keep it in an environment variable as shown.
Claude Code
claude mcp add --transport http cluvia https://mcp.usecluvia.com/mcp --header "Authorization: Bearer cluvia_pat_YOUR_TOKEN"
- Add
--scope userto use the server in every project, or--scope projectto share it through the project's.mcp.json. The default scope islocal. - Check it with
claude mcp list,claude mcp get cluvia, or/mcpin a session. - In a shared
.mcp.json, keep the token out of the file with an environment variable:
{
"mcpServers": {
"cluvia": {
"type": "http",
"url": "https://mcp.usecluvia.com/mcp",
"headers": { "Authorization": "Bearer ${CLUVIA_TOKEN}" }
}
}
}
Reference: Claude Code MCP documentation.
Cursor
Add the server to .cursor/mcp.json in a project, or ~/.cursor/mcp.json for every project:
{
"mcpServers": {
"cluvia": {
"url": "https://mcp.usecluvia.com/mcp",
"headers": { "Authorization": "Bearer ${env:CLUVIA_TOKEN}" }
}
}
}
The snippet the Cluvia app shows after you create a token also includes "type": "http"; Cursor's documentation shows remote servers without it, and the form above follows Cursor's documentation. Reference: Cursor MCP documentation.
VS Code
VS Code's own format uses a top-level servers key (not mcpServers) and requires type. Put this in .vscode/mcp.json in a workspace, or in your user configuration (command MCP: Open User Configuration). The inputs entry makes VS Code prompt for the token instead of keeping it in the file. The tools are then available in VS Code's Chat:
{
"inputs": [
{ "type": "promptString", "id": "cluvia-token", "description": "Cluvia access token", "password": true }
],
"servers": {
"cluvia": {
"type": "http",
"url": "https://mcp.usecluvia.com/mcp",
"headers": { "Authorization": "Bearer ${input:cluvia-token}" }
}
}
}
References: Add and manage MCP servers in VS Code, MCP configuration reference.
Windsurf
Windsurf's MCP documentation now lives on docs.devin.ai, where the product is called Devin Desktop. Remote servers are written with serverUrl. The file is ~/.config/devin/mcp_config.json (%APPDATA%\devin\mcp_config.json on Windows); if your version reads a different file, open it from the editor's MCP settings.
{
"mcpServers": {
"cluvia": {
"serverUrl": "https://mcp.usecluvia.com/mcp",
"headers": { "Authorization": "Bearer ${env:CLUVIA_TOKEN}" }
}
}
}
Claude Desktop and Claude.ai
Claude Desktop and Claude.ai add remote servers as custom connectors (Customize → Connectors → Add custom connector on Free, Pro and Max, or Organization settings → Connectors for Team and Enterprise owners).
- If your organization has custom request headers for connectors (Anthropic offers this to a limited set of organizations): choose authentication No sign-in, then under Request headers add
authorizationwith the full valueBearer cluvia_pat_YOUR_TOKEN. - Otherwise, custom connectors sign in with OAuth or not at all. Cluvia does not offer OAuth yet, so a custom connector cannot connect to Cluvia today. Use Claude Code, or the local bridge below.
Local bridge for Claude Desktop. Claude Desktop's claude_desktop_config.json (macOS ~/Library/Application Support/Claude/, Windows %APPDATA%\Claude\) runs local servers. The community tool mcp-remote (not made by Cluvia or Anthropic; needs Node.js) bridges a local entry to a remote server:
{
"mcpServers": {
"cluvia": {
"command": "npx",
"args": ["mcp-remote", "https://mcp.usecluvia.com/mcp", "--header", "Authorization:${AUTH_HEADER}", "--transport", "http-only"],
"env": { "AUTH_HEADER": "Bearer cluvia_pat_YOUR_TOKEN" }
}
}
}
The header value goes in env because some clients (Claude Desktop on Windows among them) do not pass spaces inside args correctly. --transport http-only matches Cluvia, which has no SSE stream. References: Claude custom connectors, MCP: connect to remote servers.
Any other MCP client
Use these settings:
| Transport | Streamable HTTP (sometimes labelled “HTTP” or “streamable-http”). Not SSE. |
|---|---|
| URL | https://mcp.usecluvia.com/mcp |
| Header | Authorization: Bearer cluvia_pat_YOUR_TOKEN |
| Auth type | Static header / bearer token. Not OAuth. |
Many clients read the common mcpServers JSON form:
{
"mcpServers": {
"cluvia": {
"type": "http",
"url": "https://mcp.usecluvia.com/mcp",
"headers": { "Authorization": "Bearer cluvia_pat_YOUR_TOKEN" }
}
}
}
A client that only speaks the older SSE transport, or only stdio, needs a bridge such as mcp-remote (above).
6. Concepts #
Companies and companyKey
Your account holds one or more companies. Every tool that works on a company takes a companyKey, a string built from the company's id and creation time, for example company-7f3a9e2b:2026-09-01T09:30:00.000Z. Get it from list_companies and pass it back unchanged; treat it as opaque. Companies are read from your cloud workspace, the same data the app shows.
Agents
Each company has a team of agents, each with an id, name, role, department and status (working while one of its tasks runs, online, or idle). Tools that take an agent accept:
- the agent's exact
id; otherwise - a role or name, matched case-insensitively and exactly (
"content writer"matches “Content Writer”). If several agents match, active agents are preferred, and exactly one must remain. Otherwise the tool answersSeveral agents match "…": … Pass an agent id.
Inactive or retired agents can still be matched, but a run given to one fails when it starts (error code agent_missing). Use ids from get_company to be exact.
Tasks and runs
assign_task creates a run: one piece of work by one agent. Runs started over MCP have origin: "mcp" and taskId: null; the app adds the task to its board when the run finishes. A run starts within seconds when there is room. When your plan's tasks at once are in use, or Cluvia is busy, it waits its turn. A run still running 15 minutes after it starts is stopped and fails with error code stale.
| Status | Meaning | Finished? |
|---|---|---|
queued | Waiting to be picked up. | No |
running | Executing. run.progress says what it is doing. In its first seconds startedAt can still be null. | No |
waiting | Paused on an approval for a connected-tool call. It continues by itself after you decide. run.waitingFor names the approval. | No |
done | Finished; the result is in run.content (or run.siteFiles for a website). | Yes |
failed | Failed; see run.error and run.errorCode. | Yes |
cancelled | Cancelled (for example from the app). | Yes |
Run modes. assign_task picks the mode from the title and brief, checking in this order:
experiment— the title says “A/B test”, “AB test” or “split test”; or it says experiment or variant together with run, start, launch or test, and a page word (page, site, website, headline, hero, button, CTA, pricing, sign-up) appears in the title or brief. The run proposes an A/B test of one page of the company's live site.website— the title names a site (website, web app, webpage, landing page, home page, marketing site, micro site, “the site”), or pairs a build verb (build, create, make, design, develop, ship, launch, prototype, code, implement, redesign, rebuild) with an app, tool, tracker, calculator, to-do, game, dashboard, prototype or MVP. The run builds a website.deliverable— everything else: a written result such as research, a plan, an email or a post.
Error codes on a failed run (run.errorCode): cut_off, narration_only, website_cut_off, cost_cap, task_error, nothing_to_review, proposal_live, key, company_missing, draft, provider_down, stale, workspace_unavailable, agent_missing, credits_exhausted. run.error always carries a readable explanation. Common ones when a run starts:
| errorCode | Message | What to do |
|---|---|---|
draft | Start work first — this company is still in planning. | Open the company in the app and start work, then assign the task again. |
company_missing | This company is no longer in your cloud workspace. | Call list_companies for current keys. |
agent_missing | The agent for this run is no longer on the team. (or: has been retired / deactivated) | Pick an active agent from get_company. |
stale | The run took longer than 15 minutes and was stopped. | Try again, perhaps with a narrower task. |
credits_exhausted | You are out of credits, so this run was not started — … | Add credits, upgrade, or connect your own OpenRouter key. |
Outputs
An output is something an agent produced: a deliverable (text) or a website. Outputs live in the company and are listed by get_company (latest 10) and list_outputs. A deliverable that used web research ends with a ranked source list:
…the deliverable text…
Sources:
1. Example source title — https://example.com/article
2. Another source — https://example.org/page
For a website, the API returns its file names, never the raw HTML.
Approvals
Actions that reach outside Cluvia wait for your decision unless you have told the agent otherwise. A pending approval lapses after 7 days. Kinds:
| Kind | Approving it… |
|---|---|
publish_site | Publishes the website a run built. |
send_email | Sends an email reply (for example to a customer who wrote in through a published site). |
start_experiment | Starts an A/B test of a page. |
finish_experiment | Ends a test: choice: "a" keeps the current page, "b" puts version B live. |
connected_tool | Runs one call to a connected tool exactly as shown (needs its payloadHash); a paused task then continues. |
When a website run finishes, the agent's publish_site policy decides: deny publishes nothing, allow publishes without asking, and ask (the default) creates a publish_site approval.
Policies
A policy is an agent's standing rule for one kind of action, set with set_policy. It applies at once. A kind with no policy set is treated as ask, which for web and images means allowed.
| Kind | Values | Effect |
|---|---|---|
publish_site, send_email, http, domain | ask, allow, deny | ask waits for your approval; allow acts without asking; deny never acts. |
web, images | allow, deny | Switches web research or image generation on or off for the agent. ask is accepted but means the same as allow. |
Schedules
A schedule gives an agent the same task each time a cron expression comes round in a time zone. Each slot queues a run with origin: "schedule" and spends credits. A slot is skipped (with the reason recorded) when the plan has no autonomous schedules, the daily run limit or the plan's scheduled-runs-per-day limit is reached, credits run out, the company's spend limit is reached, or the company was switched off or deleted. Schedules need a paid plan. An account can keep up to 100 schedules.
Cron syntax: exactly five fields — minute (0–59), hour (0–23), day of month (1–31), month (1–12), day of week (0–7, where 0 and 7 are Sunday). Supported: *, lists (1,15), ranges (1-5) and steps (*/15, 0-30/10, 5/10). Not supported: names (MON, JAN), macros (@daily) or seconds. When both day fields are restricted, a day matches if either matches. Examples: 0 9 * * 1 (Mondays at 09:00), 30 8 * * 1-5 (weekdays at 08:30), 0 */6 * * * (every six hours).
Team changes and hiring
The team lives in the company document that the app edits, so hire_agent does not change it directly. It queues a hire, which is applied within about a minute: by the Cluvia app if it has the company open, or by Cluvia's servers when nobody does. The answer's note says which applies. Checks that can be made when queuing (role in the library, role limit, team size, company not in planning) are made then, so most refusals come back at once. If the token is revoked or loses Administer before the hire applies, the hire is withdrawn. Up to 100 changes can wait per account.
Plans, credits and limits
Calls that start work run through the same checks as the app: your plan's daily run limit, how many tasks may run at once, and your credits. The defaults per plan:
| Plan | Price | Runs a day | Tasks at once | Scheduled runs a day | Schedules | Credits | Own OpenRouter key | Companies |
|---|---|---|---|---|---|---|---|---|
| Free | $0 | 20 | 1 | 0 | No | 1,000 to start (once) | No | 1 |
| Starter | $24/month | 50 | 2 | 20 | Yes | 6,000 a month | Yes | 3 |
| Pro | $59/month | 150 | 5 | 75 | Yes | 15,000 a month | Yes | 10 |
| Agency | $179/month | 300 | 10 | 200 | Yes | 45,000 a month | Yes | 30 |
- These are the standard plan limits. Your current limits are always shown in the app under Settings → Plan.
- Credits: 1 credit = $0.001 of the model cost of a run on Cluvia's key, metered per run and rounded up. While a run is in flight, the credit check sets aside an estimate (500 credits for a written task, 1,250 for a website build); the charge is the metered cost. Paid plans can top up: 200 credits per $1, 220 from $25, 240 from $50, from $1 to $500.
- Your own OpenRouter key (paid plans): the part of a run billed to your key is never metered as credits. The daily run limit and tasks-at-once limit still apply.
- Days for the daily run limit are UTC days.
- The company daily spend limit and the autopilot rules apply to automatic starts only. A task you assign over MCP is not automatic, so they do not apply to it; scheduled runs are subject to the spend limit.
- Over MCP, a plan refusal comes back as an
isErrorresult carrying the message (see Plan and credit refusals).
Not available over MCP yet
Cancelling a run, editing a schedule (delete and re-create it instead), and reading an agent's current policies are done in the app. Tokens, billing, settings and your own OpenRouter key are managed only in the signed-in app.
Ids and timestamps
Ids the server creates have the form <prefix>_<32 lowercase hex>: runs run_…, schedules sch_…, approvals apr_…, queued changes cmd_…. Company, agent, task and output ids come from the app; treat every id as an opaque string. Timestamps are ISO 8601 strings in UTC, or null. All ids in the examples on this page are made up.
7. Tools reference #
Each tool lists the permission it needs, its parameters, what it returns (the structuredContent object; content[0].text carries the same JSON as text), its errors (returned as isError results with the exact sentence shown) and its side effects. Every tool can also return the shared errors in Tool errors, such as the rate limit. Example ids are made up.
| Tool | Permission | What it does |
|---|---|---|
list_companies | read | Your companies, each with its companyKey. |
get_company | read | Team, open tasks, recent outputs, pending approvals. |
list_outputs | read | A company's outputs, newest first. |
get_output | read | One output in full. |
assign_task | tasks | Give an agent a task (starts a run). |
run_status | read | A run's status, log and result. |
wait_for_run | read | Wait for a run to finish. |
list_approvals | read | Actions waiting for your decision. |
approve | approve | Approve and carry out an action. |
deny | approve | Deny an action. |
list_schedules | read | A company's recurring runs. |
create_schedule | tasks | Schedule a recurring run. |
delete_schedule | tasks | Delete a schedule. |
hire_agent | admin | Queue a hire from the role library. |
set_policy | admin | Set an agent's standing policy. |
list_companies read-only #
Lists the companies in this Cluvia account: name, industry, team size and open tasks. Each has a companyKey the other tools take. A company-limited token sees only its company.
Parameters
None. Send "arguments": {}.
Returns
| Field | Type | Description |
|---|---|---|
companies[].companyKey | string | The key other tools take. |
companies[].name | string | Company name. |
companies[].industry | string | null | Industry. |
companies[].agents | number | Number of agents, inactive ones included. |
companies[].tasks | number | Tasks not yet complete. |
Errors
Could not reach your cloud workspace — run it again in a moment./Could not load your company from the cloud — run it again in a moment.— retry shortly.
Side effects
None.
Example
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_companies","arguments":{}}}
{
"companies": [
{
"companyKey": "company-7f3a9e2b:2026-09-01T09:30:00.000Z",
"name": "Sunrise Bakery",
"industry": "Food and drink",
"agents": 5,
"tasks": 3
}
]
}
get_company read-only #
A company's team (each agent's id, name, role, department and status), its open tasks, its latest outputs with a short excerpt, and how many approvals are waiting for a decision.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
companyKey | string | Yes | The company, as a companyKey from list_companies. Up to 200 characters. |
Returns
| Field | Type | Description |
|---|---|---|
companyKey, name | string | The company. |
industry, mission | string | null | As set in the app. |
agents[] | array | { id, name, role, department (or null), status, active }. status is working while one of the agent's tasks is running, online, or idle. active is false for a deactivated agent. |
openTasks[] | array | Up to 50: { id, title, status (or null), assigneeId, assignee (role, or null), brief (or null) }. |
recentOutputs[] | array | Up to 10, newest first: { id, title, agentId, agent (role, or null), taskId (or null), generatedAt (or null), excerpt, website }. excerpt is the text with whitespace collapsed, at most 300 characters plus “…”. website is true for a website output. |
pendingApprovals | number | Approvals waiting for a decision. |
Errors
companyKey is required.No company has the companyKey "…" — call list_companies for the keys.- The workspace errors listed under
list_companies.
Side effects
Pending approvals of this company older than 7 days are marked expired before they are counted.
Example
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_company","arguments":{"companyKey":"company-7f3a9e2b:2026-09-01T09:30:00.000Z"}}}
{
"companyKey": "company-7f3a9e2b:2026-09-01T09:30:00.000Z",
"name": "Sunrise Bakery",
"industry": "Food and drink",
"mission": "Fresh bread for the neighbourhood, ordered online.",
"agents": [
{ "id": "agent-1a2b3c", "name": "Maya", "role": "CEO", "department": "Executive", "status": "idle", "active": true },
{ "id": "agent-4d5e6f", "name": "Theo", "role": "Content Writer", "department": "Marketing", "status": "working", "active": true }
],
"openTasks": [
{ "id": "task-77ab", "title": "Write the spring menu page copy", "status": "running", "assigneeId": "agent-4d5e6f", "assignee": "Content Writer", "brief": null }
],
"recentOutputs": [
{
"id": "output-91c2",
"title": "Competitor price check",
"agentId": "agent-1a2b3c",
"agent": "CEO",
"taskId": "task-5c3d",
"generatedAt": "2026-10-06T14:02:11.000Z",
"excerpt": "Three bakeries within two miles sell sourdough between £3.20 and £4.50…",
"website": false
}
],
"pendingApprovals": 1
}
list_outputs read-only #
A company's outputs (the deliverables and websites its agents produced), newest first, each with a short excerpt.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
companyKey | string | Yes | The company. |
limit | integer | No | How many to return: 1 to 50. Default 20. |
Returns
| Field | Type | Description |
|---|---|---|
outputs[] | array | Output summaries, as in get_company's recentOutputs. |
total | number | How many outputs the company has in all. |
Errors
companyKey is required.limit must be a whole number from 1 to 50.No company has the companyKey "…" — call list_companies for the keys.
Side effects
None.
Example
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"list_outputs","arguments":{"companyKey":"company-7f3a9e2b:2026-09-01T09:30:00.000Z","limit":2}}}
{
"outputs": [
{
"id": "output-a410",
"title": "Spring menu landing page",
"agentId": "agent-8e9f0a",
"agent": "Website Builder",
"taskId": "task-61fe",
"generatedAt": "2026-10-07T08:15:40.000Z",
"excerpt": "Spring menu landing page with opening hours and an order form.",
"website": true
},
{
"id": "output-91c2",
"title": "Competitor price check",
"agentId": "agent-1a2b3c",
"agent": "CEO",
"taskId": "task-5c3d",
"generatedAt": "2026-10-06T14:02:11.000Z",
"excerpt": "Three bakeries within two miles sell sourdough between £3.20 and £4.50…",
"website": false
}
],
"total": 14
}
get_output read-only #
One output in full: its text, and for a website, its file names.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
companyKey | string | Yes | The company. |
outputId | string | Yes | An output id from list_outputs or get_company. Up to 200 characters. |
Returns
| Field | Type | Description |
|---|---|---|
id, title, agentId | string | The output. |
agent, taskId, generatedAt | string | null | The agent's role, the task, when it was produced. |
content | string | The full text, with its Sources list if any (for a website, the text saved with it); "" when there is none. |
website | object | Website outputs only: { entry (or null), files: [file names] }. |
Errors
companyKey and outputId are required.No output "…" in this company — call list_outputs for the ids.No company has the companyKey "…" — call list_companies for the keys.
Side effects
None.
Example
{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"get_output","arguments":{"companyKey":"company-7f3a9e2b:2026-09-01T09:30:00.000Z","outputId":"output-a410"}}}
{
"id": "output-a410",
"title": "Spring menu landing page",
"agentId": "agent-8e9f0a",
"agent": "Website Builder",
"taskId": "task-61fe",
"generatedAt": "2026-10-07T08:15:40.000Z",
"content": "Spring menu landing page with opening hours and an order form.",
"website": { "entry": "index.html", "files": ["index.html", "menu.html", "styles.css"] }
}
assign_task #
Gives an agent a task. It starts on Cluvia's servers within seconds when there is room, with the company's context, and appears on the app's task board when it finishes. A title asking for a website builds one (see run modes). Returns a runId for wait_for_run and run_status.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
companyKey | string | Yes | The company. |
agent | string | Yes | The agent: its id, or its role or name when only one agent has it (see Agents). Up to 120 characters. |
title | string | Yes | The task, e.g. “Draft a launch email for the bakery offer”. Up to 500 characters; a longer title is refused. |
brief | string | No | Direction: audience, constraints, what good looks like. Up to 2,000 characters; longer text is cut to 2,000. |
Returns
| Field | Type | Description |
|---|---|---|
runId | string | The new run (run_…). |
status | string | Normally queued. |
mode | string | deliverable, website or experiment. |
agent | object | { id, name, role } of the agent that got the task. |
note | string | “The run starts within seconds when there is room. Call wait_for_run with this runId for the result.” |
Errors
companyKey, agent and title are required (title up to 500 characters).brief must be a string.No company has the companyKey "…" — call list_companies for the keys.Several agents match "…": … Pass an agent id./No agent matches "…". The team: ….(without Read, the team is not listed)You've reached today's limit of N agent runs — more tomorrow.Your plan runs a limited number of tasks at once — wait for one to finish or upgrade.You are out of credits — add credits, upgrade, or connect your own OpenRouter key to keep working.(on Free:You are out of free credits — upgrade to keep working.)This email has already had its free trial credits. Pick a plan to keep going./Free trial credits need a permanent email address. …
Side effects
- Creates a queued run every time it is called; it is not idempotent. Retrying after a timeout can create a second run, so check with
get_companyorrun_statusfirst. - Spends credits when the run executes (not when it is queued), and counts toward the daily run limit and the tasks-at-once limit.
- A new account's first start receives Free's one-time 1,000 trial credits.
- A company still in planning is not refused here; its run fails when it starts, with error code
draft. - A website run that finishes may create a
publish_siteapproval, depending on the agent's policy.
Example
{
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "assign_task",
"arguments": {
"companyKey": "company-7f3a9e2b:2026-09-01T09:30:00.000Z",
"agent": "Content Writer",
"title": "Draft a launch email for the spring menu",
"brief": "Warm, local tone. Under 150 words. Mention the Saturday tasting."
}
}
}
{
"runId": "run_3f9c2a7b1e4d4c0a9b8e7f6a5d4c3b2a",
"status": "queued",
"mode": "deliverable",
"agent": { "id": "agent-4d5e6f", "name": "Theo", "role": "Content Writer" },
"note": "The run starts within seconds when there is room. Call wait_for_run with this runId for the result."
}
run_status read-only #
A run's status, what it is doing while it runs, its latest log lines, and its result once done: the text of a deliverable, or a website's file names. Returns at once; use wait_for_run to wait.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
runId | string | Yes | A runId from assign_task. Up to 100 characters. |
Returns
{ run, events }. The run object:
| Field | Type | Description |
|---|---|---|
id, companyKey, agentId, agentRole, title | string | What the run is. |
taskId, brief | string | null | taskId is null for runs started over MCP. |
mode | string | deliverable, website, experiment (runs started in the app can have other modes). |
status | string | queued, running, waiting, done, failed or cancelled (see statuses). |
origin | string | What queued it: app, schedule, mcp or site. |
scheduleId, scheduledFor | string | null | Set when a schedule queued it. |
createdAt, updatedAt, startedAt, finishedAt | string | null | ISO timestamps. startedAt is null until the run actually starts (it can be null while status is running). |
content | string | null | A deliverable's text, with its Sources list if any. |
siteFiles | string[] | Website runs only: the file names built. The HTML itself is not returned. |
error, errorCode | string | null | Why a failed run failed (see error codes). |
costUsd | number | null | What the run cost in US dollars. |
model | string | null | The model used. |
approvalId | string | null | The approval this run produced, such as a publish_site. |
progress | object | null | Only while running: { chars, outputTokens?, currentFile?, filesDone?, phase, note? }, where phase is queued, thinking, writing, searching, reading, drawing or finishing. |
waitingFor | object | null | Only while waiting: { approvalId, summary, since }. |
segments | number | How many times the run continued after an approval. |
stop | string | null | On a failed paused run: unknown (an approved action may already have happened; running again could repeat it) or lapsed (no decision came; nothing was sent). |
acted | boolean | null | Whether an approved action happened in this run; null when not known. |
startId | string | null | The app's start id; null for MCP runs. |
events is the last 20 log lines, oldest first: [{ at, kind, text }]. Kinds include queued, deferred, requeued, started, model, tool, sources, image, warning, approval, published, denied, email, paused, resumed, finished, failed and cancelled; more may be added, so handle unknown kinds. Each text is at most 500 characters, and a run's log keeps at most 200 lines.
Errors
runId is required.No run "…" in this account.(also for a run in a company the token cannot reach)
Side effects
None.
Example
{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"run_status","arguments":{"runId":"run_3f9c2a7b1e4d4c0a9b8e7f6a5d4c3b2a"}}}
{
"run": {
"id": "run_3f9c2a7b1e4d4c0a9b8e7f6a5d4c3b2a",
"companyKey": "company-7f3a9e2b:2026-09-01T09:30:00.000Z",
"taskId": null,
"agentId": "agent-4d5e6f",
"agentRole": "Content Writer",
"title": "Draft a launch email for the spring menu",
"brief": "Warm, local tone. Under 150 words. Mention the Saturday tasting.",
"mode": "deliverable",
"status": "running",
"scheduleId": null,
"scheduledFor": null,
"origin": "mcp",
"startedAt": "2026-10-07T10:01:03.000Z",
"finishedAt": null,
"createdAt": "2026-10-07T10:00:12.000Z",
"updatedAt": "2026-10-07T10:01:20.000Z",
"content": null,
"error": null,
"costUsd": null,
"model": null,
"approvalId": null,
"progress": { "chars": 412, "phase": "writing" },
"waitingFor": null,
"segments": 0,
"stop": null,
"acted": null,
"errorCode": null,
"startId": null
},
"events": [
{ "at": "2026-10-07T10:00:12.000Z", "kind": "queued", "text": "Queued from an outside tool (MCP)." },
{ "at": "2026-10-07T10:01:03.000Z", "kind": "started", "text": "Started (deliverable)." }
]
}
wait_for_run read-only #
Waits for a run to finish, up to timeoutSec seconds, then returns what run_status returns. finished is false when it was still going. The server checks the run once a second and answers as soon as it is done, failed or cancelled — or waiting, since a run paused on your approval will not move until you decide.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
runId | string | Yes | A runId from assign_task. |
timeoutSec | integer | No | How long to wait, in seconds: 1 to 120. Default 50. |
The default of 50 seconds stays under the 60-second request timeout many MCP clients use. If your client allows longer requests, pass up to 120; otherwise call again while finished is false.
Returns
| Field | Type | Description |
|---|---|---|
finished | boolean | True when the run is done, failed or cancelled. False when the wait timed out or the run is waiting on an approval. |
waitedSec | number | How long the server waited. |
run, events | object, array | As in run_status. |
Errors
runId is required.timeoutSec must be a whole number from 1 to 120.No run "…" in this account.
Side effects
None. One wait counts as one call toward the rate limit, however long it waits.
Example
{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"wait_for_run","arguments":{"runId":"run_3f9c2a7b1e4d4c0a9b8e7f6a5d4c3b2a","timeoutSec":50}}}
{
"finished": true,
"waitedSec": 21,
"run": {
"id": "run_3f9c2a7b1e4d4c0a9b8e7f6a5d4c3b2a",
"status": "done",
"mode": "deliverable",
"origin": "mcp",
"finishedAt": "2026-10-07T10:01:44.000Z",
"content": "Subject: Spring has landed at Sunrise Bakery\n\nHi neighbour,\n\nOur spring menu is here…",
"error": null,
"errorCode": null,
"costUsd": 0.0042,
"...": "the other run fields, as in run_status"
},
"events": [
{ "at": "2026-10-07T10:00:12.000Z", "kind": "queued", "text": "Queued from an outside tool (MCP)." },
{ "at": "2026-10-07T10:01:03.000Z", "kind": "started", "text": "Started (deliverable)." },
{ "at": "2026-10-07T10:01:44.000Z", "kind": "finished", "text": "Finished." }
]
}
(The run object is shortened here; the real answer carries every field listed under run_status.)
list_approvals read-only #
The actions agents want to take that are waiting for your decision, newest first. All pending kinds are returned — publish_site, send_email, start_experiment, finish_experiment and connected_tool — even though the tool's short description mentions only publishing a website.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
companyKey | string | Yes | The company. |
Returns
{ approvals: [ … ], now }, at most 100 approvals. Each approval:
| Field | Type | Description |
|---|---|---|
id | string | The approval (apr_…). |
companyKey, runId, agentId, agentRole | string | null | Where it came from. |
kind | string | See Approvals. |
summary | string | What the agent wants to do, in words. |
payload | object | The details. publish_site: { runId, projectSlug, company }. send_email: { runId, siteId, messageId, to, subject, body }. connected_tool: { connectionId, connectionName, toolName, args, schemaDigest, connectionVersion, agentId, companyKey, runId, reason, payloadHash, continues? }. |
status | string | pending here. |
createdAt, expiresAt | string | expiresAt is 7 days after createdAt. |
decidedAt, decidedNote, decidedBy, result | null while pending | Filled in once decided. result can carry the error of a failed earlier approval attempt. |
updatedAt | string | Last change. |
Errors
companyKey is required.No company has the companyKey "…" — call list_companies for the keys.(company-limited token, other company). With an account-wide token, a key that matches no company returns an empty list.
Side effects
Pending approvals older than 7 days are marked expired first.
Example
{"jsonrpc":"2.0","id":8,"method":"tools/call","params":{"name":"list_approvals","arguments":{"companyKey":"company-7f3a9e2b:2026-09-01T09:30:00.000Z"}}}
{
"approvals": [
{
"id": "apr_8c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f",
"companyKey": "company-7f3a9e2b:2026-09-01T09:30:00.000Z",
"runId": "run_0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e",
"agentId": "agent-8e9f0a",
"agentRole": "Website Builder",
"kind": "publish_site",
"summary": "Publish the Sunrise Bakery site",
"payload": { "runId": "run_0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e", "projectSlug": "sunrise-bakery", "company": "Sunrise Bakery" },
"status": "pending",
"createdAt": "2026-10-07T08:16:02.000Z",
"expiresAt": "2026-10-14T08:16:02.000Z",
"decidedAt": null,
"decidedNote": null,
"decidedBy": null,
"result": null,
"updatedAt": "2026-10-07T08:16:02.000Z"
}
],
"now": "2026-10-07T10:05:00.000Z"
}
approve #
Approves a pending approval and carries out its action: publishing the site, sending the email, starting or finishing an A/B test, or running a connected-tool call once. If the action fails, the approval stays pending with the error.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
approvalId | string | Yes | An approval id from list_approvals. Up to 100 characters. |
note | string | No | A note recorded with the decision. Up to 1,000 characters; longer text is cut. |
choice | "a" | "b" | For finish_experiment | The version to keep: "a" keeps the current page, "b" puts version B live. |
payloadHash | string | For connected_tool | The approval's payload.payloadHash from list_approvals (64 lowercase hex characters), so only the exact call you saw runs. |
Returns
{ ok: true, approval }, where approval has the fields shown in list_approvals with status: "approved", decidedBy naming the token, and result:
| Kind | result |
|---|---|
publish_site | { url, siteId } — the live address. |
send_email | { sent, reason? }. If email is not set up, the approval stays approved with sent: false and the reason. |
start_experiment, finish_experiment | { experimentId, status, minPerArm?, endsBy?, winner?, page?, url? } |
connected_tool | { state: "succeeded" | "failed" | "unknown", text?, isError?, error? }. A task paused on it then continues. |
Errors
approvalId is required, and note must be a string.Approval not found.This approval expired./This approval was already approved./This approval was already denied.choice must be "a" or "b"./Choose the version to keep: approve with {"choice": "a"} or {"choice": "b"}.payloadHash must be the 64-character hash shown with the approval./Approving a connected tool call needs the payloadHash listed with it, so exactly the call you saw is the one that runs./This action is not the one you were shown. Reload approvals and look at it again.- Publishing:
The run behind this approval no longer exists.,The run behind this approval has no finished site to publish.,This site belongs to another account.,Another of your companies already has a live site at this address — rename this company (or that site) and publish again.,The run's output isn't a website that can be published.,Publishing the site failed — please try again. - Experiments:
The test behind this approval no longer exists.,This test is no longer waiting to start…
Side effects
- Carries out a real action outside Cluvia: a site goes live, an email is sent, a test starts or ends, a connected tool is called.
- If the action fails, the approval returns to pending with
result: { error }, so you can try again; it is withdrawn instead when the failure is final, and left as it is when the call may already have been sent. - Approving a
start_experimentuses the experiment's stored minimum sample size; it cannot be changed over MCP.
Example
{"jsonrpc":"2.0","id":9,"method":"tools/call","params":{"name":"approve","arguments":{"approvalId":"apr_8c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f","note":"Looks good."}}}
{
"ok": true,
"approval": {
"id": "apr_8c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f",
"kind": "publish_site",
"status": "approved",
"decidedAt": "2026-10-07T10:06:30.000Z",
"decidedNote": "Looks good.",
"decidedBy": "token:pat_2d4f6a8c0e1b3d5f7a9c1e3b5d7f9a1c",
"result": { "url": "https://(the site's live address)/", "siteId": "(site id)" },
"...": "the other approval fields, as in list_approvals"
}
}
deny #
Denies a pending approval: its action never happens.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
approvalId | string | Yes | An approval id from list_approvals. |
note | string | No | Why, recorded with the decision. Up to 1,000 characters; longer text is cut. |
Returns
{ ok: true, approval } with status: "denied".
Errors
approvalId is required, and note must be a string.Approval not found.This approval expired./This approval was already approved./This approval was already denied.
Side effects
- The run's log records
Denied: <your note, or the approval's summary>. - For an experiment approval, the experiment is denied.
- A task paused on a
connected_toolapproval continues without that call.
Example
{"jsonrpc":"2.0","id":10,"method":"tools/call","params":{"name":"deny","arguments":{"approvalId":"apr_8c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f","note":"Wait for the new photos."}}}
{
"ok": true,
"approval": {
"id": "apr_8c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f",
"kind": "publish_site",
"status": "denied",
"decidedNote": "Wait for the new photos.",
"...": "the other approval fields, as in list_approvals"
}
}
list_schedules read-only #
A company's recurring runs: agent, task, cron expression, time zone, and when each runs next. Ordered by creation.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
companyKey | string | Yes | The company. |
Returns
{ schedules: [ … ] }. Each schedule:
| Field | Type | Description |
|---|---|---|
id | string | The schedule (sch_…). |
companyKey, agentId, agentRole, title | string | Who does what. |
brief | string | null | Direction for each run. |
cron, timezone | string | When it runs. |
nextRunAt, lastRunAt | string | null | Next and last slot. nextRunAt is null when disabled. |
enabled | boolean | Whether slots fire. |
createdAt, updatedAt | string | Timestamps. |
Errors
companyKey is required.No company has the companyKey "…" — call list_companies for the keys.(company-limited token, other company). With an account-wide token, a key that matches no company returns an empty list.
Side effects
None.
Example
{"jsonrpc":"2.0","id":11,"method":"tools/call","params":{"name":"list_schedules","arguments":{"companyKey":"company-7f3a9e2b:2026-09-01T09:30:00.000Z"}}}
{
"schedules": [
{
"id": "sch_0a1b2c3d4e5f60718293a4b5c6d7e8f9",
"companyKey": "company-7f3a9e2b:2026-09-01T09:30:00.000Z",
"agentId": "agent-4d5e6f",
"agentRole": "Content Writer",
"title": "Write this week's newsletter",
"brief": "Lead with the weekend specials.",
"cron": "0 9 * * 1",
"timezone": "Europe/London",
"nextRunAt": "2026-10-12T08:00:00.000Z",
"lastRunAt": "2026-10-05T08:00:00.000Z",
"enabled": true,
"createdAt": "2026-09-20T17:44:10.000Z",
"updatedAt": "2026-10-05T08:00:02.000Z"
}
]
}
create_schedule #
Schedules a recurring run: the agent gets this task each time the cron expression comes round in the time zone. Needs a paid plan.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
companyKey | string | Yes | The company. |
agent | string | Yes | The agent: id, or a role or name only one agent has. |
title | string | Yes | The task each run does. Up to 500 characters; longer is refused. |
brief | string | No | Direction for each run. Up to 2,000 characters; longer text is cut. |
cron | string | Yes | A 5-field cron expression (minute hour day-of-month month day-of-week), e.g. "0 9 * * 1" for Mondays at 09:00. Up to 100 characters. See cron syntax. |
timezone | string | No | An IANA time zone, e.g. "Europe/Paris". Default "UTC". Up to 64 characters. |
enabled | boolean | No | Whether slots fire. Default true. |
Returns
{ schedule }, with the fields shown in list_schedules. nextRunAt is the next slot after now (null if disabled).
Errors
companyKey, agent, title and cron are required.(companyKey or agent missing)companyKey, agentId, title and cron are required.(title or cron missing or too long — the message saysagentIdeven over MCP)brief, agentRole and timezone must be strings./enabled must be true or false.- Cron:
A schedule needs five fields: minute hour day-of-month month day-of-week.,Invalid <field> "<part>".,The <field> "<part>" is outside <min>-<max>.,The <field> step in "<part>" must be at least 1.,That schedule never comes round — check the day and month fields. Unknown time zone "…".An account can keep at most 100 schedules.Autonomous schedules need a paid plan — upgrade to put work on a schedule.(Free)- Company and agent errors, as in
assign_task.
Side effects
- Saves the schedule at once. Every slot that fires queues a run (
origin: "schedule") that spends credits and counts toward the daily limits, including the plan's scheduled-runs-per-day limit. - Not idempotent: calling twice creates two schedules.
Example
{
"jsonrpc": "2.0",
"id": 12,
"method": "tools/call",
"params": {
"name": "create_schedule",
"arguments": {
"companyKey": "company-7f3a9e2b:2026-09-01T09:30:00.000Z",
"agent": "agent-4d5e6f",
"title": "Write this week's newsletter",
"brief": "Lead with the weekend specials.",
"cron": "0 9 * * 1",
"timezone": "Europe/London"
}
}
}
{
"schedule": {
"id": "sch_0a1b2c3d4e5f60718293a4b5c6d7e8f9",
"companyKey": "company-7f3a9e2b:2026-09-01T09:30:00.000Z",
"agentId": "agent-4d5e6f",
"agentRole": "Content Writer",
"title": "Write this week's newsletter",
"brief": "Lead with the weekend specials.",
"cron": "0 9 * * 1",
"timezone": "Europe/London",
"nextRunAt": "2026-10-12T08:00:00.000Z",
"lastRunAt": null,
"enabled": true,
"createdAt": "2026-10-07T10:10:00.000Z",
"updatedAt": "2026-10-07T10:10:00.000Z"
}
}
delete_schedule #
Deletes a schedule. Runs it already queued are not affected.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
scheduleId | string | Yes | A schedule id from list_schedules. Up to 100 characters. |
Returns
{ ok: true }
Errors
scheduleId is required.Schedule not found.
Side effects
The schedule is removed permanently; no further slots fire.
Example
{"jsonrpc":"2.0","id":13,"method":"tools/call","params":{"name":"delete_schedule","arguments":{"scheduleId":"sch_0a1b2c3d4e5f60718293a4b5c6d7e8f9"}}}
{ "ok": true }
hire_agent #
Asks Cluvia to add an agent to the team. The team lives in the company, so the hire is queued and applies within about a minute: the Cluvia app applies it when it has the company open, and Cluvia applies it itself when nobody does (the answer's note says which). Only roles from the role library are hired, and a team past 8 active agents needs a justification.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
companyKey | string | Yes | The company. |
role | string | Yes | A role from the role library (below), e.g. "Growth Marketer". Case-insensitive. Up to 120 characters; longer is refused. |
name | string | No | A name; the app picks one otherwise. Up to 80 characters (longer is cut). |
department | string | No | A department; the app infers it from the role otherwise. Up to 80 characters (cut). |
instructions | string | No | Standing instructions for the agent. Up to 2,000 characters (cut). |
specialization | string | No | A focus, shown as “Role · Specialization”, e.g. "Website". Up to 60 characters (cut). |
justification | string | No | Why this role, and why now. Required once the team would pass 8 active agents. Up to 300 characters (cut). |
Role library and how many of each role a company can have:
| Department | Roles (maximum per company) |
|---|---|
| Executive | Strategy Lead (1) |
| Operations | Ops Manager (2) |
| Product | Product Manager (3), Product Builder (2) |
| Engineering | Engineering Lead (1), Solutions Architect (2), Automation Developer (4), Website Builder (2) |
| Research | Research Analyst (4), Data Analyst (3) |
| Marketing | Content Lead (1), Marketing Manager (2), Content Writer (4), Editor (2), Copywriter (3), Growth Marketer (3), SEO Specialist (3), Social Media Manager (2), Local SEO Specialist (2) |
| Sales | Sales Lead (1), Account Manager (4) |
| Support | Customer Success Manager (2), Review/Reputation Manager (1) |
CEO is also a library role, at most one per company; the “not in the role library” message leaves it out. A few common aliases also resolve, such as “writer” (Content Writer), “designer” (Website Builder) and “market researcher” (Research Analyst). Older roles that are no longer hired (for example “Software Engineer”) are refused with the library role to hire instead.
Returns
| Field | Type | Description |
|---|---|---|
commandId | string | The queued change (cmd_…). |
status | string | pending. |
note | string | applies within about a minute, or applies when the Cluvia app next has this company open. |
Errors
companyKey and role are required (role up to 120 characters).name, department, instructions, specialization and justification must be strings."…" is not in the role library. Hire one of: ….<Role> is a legacy role: existing teams keep it, but new hires come from the role library. Hire <a Replacement> instead.This company is still being planned: its team comes from the plan. Hire once work has started in the Cluvia app.Role limit reached: this company can only have <n> <Role>.A <Role> hire is already waiting for the Cluvia app (cmd_…); it applies when the app next syncs.This would make N active agents. A team above 8 needs a justification: why this role, and why now.(hires already waiting count toward N)100 changes are already waiting for the Cluvia app — open it so it can apply them first.No company has the companyKey "…" — call list_companies for the keys.
Side effects
- Queues a change; the team changes when it is applied (usually within about a minute). The app checks the role and team size again when it applies the hire.
- If the token is revoked or loses the
adminpermission first, the hire is withdrawn and never applied. - Not idempotent, but a second hire of the same role while one is waiting is refused.
Example
{
"jsonrpc": "2.0",
"id": 14,
"method": "tools/call",
"params": {
"name": "hire_agent",
"arguments": {
"companyKey": "company-7f3a9e2b:2026-09-01T09:30:00.000Z",
"role": "Growth Marketer",
"specialization": "Local ads",
"instructions": "Focus on customers within three miles of the shop."
}
}
}
{
"commandId": "cmd_5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b",
"status": "pending",
"note": "applies within about a minute"
}
set_policy #
Sets an agent's standing policy for a kind of action. publish_site, send_email, http and domain: ask (wait for approval), allow or deny. web and images: allow or deny. It applies at once.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
companyKey | string | Yes | The company. |
agent | string | Yes | The agent: id, or a role or name only one agent has. |
kind | string (enum) | Yes | One of publish_site, send_email, http, domain, web, images. |
policy | string (enum) | Yes | One of ask, allow, deny. For web and images, ask means the same as allow; only deny switches the capability off. |
Returns
{ status: "applied", agentId, agentRole, kind, policy }
Errors
companyKey, agent, kind and policy are required.kind must be one of: publish_site, send_email, http, domain, web, images.policy must be one of: ask, allow, deny.An account can keep at most 2000 agent policies.- Company and agent errors, as in
assign_task.
Side effects
Writes the policy immediately; nothing is queued. Setting the same value again changes nothing (idempotent). allow on publish_site or send_email lets the agent act outside Cluvia without asking you.
Example
{"jsonrpc":"2.0","id":15,"method":"tools/call","params":{"name":"set_policy","arguments":{"companyKey":"company-7f3a9e2b:2026-09-01T09:30:00.000Z","agent":"Website Builder","kind":"publish_site","policy":"ask"}}}
{
"status": "applied",
"agentId": "agent-8e9f0a",
"agentRole": "Website Builder",
"kind": "publish_site",
"policy": "ask"
}
8. Errors #
There are three layers. Check them in this order: the HTTP status, then a JSON-RPC error, then result.isError.
HTTP and JSON-RPC errors
These come back as a JSON-RPC error object: {"jsonrpc":"2.0","id":…,"error":{"code":…,"message":"…"}} (no id when the request could not be read that far).
| HTTP | Code | Message | What to do |
|---|---|---|---|
| 202 | — | (no body) | Normal answer to a notification or a client response. |
| 400 | -32700 | Parse error: send one JSON-RPC message as JSON. | Send valid JSON. |
| 400 | -32600 | Batches are not supported: send one JSON-RPC message per request. | Send messages one per request. |
| 400 | -32600 | Invalid request: expected a JSON-RPC 2.0 message. / Invalid request: method is required. / Invalid request: id must be a string or a number. | Fix the envelope: "jsonrpc": "2.0", a method, and a string or number id. |
| 400 | -32602 | params must be an object. | Send params as an object. |
| 400 | -32022 | Unsupported protocol version | Use a version from error.data.supported. |
| 401 | -32600 | See Authentication. | Fix or replace the token. |
| 403 | -32600 | Requests from this origin are not allowed. | Do not call from a web page on another site; call from a client or server without an Origin header. |
| 404 or 200 | -32601 | Method not found: <method> | Use a supported method. (404 for 2026-07-28 requests, 200 otherwise.) |
| 405 | -32600 | This MCP endpoint takes POST requests only. | Use POST. |
| 413 | -32600 | Request body is too large. | Keep the body under 64 KB. |
| 200 | -32602 | Unknown tool: <name> / tools/call needs the tool name. / arguments must be an object. | Call tools/list for the names; send arguments as an object. |
| 500 | -32603 | Something went wrong on the server — please try again. | Retry; contact support if it persists. |
| 501 / 503 | -32603 / -32600 | A setup or availability message. | Retry later; contact support if it persists. |
Tool errors (isError)
When a tool refuses or fails, the answer is HTTP 200 with a normal result: isError: true and the reason in content[0].text. The tool's own errors are listed in its section. Errors any tool can return:
| Message | What to do |
|---|---|
| This access token doesn't have the "<permission>" permission this needs. Create a token that has it on the Connections page in Cluvia. | Create a token with that permission. |
| Rate limit reached — try again in a minute. | Wait until the next minute (see Rate limits). |
| The rate limiter is unavailable — try again shortly. | Retry shortly. |
| No company has the companyKey "…" — call list_companies for the keys. | Use a key from list_companies. A company-limited token gets this for every other company. |
| Could not reach your cloud workspace — run it again in a moment. | Retry shortly. |
| Something went wrong on the server — please try again. | Retry; contact support if it persists. |
Plan and credit refusals
Tools that start work (assign_task, create_schedule) are refused with an isError result when a plan limit applies. The server never answers HTTP 429 or 402 on /mcp; only the message comes through.
| Message | Cause | What to do |
|---|---|---|
| You've reached today's limit of N agent runs — more tomorrow. | Daily run limit (UTC day). | Wait until 00:00 UTC, or upgrade. |
| Your plan runs a limited number of tasks at once — wait for one to finish or upgrade. | Tasks-at-once limit. | Wait for a run to finish (wait_for_run), or upgrade. |
| You are out of credits — add credits, upgrade, or connect your own OpenRouter key to keep working. | No credits left (paid plan). | Top up, upgrade, or add your own OpenRouter key in the app. |
| You are out of free credits — upgrade to keep working. | No credits left (Free). | Pick a paid plan. |
| This email has already had its free trial credits. Pick a plan to keep going. | Trial already used by this person. | Pick a paid plan. |
| Free trial credits need a permanent email address. … | Disposable email address. | Pick a plan, or sign up with your usual email. |
| Autonomous schedules need a paid plan — upgrade to put work on a schedule. | create_schedule on Free. | Upgrade. |
9. Rate limits and usage #
| Limit | Value |
|---|---|
| Tool calls | 20 tools/call requests per minute per account, across all of the account's tokens, counted in fixed one-minute windows (UTC). initialize, tools/list, ping and server/discover are not counted. A call refused for a missing permission is not counted. This allowance is separate from the app's own. |
| Runs per day | Your plan's daily run limit (Free 20, Starter 50, Pro 150, Agency 300), per UTC day, shared with runs started in the app. |
| Tasks at once | Your plan's limit (Free 1, Starter 2, Pro 5, Agency 10). A run waiting on an approval does not hold a slot. |
| Scheduled runs per day | Free 0, Starter 20, Pro 75, Agency 200. |
| Credits | Spent per run, metered at 1 credit = $0.001 of model cost (not for the part billed to your own OpenRouter key). |
| Schedules | 100 per account. |
| Access tokens | 10 live tokens per account. |
| Queued team changes | 100 waiting per account. |
| Agent policies | 2,000 per account. |
| Request body | 64 KB. |
wait_for_run | Up to 120 seconds per call (default 50). |
| Run length | A run is stopped after 15 minutes. |
| Approvals | Lapse after 7 days. |
Being rate limited is an isError result (“Rate limit reached — try again in a minute.”), never HTTP 429. To stay well inside the limit, prefer one wait_for_run with a long timeout over many run_status calls.
10. Security and data #
- A token acts as you. Anything it is permitted to do happens as if you did it in the app, and spends your credits. Give each tool its own token with only the permissions it needs, and limit it to one company where you can.
- Your account only. A token reaches only the account that created it, and a company-limited token only that company. Data from other accounts is never visible.
- Outside actions wait for approval by default. Publishing a site, sending email and similar actions create approvals unless you have set the agent's policy to
allow. Only a token with Approve and perform can approve, and approving carries the action out. Aconnected_toolapproval runs only the exact call whosepayloadHashyou pass. - Stored safely. Cluvia keeps only a SHA-256 hash of each token. The token is shown once, at creation.
- Revoke at any time on the Connections page; the token stops working on the next request. Queued hires from a revoked token are withdrawn. Deleting a company revokes the tokens limited to it; deleting your account deletes all of your tokens.
- Account checks. On each request the server confirms the token's account can still use Cluvia. That check is cached for up to 10 minutes.
- What a token cannot do: manage tokens, settings, billing, plan checkout or your own OpenRouter key. Those need a signed-in app session.
- Transport. Use HTTPS. Requests from web pages on other sites are refused. Keep tokens in environment variables or your client's secret storage, not in files you commit.
- Data handling is described in the Privacy policy and the Subprocessors list. Use of the API is covered by the Terms.
- Report a security problem to usecluvia@gmail.com. Please include what you found and how to reproduce it, and do not access other people's data.
11. Changelog #
Changes to the MCP server and to this page, newest first.
- 2026-10-08
- Runs start within seconds when there is room (run queue), instead of at the next minute.
startedAtcan be null for a run's first seconds inrunning. New event kinds:deferredandrequeued. - 2026-10-07
- Docs published; MCP server available at
https://mcp.usecluvia.com/mcp. The earlierworkers.devaddress keeps working. Server version1.0.0: 15 tools; protocol versions2026-07-28,2025-11-25,2025-06-18and2025-03-26.
12. Support #
Questions, bugs or feature requests: usecluvia@gmail.com. Include the tool name, the time of the call (UTC) and the error text, and never send your token.