Skip to content

Tool surfaces

Every Arcane capability is declared once, as a catalog action with a canonical ID domain.action (container.restart, swarm_service.scale). A tool surface decides how that catalog appears in the client’s tool list. The three surfaces are projections of the same catalog, never parallel registrations: whichever you pick, every action is reachable, takes the same parameters, follows the same safety rules and returns the same result.

Pick one with ARCANE_MCP_TOOL_SURFACE or --tool-surface.

SurfaceToolsCall shape
dynamic (default)2: arcane_find_action, arcane_execute_action{"action": "container.restart", "params": {...}, "confirm": false}
meta52: one per domain, arcane_<domain>{"action": "restart", "params": {...}, "confirm": false}
individual461: one per action, arcane_<domain>_<action>{...params, "confirm": false}

What each surface’s tool definitions cost in the model’s context: dynamic 1.8 KB (~400 tokens), meta 71 KB (~16 k tokens), individual 531 KB (~111 k tokens). These are the name, description and input schema of every tool, which is what a client passes to a model (titles, annotations, icons and output schemas are not), as cmd/audit_tokens measures them on the full catalog; tokens are its estimate, within a few percent of the GPT tokenizers. make check fails when these figures drift more than 15% from the measurement.

Two tools, whatever the size of the catalog.

arcane_find_action searches the catalog locally — it makes no call to Arcane — and returns the best matches with their ID, title, description, usage note, required parameters, whether they mutate or destroy, related actions and a schemaUri. The top five matches carry their full input schema; the rest a one-line summary of each parameter.

{ "query": "restart container", "limit": 10 }

query is plain words, a resource and a verb (list containers, pull image, swarm service scale), at most 256 characters; limit defaults to 10 and is capped at 50.

arcane_execute_action runs one action by canonical ID. The action’s parameters go inside params; confirm stays at the top level.

{
"action": "container.restart",
"params": { "containerId": "web-1", "environmentId": "0" },
"confirm": false
}

action also accepts another surface’s name for the action (arcane_container_restart, arcane_container.restart) or Arcane’s operationId. An unknown name is answered with the closest IDs; an action removed by read-only mode or an exclusion is answered with the reason.

One tool per domain: arcane_container, arcane_image, arcane_project, arcane_swarm_service… Each tool’s description lists its actions with their required parameters and marks them [read-only] or [destructive]; its action property is an enum of the domain’s actions.

{ "action": "restart", "params": { "containerId": "web-1" } }

sent to arcane_container. action also accepts the canonical ID (container.restart) as long as it belongs to the tool’s domain. An action of the domain removed by read-only mode or an exclusion is answered with the reason, as on dynamic. A domain excluded as a whole has no meta tool, so a call to it gets the client’s unknown-tool error, as on individual (read-only mode leaves no domain empty).

One tool per action: arcane_container_restart, arcane_image_pull, arcane_project_up… Each tool carries its action’s full input schema and the MCP annotations readOnlyHint, destructiveHint and idempotentHint. Parameters go at the top level; destructive tools add a confirm boolean to their schema. An action removed by read-only mode or an exclusion has no tool, so a call to its name gets the client’s unknown-tool error.

{ "containerId": "web-1" }
If…Use
You want the smallest tool list, or the client caps the number of toolsdynamic
The model works better with a fixed, browsable list it can read up frontmeta
You want per-tool allow-lists or approvals in the client, or the clearest annotations per callindividual
  • dynamic costs one extra round trip the first time an action is used (find, then execute), but keeps the context small: two tool definitions instead of hundreds.
  • meta puts ~52 descriptions in the context. The model sees every action name up front; for an action’s full schema it reads arcane://tools/<id>, and a call with wrong parameters is answered with the accepted and required ones before anything reaches Arcane.
  • individual registers 461 tools. Some clients limit how many tools they send to the model; check yours before choosing it. Combine it with exclusions to trim the list.

Whatever the surface, the server’s instructions tell the client that destructive actions need confirm: true and that environmentId defaults to the configured environment.

The server publishes the catalog it serves as two MCP resources, adapted to the active surface:

  • arcane://tools lists every action with its ID, title, the tool that runs it on this surface, a ready-to-adapt call line and its mutating and destructive flags:

    {
    "surface": "meta",
    "count": 461,
    "actions": [
    {
    "id": "container.kill",
    "title": "Kill container",
    "tool": "arcane_container",
    "call": "arcane_container {\"action\":\"kill\",\"params\":{...},\"confirm\":true}",
    "mutating": true,
    "destructive": true
    }
    ]
    }
  • arcane://tools/{id} returns one action in full: description, usage note, Arcane route, operationId, required parameters, the complete input schema and related actions. For an action that read-only mode or an exclusion removed, it returns the reason instead.

The resources reflect the filtered catalog, so with --read-only the count drops and mutating actions disappear from the list.

The server also publishes Docker state (environments, containers, projects, images) and workflow guides as resources, the same on every surface: see Resources.

A successful call returns the complete value as structuredContent and a Markdown rendering of it as text, the same on every surface. A list is a table of the fields that identify and describe each item, under the count and pagination (3 item(s) — 12 total, page 1 of 4); one object is a field list with nested values in a JSON block; a downloaded text is the text itself. When a table leaves something out, the text says so: it names the fields not shown and the action that returns one item in full (for example container.get), and a page of more than 50 items says how many rows were cut and how to page through the rest with start and limit. Clients that read structuredContent always have every field.

Failures come back as tool results with isError set, never as protocol errors, and carry a next step: the accepted and required parameters when arguments fail validation (checked locally, before Arcane is called), the list action that finds a valid ID after a 404, the credential to check after a 401, or the role assignment involved after a 403. When Arcane could not reach the environment’s Docker daemon or agent (it then answers 404, 500, 502 or 503 with its own words for it), the hint says so instead of calling the resource missing, and points at environment.test and environment.get where the server offers them.