Skip to content

The Flui assistant and MCP

Flui’s assistant is a full surface of the product. It answers from a compiled knowledge base, it can act through a shared tool registry, and every action it can take follows the same rules whether a person drives it in the dashboard or an external agent calls it over MCP.

This chapter covers how that is built and the safety model underneath it. For an assistant that can deploy and uninstall your apps, the part that matters most is what stops it from doing the wrong thing.

Two surfaces, one tool registry

There are two ways into the same capabilities:

  • The in-product assistant: the chat inside Flui’s dashboard. You ask a question or make a request in plain language. It answers from the docs, and when you ask it to act it runs a tool-use loop on your behalf.
  • The MCP server: a Model Context Protocol endpoint that offers the same tools to external agents: your own scripts, an agent in your editor, another LLM application.

A tool is written once. One registry backs both the assistant loop and the MCP server, so there is no second surface to keep in sync and no second place where the safety checks could drift apart.

The registry holds over a hundred and sixty tools in about twenty families: applications, the catalog, observability, manifest validation, infrastructure and the operations that change it, scaling, DNS, the gateway, backups, migrations, scheduled jobs, mail, repositories, access, how this installation is run, and a family that asks a person for a credential. Nobody sees all of them at once, for reasons covered in the safety model below.

Both surfaces get their model from Flui’s inference layer instead of a hard-coded provider. See Inference and bring-your-own-LLM for how that endpoint is resolved.

A tool has no private door into the product

The rest of the safety model rests on one rule: a tool reaches Flui only by calling Flui’s own API, as the caller’s own principal. It does not hold the services underneath and does not create a token for itself. It sends a request over the same route your browser would use, carrying the credential the agent presented.

Every guard Flui has sits on the request path: who may reach this application, which sections you hold, what a guest is kept out of. A tool that called the code directly would bypass all of them, so it makes a real request instead.

As a result, a tool call meets exactly the checks a person clicking would meet. An agent cannot be given a shortcut that a person does not have.

The assistant answers from a knowledge base

The assistant does not use a general-purpose model’s training data to answer questions about Flui. Flui’s documentation is compiled into a knowledge base: a single versioned artifact (kb.json) built by a script from the concept docs, the CLI prose, a generated CLI reference, the flui.yaml schema and a hand-written guardrails file. The assistant answers only from this corpus.

The guardrails ask the assistant not to invent commands, flags, manifest fields or version numbers. If the answer is not in the corpus, it says so and points you to flui <command> --help or the docs. The knowledge base is also stamped with the CLI, platform and schema versions it was built against, so the assistant can tell you which version it is describing.

This is why these docs have to be accurate. The page you are reading is one of the sources compiled into the knowledge base, so an inaccurate page becomes a wrong answer the assistant gives with confidence.

A scope gate that also routes

Sending the whole corpus to the model on every message would be slow and expensive. Flui avoids that with one cheap model call, a guard step, that does two jobs:

  1. Scope gate. It decides whether your message is about Flui at all. A clearly off-topic question (the weather, philosophy, general trivia) gets a short, friendly reply in your own language saying the assistant only helps with Flui, and the question itself is not answered. The gate leans strongly toward answering: greetings, follow-ups and anything that plausibly concerns running apps on Flui count as on-topic.
  2. Router. For an on-topic message, the same call returns the ids of the one to four most relevant knowledge sections. Only those go into the answering prompt, which keeps it small.

The tool-use loop

When you ask the assistant to do something, it runs a loop: the model can call tools, see their results and call more tools, up to a bounded number of rounds, before it writes a final answer. In the dashboard this is streamed, so you see the text and each tool step as they happen.

The loop has safeguards so it stays reliable on medium and small models. Some of them:

  • A URL guard. Application endpoint URLs are passed through only when they came from a tool result; anything else is stripped before it reaches you. Every link you get was produced by the platform, not assembled in the answer.
  • Doc-link surfacing. The sections the router used for the answer are turned, deterministically, into links to the published docs.
  • Parameter recovery and rate-limit handling. If a tool call comes back malformed, or a provider’s per-minute limit is hit, the loop recovers and retries instead of failing the whole turn.

Because the router narrows the corpus, the answers are grounded in it, and these guards are code rather than model behaviour, the assistant does not need a large frontier model. A medium or small model is enough, and that is what Flui’s inference layer recommends by default.

The safety model

Scopes, ordered into tiers

Every tool declares a scope, and scopes are ordered into four tiers by blast radius:

read → plan → write → destructive
  • read: list and inspect applications, the catalog, logs and metrics, backups, migrations, mail, who can reach what.
  • plan: analysis that changes nothing, such as validating a flui.yaml.
  • write: operations that change state: install, deploy, scale, restart, stop, connect a repository.
  • destructive: delete and uninstall.

A grant at a tier implies every lower tier, so granting write also grants read and plan.

Three areas are in no tier at all: the machine room (reading it as well as operating it), granting or revoking someone’s access, and updating the platform itself. A tier is what a credential falls back to when it names no scopes of its own, so anything inside a tier can be obtained without being asked for. Stopping somebody’s cluster or handing out access has to be an explicit choice, so those scopes are only ever held by naming them. See Choose what your agent may do.

Default-deny, with three exceptions

The system is default-deny. A tool runs only if its scope is in the calling principal’s set, and an explicit grant on the credential always wins: a scoped key stays least privilege even when the person who created it is an administrator.

What a principal gets when its credential names no scopes depends on who it is:

  • An ordinary authenticated principal gets the safe default: read and plan only.
  • A platform administrator driving the assistant from their own dashboard session gets the full tier. Writes still stop for a person, and destructive ones stop for a person every time.
  • A guest in the demonstration sandbox gets one group instead, “deploy and operate applications”, because the platform default spans backups, migrations and mail, areas where a guest would only be refused.

So “no explicit grant means read and plan” is the ordinary case, not a universal rule.

Everyone is handed a different toolbox

tools/list is filtered to the calling key’s scopes. A tool this credential could never run is not advertised at all, on either surface. Two agents connected to the same installation with different keys see different toolboxes, and neither sees the whole registry.

An offered tool costs the agent context, invites a plan built around it, and ends in a refusal. Hiding it is cheaper and leads to better behaviour. Execution is still checked separately: the filter only trims what is offered and never decides what is allowed.

The two kinds of refusal are worded differently. “Missing required scope” means the tool was never given to you. That is a grant problem, fixed by reissuing the credential, and no other resource will make the call work. “Refused by Flui access control” means you hold the tool but not access to this resource. That is a permission problem, fixed by changing who can reach what. An agent that cannot tell them apart tells you the wrong thing.

Arguments are strict

An unknown argument name is rejected, and the error says both what was sent and what is accepted.

A permissive schema silently drops an unknown key, which is the worst failure for a model-driven caller. The plausible but wrong argument disappears, the real parameter is left empty, the tool runs unfiltered, and the agent gets a confident, successful answer to a question it never asked. Rejecting costs one turn, while accepting silently corrupts every step that follows.

Long work hands back an operation id

Installing, deploying, uninstalling, migrating and scaling do not finish when the call returns. They return an operation id and a status, and the tool result says so.

Nothing polls the operation for the agent. On the MCP surface, the agent is told that it must follow the operation itself with operation_status until it is done, and never claim it finished before then. In the in-product assistant, the dashboard shows a live progress panel, so the assistant is told the opposite: do not wait or re-check, just say it started.

One route waits instead of handing back a receipt

repo_map and repo_map_apply are how an agent reaches the repository-mapping engine. The first reads a connected repository (units, services, blockers, a verdict, one rendered flui.yaml per unit), and the second acts on it. They sit at the same tiers as everything else: repo_map needs mcp:app:read and repo_map_apply needs mcp:app:write. Because repo_map_apply writes to a real repository and starts real builds, it goes through the action cycle like any other write. A third tool, app_deploy_image, goes straight from a container image to a running application with no repository at all, at the same tier and through the same cycle. All three tools say in their own descriptions that they are one way among equals: reading a repository, writing a manifest by hand and shipping an image you already built are equally supported, and none is presented as the preferred path.

repo_map_apply is also the only route in the registry that does not return an operation id. Within the one call, it waits for every service the applied units declare to reach a running state before it answers, which takes minutes when the manifest attaches a new database. An MCP tool call, meanwhile, is capped at 120 seconds by default on this installation’s own API. An apply that installs a database can outlast that limit while the work keeps running on the server. When that happens, the agent is told that a mutation that timed out may still have been applied: it must read the current state before repeating it, and never repeat it twice. The CLI’s equivalent, flui repo map apply, sets no client-side timeout of its own and does not hit this limit.

Secrets never pass through an agent

An agent that needs a credential (an API key, the token that pulls container images, a mail provider, a backup destination, provider credentials, a model connection, an invitation for a new user) does not ask you to paste it into the chat. Seven tools exist only to ask a person instead, and they share three properties:

  • None of them has an argument that could carry a value. Not even an optional one for automation: an argument is context, and context ends up in the transcript.
  • Each one answers whether a thing is configured, never what it is.
  • None performs the privileged write. They return either a command for you to run in your own terminal or the screen you should open, so you make the write yourself.

An invitation link is itself treated as a credential and is never returned to an agent.

The destructive gate

A destructive tool (delete or uninstall) is offered only to a credential that carries a destructive scope. A credential without it does not see the tool at all: if an agent cannot find a delete tool, that is the reason.

Every destructive call stops for a person, every time. There is no “always” for removing something, nor for widening what a key may do.

When an agent needs a scope its key does not carry, it asks for it with credential_scope_request. You approve the request at a link, the key gains the scope, and the agent reconnects to see the new tools.

Whoever runs the instance can switch destructive tools off altogether with MCP_ALLOW_DESTRUCTIVE=false. The refusal is then deterministic and recorded.

A person in the loop

The checkpoint for each action is a person.

Flui declares more than sixty write routes as inside the action cycle. When the caller is an agent, the call does not run: it comes back as a request addressed to a person, with one plain sentence describing what was asked for and a link to the page where they answer once, always or no. The agent learns the answer by making the identical call again.

The in-product assistant is inside the same cycle, on the same person’s own session. That may look redundant with the person right there, but what decides the pause is who wrote the arguments, not whose credential it is. A dashboard form and a flui command carry what a person typed. A tool call carries what a model chose, so confirming it means checking something the person did not write.

A pause is not an error. It is returned in the shape the protocol has for waiting, so a client that understands it shows you a decision to make, and one that does not still shows a sentence saying what is being asked. An agent that mistakes it for a failure either gives up or starts changing its arguments to get around it, and changing them only raises a second question instead of answering the first.

The action cycle covers the details, including how narrow “always” is and why a standing permission can never widen what anyone may do.

Why this shape

The design starts from blast radius. The most useful thing an assistant can do is also the most dangerous, so instead of relying on every proposal being correct, the design makes a wrong one cheap to recover from. Reads and plans run freely. Writes run with the caller’s own permissions and stop for a person. Irreversible operations need a destructive scope and a person’s yes, every time. No single mistaken token and no single bad turn should be able to destroy your data.

Getting an agent connected

Three task pages cover the whole path:

  • Connect an agent: creating the credential and pointing an agent at POST /mcp. The first tool an agent should call is get_started, which returns the instructions for operating this installation and records that the credential has connected.
  • Choose what your agent may do: the permission groups a person picks from, and what each one carries.
  • When your agent stops to ask you: answering a request, and taking a standing permission back.

Where this chapter goes from here