Fuzzball Documentation
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

MCP Server for LLM Agents

The Fuzzball CLI can run a Model Context Protocol (MCP) server that exposes cluster operations as tools an LLM agent can call. This lets an assistant such as Claude Desktop or an agent runtime observe your workflows, author and run new ones, transfer artifacts through the object cache, and search the cluster’s own documentation – all through the same authenticated connection you already use.

Model Context Protocol is an open standard for connecting LLM applications to external tools and data. fuzzball mcp serve speaks MCP over standard input/output (stdio), so it is launched as a subprocess by an MCP client rather than run interactively.

Prerequisites

  • A reachable, running Fuzzball cluster.
  • A fuzzball CLI that is logged in to that cluster. The MCP server inherits authentication from your active context – run fuzzball login first. No credentials are ever passed on the command line.
The MCP server acts as you. Any tool the agent calls runs with your identity and permissions on the cluster. Grant only the capability tiers the agent actually needs (see Capability tiers below).

Starting the server

The fuzzball mcp command group holds the MCP tooling; serve runs the server:

$ fuzzball mcp serve

Started with no flags, the server registers read-only tools only – the safe default. It reads and writes MCP JSON-RPC messages on stdout, and sends all diagnostic logging to stderr, so you normally do not run it by hand. Instead, an MCP client is configured to launch it. Most clients accept a command plus arguments; point them at your fuzzball binary with the mcp serve arguments and whichever capability flags you want to grant:

{
  "command": "fuzzball",
  "args": ["mcp", "serve", "--allow-write", "--allow-exec"]
}

Because authentication is inherited from the active context, the client does not need any tokens or secrets in its configuration.

Connecting a client

Every client launches the same stdio subprocess – your fuzzball binary with the mcp serve arguments – so setup has the same shape everywhere: a command (fuzzball) and its arguments (mcp serve plus any capability flags). Two things apply to all of them:

  • Run fuzzball login first. The server inherits that logged-in context; the client needs no tokens.
  • fuzzball must be on the client’s PATH, or give the absolute path to the binary as the command.

Add the --allow-write / --allow-exec / --allow-destructive flags to the arguments only for the tiers you want to grant (see Capability tiers).

Claude Code

Register the server, granting whichever tiers you need:

$ claude mcp add fuzzball -- fuzzball mcp serve --allow-write --allow-exec

claude mcp list shows connection status, and /mcp inside a session reconnects after a rebuild. Add -s user (before --) to make it available in all your projects, or -s project to share it via a committed .mcp.json.

Codex

Add a server to ~/.codex/config.toml (or a project .codex/config.toml):

[mcp_servers.fuzzball]
command = "fuzzball"
args = ["mcp", "serve", "--allow-write", "--allow-exec"]

Or register it non-interactively:

$ codex mcp add fuzzball -- fuzzball mcp serve --allow-write --allow-exec

OpenCode

Add a local server under the mcp object in opencode.json:

{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "fuzzball": {
      "type": "local",
      "command": ["fuzzball", "mcp", "serve", "--allow-write", "--allow-exec"],
      "enabled": true
    }
  }
}

Pi

Pi does not support MCP natively, so an MCP extension for Pi is a prerequisite – install one before continuing. Once the extension is in place, configure the Fuzzball server through it as a stdio server with the command fuzzball and the arguments mcp serve (plus any --allow-* flags).

Claude Desktop

Edit claude_desktop_config.json (Settings -> Developer -> Edit Config) and add the server under mcpServers:

{
  "mcpServers": {
    "fuzzball": {
      "command": "fuzzball",
      "args": ["mcp", "serve", "--allow-write", "--allow-exec"]
    }
  }
}

Restart Claude Desktop after editing.

Cursor

Add the server under mcpServers in ~/.cursor/mcp.json (global) or .cursor/mcp.json (project):

{
  "mcpServers": {
    "fuzzball": {
      "command": "fuzzball",
      "args": ["mcp", "serve", "--allow-write", "--allow-exec"]
    }
  }
}

Capability tiers

Tools are grouped into tiers by how much power they hand the agent. Read-only tools are always available; the more powerful tiers are opted in with launch flags, in ascending order of risk:

FlagTierWhat it unlocks
(none)ReadObserve workflows, nodes, volumes, objects, secrets (metadata only), catalog entries, and identity; search and fetch docs; validate a Fuzzfile without running it.
--allow-writeWriteState changes that are not arbitrary compute: upload artifacts to the object cache (object_put) and download them to a local file (object_download). Reading object content inline (object_get) stays in the read tier.
--allow-execExecRun arbitrary cluster compute: start a workflow, run a one-off command, exec into a running job, or start a catalog workflow.
--allow-destructiveDestructiveCancel a workflow (workflow_stop) and delete object-cache entries (object_delete).

Flags are additive, so grant exactly the tiers you need:

# Read-only server (safe default)
$ fuzzball mcp serve

# Allow the agent to run workflows and stage/retrieve artifacts
$ fuzzball mcp serve --allow-write --allow-exec

# Full access, including cancel/delete
$ fuzzball mcp serve --allow-write --allow-exec --allow-destructive

Each tool also advertises the standard MCP annotations (readOnlyHint, destructiveHint, and so on) so a well-behaved client can warn you before invoking a mutating tool.

What the agent can do

The tool set is built around a compose -> run -> examine -> iterate loop. Rather than listing every tool, the tables below group them by tier so you can see what each launch flag grants.

Read (always available)

AreaTools
Workflowsworkflow_list, workflow_describe (pass include_spec to get the workflow’s specification as JSON; omitted by default because it is large and costs an extra call), workflow_log, workflow_events, workflow_endpoints, workflow_score, workflow_wait
Authoring pre-flightworkflow_validate (check a Fuzzfile without spending compute)
Compute & clustersnode_list, node_show, cluster_resources, cluster_list, cluster_get
Schedulerqueue_show (your queued allocations), workflow_why (typed reason a workflow is pending)
Storage & datavolume_list, volume_info, object_list, object_describe, object_get (retrieve content inline, bounded — see below), secret_list, secret_get (metadata only – secret values are never exposed)
Identity & configcontext_show, organization_get, organization_member_list, group_list, group_get
Catalogcatalog_list, catalog_get, catalog_render
Documentationdocs_search (returns {"results": [...], "total": N}; no matches is an empty array, not a sentence), docs_get (also returns a worked example Fuzzfile verbatim when given its .fz href)

Response shape

List tools return a JSON object, never a bare array:

{
  "clusters": [ { "id": "...", "name": "..." } ],
  "total": 1
}

total always matches the length of the array, so a query that matches nothing returns {"clusters": [], "total": 0} rather than an empty object. That distinction matters to an agent: “no results” and “the request was rejected” must not look the same.

Two controls keep replies inside a model’s context budget:

  • fields – an optional array naming the top-level keys to keep on each item, for example ["id", "name"]. Naming a field that does not exist returns an error listing the valid names. Either spelling works – create_time and createTime select the same key – and the reply always uses the lowerCamelCase form.

    Omitting fields returns every field on most tools. node_list is the exception: it returns a documented summary by default, because a node’s full resource report – NUMA topology, socket maps and per-device plugin inventory – runs to kilobytes per node and is rarely what you asked the question for. Name it explicitly (fields: ["id", "resource"]) to get it. Anything a default omits is still reachable this way.

    Separately, a few tools drop a field entirely before replying (catalog_list removes keyArt, which is thumbnail image data). That is a different thing from a default: asking for one of those is an error saying so, rather than a page of empty objects.

  • A size cap. If a reply would exceed the limit, the tool returns as many items as fit and adds "truncated": true, "omitted": <count>, and a truncationNote suggesting how to narrow the request. Responses are never silently cut short.

Output keys are lowerCamelCase (createTime, workflowId). Tool arguments stay snake_case (max_results, order_by).

Filtering

Most list tools accept an AIP-160 filter. Use = for an exact match and : for a substring, so name = "prod" and name:"pro" differ. A bare word with no comparator is rejected: the server’s parser accepts it but produces no condition, so the call would return the whole collection and look like a successful filter.

Filter field names are snake_case (display_name, create_time) even though the response keys are lowerCamelCase. Each tool’s filter description lists the fields it accepts, because the accepted set is per-tool and narrower than what the tool returns – volume_list, for instance, filters only on name, display_name and description.

cluster_list takes no filter or order_by: the control plane returns every registered cluster and ignores both, so match on the returned name yourself.

Reading an object that is too big

object_get returns content inline and is bounded by max_inline_bytes (64 KB by default). An object over the limit is not truncated — it comes back as a refusal carrying "tooLarge": true, the object’s size, and the maxInlineBytes that applied, so the reason is unambiguous rather than looking like empty content.

Two ways forward: raise max_inline_bytes if the object is only somewhat over, or use object_download to write it to a local file. object_download is write-tier, so it is only present when the operator started the server with --allow-write; on a read-only server, raising the limit is the only route.

Workflow status words

Every tool that reports a workflow’s status – workflow_list, workflow_describe, workflow_endpoints, workflow_wait, workflow_stop – uses the same six words, and they match what fuzzball workflow list prints:

pending · started · finished · errored · canceled · down

The underlying API enum is stage-oriented (STAGE_STATUS_*) and never reaches the agent. Two of those spellings are worth noting: a failed workflow is errored, not failed (failed is job-level vocabulary), and a running one is started. down is an autoscaled service scaled to zero — not terminal, but nothing is listening.

When filtering, workflow_list additionally accepts running as an alias for started and failed as an alias for errored, plus the shorthand active for “pending or started” — so “what is running right now?” is a single call.

One tool reports a different vocabulary, deliberately. workflow_why explains queued allocations, whose status is a separate and larger set (CREATING, RUNNING, FINISHED, CANCELLED, PREEMPTED and others) — note CANCELLED, with two Ls, against a workflow’s canceled. Those values are passed through as the scheduler reports them rather than mapped onto the six words above, because they are not the same thing and flattening them would lose the distinction. When workflow_why says a job is waiting for a dependency to reach a status, that status is an allocation status.

Every tool that starts a workflow — run, workflow_start, catalog_start — returns the new workflow’s id under workflowId, so the value can be handed to workflow_describe, workflow_log or workflow_wait without checking which tool produced it. Note that workflow_stop is asynchronous: the status it reads back is usually still started, and the workflow settles to canceled shortly after.

Write (--allow-write)

ToolPurpose
object_putUpload a local artifact to the object cache.
object_downloadWrite an object-cache artifact to a local file (for large or binary objects, or anything past object_get’s inline limit).

Exec (--allow-exec)

ToolPurpose
workflow_startSubmit a Fuzzfile (as a string or a path).
runRun a structured one-off command (image, command, resources, environment, mounts). Pass dry_run to get the generated Fuzzfile back validated instead of started — see below.
workflow_execRun a command inside an existing job’s container.
catalog_startRun a workflow catalog entry directly with parameter values.

run with dry_run

dry_run returns the Fuzzfile run would have submitted, having validated it, and starts nothing. It replies with {"workflow": "<yaml>", "warnings": [...]} — the YAML as a string so it stays paste-able, and any warnings the validator returned.

Validation is done by the server, using the same call workflow_validate makes, so a dry run rejects anything a real start would reject rather than only what parses. A value like memory: "not-a-size" is a validation failure, not a preview. That means dry_run needs a reachable cluster; it is not an offline formatter.

When cluster_id is set, the schedulability half is evaluated against that cluster, matching what starting the workflow would do — so a dry run cannot bless a workflow the target federate would refuse, or refuse one it would accept.

Destructive (--allow-destructive)

ToolPurpose
workflow_stopCancel a running workflow.
object_deleteDelete an object-cache reference and all of its versions.

Both destructive tools additionally require confirm: true. Granting the flag makes the tool available; confirming states the intent for a specific call, so an agent cannot reach an irreversible operation through a malformed or half-built argument list. workflow_stop’s separate force parameter escalates a graceful cancel to an immediate stop — it is not a confirmation and does not substitute for one.

Prompts

Beyond tools, the server publishes guided prompts for common tasks (write_workflow, run_command, inspect_workflow, research_loop) that a client surfaces as ready-made starting points. research_loop drives an iterative experiment loop: it frames a measurable goal with you, then authors a workflow and runs it repeatedly, improving toward that goal.

Workflow outputs must egress to the object cache. The agent has no tool for reading files out of a storage volume, so a workflow that writes results only to a volume produces nothing the agent can retrieve. Any workflow authored for an agent should egress its results to the object cache, where object_get can fetch them. This guidance is also baked into the relevant tool descriptions and prompts so the agent follows it automatically.

Documentation source

The documentation tools (docs_search, docs_get) serve the connected cluster’s own documentation site. The base URL is derived from your active context automatically. If your cluster publishes its docs at a different location, override it with the --docs-url flag or the FUZZBALL_DOCS_URL environment variable:

$ fuzzball mcp serve --docs-url https://docs.example.com/

The worked example Fuzzfiles are part of that site, so they need no separate tool: pass an example’s .fz href to docs_get and it comes back verbatim, ready to adapt and validate.

docs_get reads the rendered page, so the text an agent sees matches what you see. Where a page presents alternatives in tabs – Kubernetes Operator or Docker Compose, for instance – each tab is returned under its own ### Tab: <name> heading, so the agent follows one set of instructions rather than a blend of both.