MCP server

Soleil Clusters ships an MCP server exposing the API as tools an AI assistant can call directly. Point a client at https://clusters.soleilpictures.com/api/v1/mcp and approve it in the browser — it signs you in over OAuth, so there is no token to paste and no account needed beforehand. Run it locally with npx only for tools that need your filesystem. Either way it holds no credentials of its own, so an agent reaches exactly what your account reaches and no more.

The MCP server puts Soleil Clusters in reach of Claude and any other Model Context Protocol client, so an assistant can read and build boards directly.

What it is

A layer over the REST API. It holds no credentials and implements no permissions of its own — it forwards whatever credential you connected with, so everything about the authorization model applies here unchanged. An OAuth access token and a personal access token resolve to the same thing: your own session, under ordinary row-level security.

Two ways to connect

Hosted — nothing to install, nothing to paste

{
  "mcpServers": {
    "soleil-clusters": {
      "type": "http",
      "url": "https://clusters.soleilpictures.com/api/v1/mcp"
    }
  }
}

A URL. That is the whole setup.

The first call comes back 401 with a pointer to our OAuth metadata; the client registers itself, opens a browser, and you approve the connection on one screen. No token is ever copied, and if you do not have an account yet you get one on that screen — signing in is a single email box.

Afterwards the connection is listed under Settings → API → Connected apps, where you can see what it has done and disconnect it.

If your client cannot do OAuth, a personal access token still works exactly as before:

"headers": { "Authorization": "Bearer undefined…" }

For a team rather than a person, use a service account — a credential that does not stop working when someone leaves.

Local — for files on your machine

{
  "mcpServers": {
    "soleil-clusters": {
      "command": "npx",
      "args": ["-y", "soleil-clusters-mcp"],
      "env": { "SOLEIL_API_TOKEN": "undefined…" }
    }
  }
}

SOLEIL_API_BASE overrides the host. Without a token the server exits at startup rather than running in a state where every call fails.

The local server has one extra tool, upload_file, which reads a file from your disk and uploads it — including large ones like video, which cannot fit through an assistant's message. Everything else is identical: both servers are built from one registry, so a tool cannot exist on one and not the other.

Protocol versions

Both servers accept 2026-07-28 · 2025-11-25 · 2025-06-18.

2026-07-28 is the revision that removed the initialize handshake: protocol version, client identity and client capabilities now travel in _meta on every request, and there is no session to establish or keep alive. Nothing needs to be configured to use it — the server decides per request:

  • A request whose params._meta carries io.modelcontextprotocol/protocolVersion is served under that revision.
  • An initialize request is served under the older, session-based rules.

So a client built on the current official SDK — which tops out at 2025-11-25 — connects exactly as it always did, and a newer one gets the newer behaviour from the same URL. If you ask for a version the server does not implement, it answers -32022 and lists the ones it does, so a client can retry rather than guess.

server/discover returns the supported versions, the capabilities and the server identity in a single call. On the local server it doubles as the probe that tells a client which era it is talking to, because stdio has no HTTP status code to branch on.

Under 2026-07-28 the hosted transport also requires the mirrored request headers — MCP-Protocol-Version, Mcp-Method, and Mcp-Name for tools/call and prompts/get. They must agree with the request body; a mismatch is refused with -32020 rather than served, because a proxy is allowed to route on the header without reading the body, and two components acting on different values is exactly the confusion the rule exists to prevent. Older clients send no such headers and are not held to them.

Two things that changed with the revision and may surprise you: ping is gone (a stateless protocol has no connection to keep alive), and a POST body must be a single message — JSON-RPC batching is no longer accepted. Both still work for clients connecting under an older version.

Protocol errors

These are JSON-RPC error codes, distinct from the REST API's error codes — a client branches on the number.

CodeMeansHTTP
-32700The body was not JSON400
-32600Not a valid single JSON-RPC message400
-32601No such method404
-32602Bad params — also an unknown tool or prompt200
-32603The server failed200
-32020A mirrored header disagrees with the body400
-32021The request needs a capability the client did not declare400
-32022Unsupported protocol version — the answer lists the supported ones400

A tool that runs and fails is not an error here. It returns a normal result with isError: true and the API's own sentence in the content, because a model that reads "this token cannot delete" can correct itself, while one that gets a transport error only learns that something broke.

Choosing scopes for an agent

The three scopes (delete · read · write) exist mainly for this. "Can add cards to my moodboard" and "can delete my moodboard" are different levels of trust to place in a language model, and they are separate grants.

Give itFor
readResearch, summarising, answering questions about your boards
read + writeBuilding boards, adding references — the usual choice
all threeHousekeeping and cleanup agents

An agent without delete gets a 403 naming the missing scope rather than quietly failing.

Tools

Orientation and finding things

ToolInput
whoami— · the account, its scopes, rate limit, and whether it is a service account
list_workspaces
searchq (min 2 chars), kind? (board\card), workspace_id?, paging
list_boardsworkspace_id?, parent? ("root" for top level), since?, cursor?, paging
board_treeroot? or workspace_id?, depth? — a whole hierarchy in one call
get_boardboard_id — one board, with how much of the card allowance is used
resolve_identifierscope, value — find something by an id from another system
list_deleted_boardsworkspace_id?
list_groupsboard_id — the labelled sets on a board

Reading

ToolInput
read_boardboard_id, full?, include?, source?, since?, paging
view_imageimage_key — fetch an image card's actual picture
list_imagesworkspace_id?, board_id?, since?, cursor?
export_boardboard_id, format? (json\omc)
get_metadataboard_id, cards? — identifiers and properties
list_auditsince?, cursor? — recent writes and image fetches

Writing

ToolInput
create_boardname, workspace_id?, parent_board_id?, identifiers?, props?
create_boardsboards[], on_conflict? — build a structure in one call
add_cardsboard_id, cards[] — up to 1000, on_conflict?
upload_imageboard_id, data (base64), content_type
upload_fileboard_id, pathlocal server only; handles large files
import_urlsboard_id, urls[], titles?, dry_run? — bring reference in from the web; safe to re-run
arrange_boardboard_id, layout?, card_ids?, dry_run? — lay a board out
create_groupboard_id, name, color?, shape? — say a set of cards is one thing
rename_boardboard_id, name?, view?, parent_board_id?
move_boardsboard_ids[], parent_board_id
update_cardboard_id, card_id, plus any writable field
update_cardsboard_id, cards[] — many at once
move_cardsfrom_board_id, to_board_id, card_ids[]
set_metadataboard_id, card_id?, identifiers?, props?
restore_boardboard_id

Deleting — requires the delete scope

ToolInput
delete_cardboard_id, card_id
delete_cardsboard_id, card_ids[]
delete_boardboard_id

Card kind is doc, file, image, link, note, video, defaulting to note.

Every tool carries annotationsreadOnlyHint, destructiveHint, idempotentHint — which is what a client reads when deciding whether a call needs confirming. They are structured, so unlike a warning in a description they actually participate in that decision.

Prompts

Three starting points, offered by name rather than buried in a tool description:

PromptDoes
describe_boardLooks at every image and writes what the board is reaching for
organize_boardProposes a grouping into child boards, without moving anything
import_planTurns a file listing into a plan of boards and cards, with identifiers so it can be re-run

Adding an image

Two calls:

  1. upload_image with the base64 bytes and a content_type — returns an image_key
  2. add_cards with {"kind": "image", "image_key": "…"}

Images are limited to 25 MB through the API and are charged to the board owner's storage. See Images API.

What agents should know

read_board truncates by default. Pass full: true for untruncated bodies, and expect it to be large. Both read_board and list_boards paginate — check for more rather than assuming one page is everything.

Deleting returns the card. delete_card responds with the full card it removed. That object is the undo — pass it back to add_cards to restore it. Worth keeping in context before destructive work.

Deleting a board is destructive and confirmable. delete_board removes a whole board; the tool description says to confirm with the user first, and restore_board exists because that is not always heeded.

Writes are idempotent by key. Every POST the server issues carries a generated idempotency key, so a retried call replays rather than duplicating.

Batch. One add_cards call with forty cards is one request against the rate limit; forty calls are forty.

live: false is not an error. The change is saved; a canvas someone already has open will not show it until reload.

Start with whoami. It reports the scopes and remaining rate budget, which is cheaper than discovering both through failures.

Reading the docs as an agent

Frequently asked questions

Do I have to install anything?

No. Point an MCP client at https://clusters.soleilpictures.com/api/v1/mcp. It discovers the sign-in flow on its own and opens a browser; you approve once. Running it locally is only needed for uploading files from your own machine.

Do I need an account before connecting?

No. Sign-in is a single email box that creates the account if there isn't one, so you can go from Connect to a working assistant without visiting the site first.

Do I still need a personal access token?

Only for a client that cannot do OAuth, or for your own scripts. Assistants that speak current MCP handle it themselves.

Does the MCP server have its own permissions?

No. It forwards your personal access token to the same API, so it inherits your account's permissions exactly. A token without the delete scope cannot delete.

How do I stop an agent deleting things?

Mint a token without the delete scope. Deleting is a separate grant from writing precisely so an agent can be allowed to build without being allowed to destroy.

Can an agent upload a video?

Only the local server can, with upload_file, because the file has to be read from a disk. The hosted one handles images with upload_image.

Is it the same set of tools either way?

Yes, apart from upload_file, which needs a filesystem. Both servers are built from one registry so they cannot drift apart.

Machine-readable: /docs/mcp.md · /llms.txt