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:
- 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.
- 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 isget_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
- The action cycle: why an agent’s write stops and asks, and what “always” means.
- How Flui reads a repository:
what
repo_mapandrepo_map_applydo, and the citation and confidence behind every fact they report. - Who can reach what: the access model every one of those tool calls lands on.
- Inference and bring-your-own-LLM: how the assistant and MCP get their model endpoint.
- Identity and OIDC: where the authenticated principals, and the machine keys the MCP surface accepts, come from.
- Observability is not an afterthought: the logs and metrics the read tools surface when you ask the assistant to debug an app.