Skip to content
BlenderMCP

Chat protocol

The Chat panel is an ordinary MCP client of the BlenderMCP server. Each message is one call to blender_chat over the add-on’s existing session; the server runs the model and its tool calls against the Blender that sent the message, and streams progress back on that call. This page is for anyone building another client against the same interface. Using the panel is covered in Chat inside Blender.

blender_chat(message: str,
history: list[{role: "user"|"assistant", content: str}] | None = None,
attachments: {selection?, text?, viewport?} | None = None)
  • The server targets the Blender whose add-on made the call; there is no target_uuid argument.
  • history is the recent conversation, text only, at most 20 entries. The server keeps no conversation state.
  • attachments is what the user clipped to this message: selection ([{name, type, location?, dimensions?}], at most 50 objects), text ({name, body}, a text block, cut at 20,000 characters) and viewport (true to include what the user sees). Selection and text are added after the message as context. The viewport is captured by the server during the turn: Claude receives the image itself, other backends a description from the vision model.
  • The call is long-running (up to the server’s turn limit, 5 minutes by default). Progress arrives as MCP progress notifications on the call; each message is a JSON object:
{"t": "status", "text": "Thinking…"}
{"t": "delta", "text": "Added a 2 m "}
{"t": "tool", "name": "create_mesh", "phase": "start"}
{"t": "tool", "name": "create_mesh", "phase": "end", "ok": true, "ms": 412, "head": "{\"object\": \"Table\", …"}
{"t": "tool", "name": "execute_code", "phase": "end", "ok": true, "ms": 4310, "wait_ms": 81200}
{"t": "tool", "name": "add_primitive", "phase": "end", "ok": false, "ms": 3, "error": "{\"status\": \"error\", \"error\": \"invalid_argument\", …"}
{"t": "text", "text": "Added a 2 m cube named Table…"}
  • delta events carry reply text as it streams; append them in order. A text event then carries that round’s whole text, so a client that ignores delta loses nothing.
  • ms is the tool’s own time. wait_ms appears only when the step asked for approval, and is the time spent waiting for the user.
  • A failed step carries error, the start of its error text (at most 300 characters, whitespace collapsed), so a fast argument rejection can be told from a failure inside Blender. A successful step carries head, the first 160 characters of its result. Both are optional: clients must not require them. The add-on keeps them on the step’s transcript entry and writes a failed step’s error to the chat log.
  • The result is a JSON string:
{"status": "ok", "reply": "…", "steps": [{"tool": "create_mesh", "ok": true, "ms": 412, "head": "…"}], "elapsed_s": 9.8,
"backend": {"provider": "gateway", "model": "qwen3"}, "trial": null}
  • Other statuses: disabled (chat is off on this server), no_backend (the gateway isn’t available to this account and no backend is configured; hint says what to do), busy (a turn is already running for this account), backend_error (the model call failed; detail has the reason), timeout, trial_ended (see below). backend_error and timeout also carry steps, elapsed_s and backend for whatever ran before the failure, and timeout carries the reply so far (often empty).
  • Problems with the call itself come back as {"status": "error", "error": …}: not_registered (this session hasn’t called blender_register_client, so there is no Blender to target), empty_message, unauthenticated.

When the server has a trial configured (CHAT_TRIAL_USD, see Server configuration), accounts without a backend of their own may spend that many US dollars of model use on the server’s default backend, priced at Anthropic’s list prices. Every result from the backend lookup on (ok, timeout, backend_error, no_backend, trial_ended) carries a trial field:

"trial": {"unit": "usd", "limit": 0.5, "used": 0.1234, "remaining": 0.3766, "percent_left": 75}

used and remaining are rounded to 4 decimals. percent_left is a whole number from 0 to 100, rounded down, and is the easiest thing to show. remaining never goes below 0, but used can pass limit: a turn that starts with budget left runs to the end, and its last calls may overshoot.

trial is null for an account with no limit: one that saved its own Claude key or OpenAI-compatible URL, one listed in CHAT_GATEWAY_USERS, or any account when the server has no trial. Results refused before the lookup (disabled, busy, and the error results above) have no trial field at all; keep showing the last value you saw.

Every model call in a trial turn is charged as soon as it returns: each round, retried requests, requests that were billed but then failed, and the vision model’s descriptions for look_at_viewport and viewport attachments. On an ok result, trial already includes the whole turn. A turn that is cancelled loses only the cost of the call still running.

When used has reached limit, the server makes no model call and returns:

{"status": "trial_ended", "trial": {"unit": "usd", "limit": 0.5, "used": 0.5012, "remaining": 0.0, "percent_left": 0},
"hint": "Your free trial is used up. Paste your Claude API key in the Chat panel to keep going."}

Saving a Claude key (or an OpenAI-compatible URL) with blender_set_chat_backend lifts the limit at once.

For a tool that needs approval, the server sends an elicitation/create request on the same session while the call is running. The message starts with BlenderMCP approval: and the requested schema has no fields, so the client answers accept (allow) or decline (deny). Clients should decline elicitations they don’t recognise. Unanswered requests are declined by the server after 120 seconds.

blender_set_chat_backend (add-on role only)

Section titled “blender_set_chat_backend (add-on role only)”
blender_set_chat_backend(provider: "gateway"|"anthropic"|"openai", model: str | None = None,
base_url: str | None = None, api_key: str | None = None, clear: bool = False)

Returns {"status": "ok", "backend": {"provider", "model", "base_url", "has_key", "source"}, "trial"}, where source is user for a backend the account saved and server for the server default (then has_key says whether the server has a key for it), and trial is the account’s free trial status after the change (null once it has its own backend). No key, the account’s or the server’s, is ever returned. Omitting api_key keeps the saved key when the provider stays the same; an empty string removes it. blender_get_chat_backend() and blender_set_chat_advisor() return the same shape. Arguments to this tool are never recorded by the server’s call logging.

A new Claude key is checked with Anthropic before it is stored, with a call that spends no tokens (GET /v1/models, 10 second limit). Changing only the model, with api_key omitted, does not check the saved key again. With provider: "anthropic" and no model, the account uses the server default’s model when the server default is Claude (CHAT_DEFAULT_MODEL), and claude-opus-5 otherwise. The escalation default (CHAT_ANTHROPIC_ADVISOR) and the account’s own choice apply to an account’s own key exactly as they do to the server’s.

Refusals are {"status": "error", "error", "detail", "trial"}, and nothing is stored. error is one of:

| error | Meaning | |---|---| | invalid_provider | provider isn’t gateway, anthropic or openai. | | invalid_argument | A missing or malformed model or base_url, or a value that is too long. | | api_key_required | provider: "anthropic" with no key given and none saved. | | secret_key_not_configured | The server can’t store keys (CHAT_SECRET_KEY is unset). | | invalid_api_key | Anthropic answered 401 or 403. detail is “Anthropic didn’t accept that key. Check it at platform.claude.com/settings/keys.” | | key_check_failed | Anthropic couldn’t be reached, didn’t answer in time, or answered something other than success or 401/403. detail says which. The key may be fine; try again. |

The account’s previously saved backend, if any, is left as it was.

When chat is enabled, the result of blender_register_client includes "features": ["chat"]. The add-on shows the Chat panel only then.

Accounts that haven’t saved a Claude key or an OpenAI-compatible URL use the server’s default backend. It is normally the GPU gateway, but the operator can point it elsewhere, for example at Claude with the server’s own key while the local models are paused (CHAT_DEFAULT_PROVIDER=anthropic plus ANTHROPIC_API_KEY; the model defaults to claude-opus-5, or CHAT_DEFAULT_MODEL). A backend you saved yourself always wins. The Current: line in the preferences says server default when you are on it; the server’s key is never sent to Blender. CHAT_GATEWAY_USERS, when set, limits the server default to the listed accounts, whichever provider it is.

CHAT_TRIAL_USD (default 0, a decimal number of US dollars such as 0.5) turns the server default into a free trial. With 0 there is no trial and nothing above changes. With an amount above zero, every account that has no backend of its own and isn’t listed in CHAT_GATEWAY_USERS may spend that much on the server default, and is then asked for its own key (the trial_ended status). This includes accounts outside a non-empty CHAT_GATEWAY_USERS, which would otherwise get no_backend: with a trial they get the trial instead. Listed accounts and accounts with their own key or URL are unlimited. Only blender_chat spends the trial; other sampling requests from a trial account get no server backend.

Spend is kept per account in the chat_trial_usage table (migration 20261001_0013): used_usd, plus running tokens_in and tokens_out totals. The server never resets it; to give an account more, lower its used_usd or raise the limit. Each call’s cost is worked out from the token counts the provider reports and the per-model table in src/blender_mcp/chat/pricing.py, which holds Anthropic’s list prices (input, cache writes, cache reads, output). Cache writes are priced at the 5 minute rate, which is what the server’s requests use, unless Anthropic’s usage breaks them down by cache lifetime. Advisor (escalation) tokens are priced at the advisor model’s rates. Any model not in the table, including the GPU gateway’s and OpenAI-compatible models, is priced at Claude Haiku 4.5 rates; that is a stand-in so a trial on a non-Claude default still drains, not that model’s real cost. A call whose provider reports no token usage costs nothing. When the server’s call logging is on, each llm.call event carries the call’s cost_usd (Claude calls always; other providers only when the call was charged to a trial).

With Claude (your key or the server’s), look_at_viewport sends the screenshot to Claude itself rather than to the gateway’s vision model. Claude calls think adaptively between tool steps, and requests Claude declines are re-run on Anthropic’s recommended fallback model where the API supports it (CHAT_ANTHROPIC_FALLBACKS, on by default); a request that is still declined ends the turn with a message naming the refusal category. CHAT_ANTHROPIC_EFFORT (low to max) sets the effort level; unset leaves the API’s default.

Escalation (the panel’s Escalate to) uses Anthropic’s advisor tool: the answering model can consult a stronger one mid-turn. CHAT_ANTHROPIC_ADVISOR sets the server’s default advisor (empty means off), CHAT_ANTHROPIC_ADVISORS limits which models accounts may choose (empty allows any stronger Claude model), CHAT_ANTHROPIC_ADVISOR_CACHE turns on prompt caching for advisor calls, and CHAT_ANTHROPIC_ADVISOR_MAX_TOKENS caps each consultation (default 2048). An account’s own choice, saved with blender_set_chat_advisor, wins over the server default.