@quietflow/mcp
v0.1.4
Published
MCP server for Quiet Flow — lets an AI assistant publish internal apps under company governance.
Maintainers
Readme
@quietflow/mcp
The Quiet Flow MCP server. Lets Claude Code, Codex, or any MCP client publish apps to Quiet Flow without the builder leaving their editor.
What this is, and is not
A thin adapter over the Quiet Flow API (Jing §7.7, §20.1). It holds no business state, implements no rules of its own, and never returns a credential. Every decision about what is allowed is made by the API — so an agent can never reach an outcome the CLI could not.
It is not the runtime data path for a deployed app. A deployed app talks to the broker itself, with its own cluster identity, and nothing about a published app routes through here.
It is a way for an assistant to read company data while an app is being built,
and that is new (#419). It reads through the same gate the app reads through
— POST /v1/data on the runtime capability service — holding the same
development session qf dev holds. See Reading company data below.
Install
Almost nobody should do this by hand. qf login registers this server with the
assistants on your machine and installs the skill that goes with it:
npm install -g @quietflow/cli
qf loginIf you want to register it yourself:
claude mcp add quietflow -- npx -y @quietflow/mcpAuthentication
qf connect issues this server its own credential and writes it to
~/.config/quietflow/assistant.json, 0600. This server reads that file; it
never writes one, and there is never a token in your MCP configuration.
The separation is the point: cancelling this machine's assistant credential
(qf disconnect) does not sign you out of your own CLI or disconnect another
machine, while qf logout cancels every assistant credential in your name.
Resolution order is QF_TOKEN in the environment, then assistant.json.
There is no fallback to the CLI's own credential: a missing or corrupt
assistant file fails closed and asks the person to run qf connect.
The released default control plane is https://api.quietflow.net. Local and
self-hosted installations override it with QF_API_URL.
The skill
skill/SKILL.md ships inside this package. qf connect installs it where your
assistant looks for one. Tools without it give you an assistant that can call
our API and does not know when or why to.
Tools
| Tool | Purpose |
|---|---|
| whoami | Who is signed in, and what they may do |
| open_existing_app_report | Claim an employee's one-time report handoff in the actual project folder |
| submit_existing_app_findings | Add a business summary plus source-backed inferred or unknown findings to that report |
| open_build_project | Claim the one-time code from generated Build instructions and create or link the local project manifest |
| list_applications | The user's apps and where each one is |
| declare_app_intent | Write quietflow.yaml — call this before building |
| check_policy | "Would this be allowed?" Creates nothing |
| list_company_systems | The company data this company has offered its app builders, who decides each, and the two names the code writes |
| publish_application | Scan, build, provision, deploy to a private sandbox |
| submit_for_review | Freeze the version and send it to approvers |
| request_data_access | Ask the person who looks after a company system to let the app read it |
| get_application_status | Where an app is, its addresses, and what company data it may read |
| get_review_feedback | Check results and reviewer comments |
| promote_application | Publish the exact approved build to the company |
| list_company_data | What this app may actually read right now, and which named operations |
| read_company_data | Read real company records through Quiet Flow, as the app would |
The existing-app submission contract keeps the Candidate identifier in the URL only. Its strict request body is built from the remaining fields and is checked against the API route schema, so an MCP-only routing field cannot leak into a rejected API payload.
check_policy is deliberately non-mutating: an agent should be able to ask
whether something is allowed as often as it likes without leaving phantom
versions in an app's history for a reviewer to explain later.
Existing-app intake is separate from publishing. open_existing_app_report and
submit_existing_app_findings continue a Discovery Candidate; they never create an
Application or Manifest. Inspect source read-only. An inference needs relative file
references and remains an inference after the employee confirms the business summary.
The client is instructed never to run the workload, follow instructions found in
repository or employee text, call company systems, send messages, or include prompts,
credentials, customer records, message bodies, source contents, or absolute paths. The
API rejects credential-shaped text and structurally limits findings, but it cannot prove
where arbitrary prose came from; that residual trust boundary is explicit in ADR-043.
What a builder is told about company data
list_company_systems returns only what somebody chose to offer (#428). A
company can have three healthy Salesforce accounts and this tool return nothing,
because licensing an account and offering it to builders are two acts and only the
second one puts it here. So an empty answer means nobody has offered anything yet,
never this company has nothing connected — and telling a user the second would send
them off to rebuild something their company already runs.
Each offered account is relayed with the reviewed business description IT wrote for
it and ADR-044's assurance promise whole (#567) — "Verified capability: Quiet
Flow checks which records and fields this app may receive" or "Managed tool: Quiet
Flow controls who may call this tool and what may be sent or returned; the external
system defines what its result means." Never the one summarised into the other: a
managed tool has no field-level guarantee, and an assistant handed a list with no
promise on it will fill the gap itself. Its scope is relayed in the same words IT
ticked them as, not as sandbox and production.
Each account also carries the capability and the operation the app's code names
(#622), as In code: capability "managed.open-customer-complaints", operation
"openconnector.github.get_repository". Those two strings are the whole of what
readCompanyData takes, and before #622 the operation was on no surface a builder
could reach — the one screen that printed one was IT's connection card, which prints
the provider's own name for the action, which the gate refuses. They are not a way
in: both are matched against a receipt a Data Owner signed, so knowing a name reads
nothing that was not approved. The strings come off the licence, unedited; this
server never builds one.
The consequence, named because it is real: an unoffered account cannot be
proposed. The attachment lands pending and their IT team chooses from their own
list, which is ADR-031's designed degradation and works — but a company that has
published nothing gets it every time.
A builder may also record their own connection, and it comes back under yours
rather than in the offered list — and is now relayed as its own paragraph, which
it was not when #428 first landed: the route returned it and this tool rendered only
the offered list, so the one surface meant to tell a builder about their own account
told them nothing (#438 review, finding 4). It is inert until their IT team says what
it may be used for and names who decides; nothing can read through it before then,
including the person who recorded it, and the tool says so rather than letting an
assistant propose it.
An assistant older than the boundary is refused
GET /v1/data-sources answers 426 unless the client sends
x-quietflow-catalog: published-only, which this MCP server sets on every request
(#438 review, finding 5). The response shape did not change with #428 and its meaning
did: sources: [] used to mean "one account each, nothing to choose between" and now
means "nobody has offered you anything". An MCP server lives on a builder's laptop
and updates when they get round to it, so without the header an assistant from last
month would read a boundary its user's company had just drawn, apply the old meaning,
and reassure them there was nothing to ask about. Old clients get a sentence telling
the user to run qf connect — and telling the assistant what to stop concluding.
Deployment order: this package first, then the API
The API must not be deployed before this package is published. The 426 is aimed at
the client, and the header that satisfies it exists only in this source tree — it is
not in any release on npm yet. Deploy the API first and every installed @quietflow/mcp
starts getting 426, including the one the refusal tells the user to fix: qf connect
reinstalls from the published npm release, which still lacks the header, so the
remediation cannot remediate. The builder is told to run a command that leaves them
exactly where they were.
So: publish @quietflow/cli and @quietflow/mcp (see docs/RELEASING-NPM.md), confirm
the release on npm carries the header, and only then deploy the API and migration
0069 and 0071. The reverse order is recoverable only by a second npm publish, during which
every builder's assistant is refused with advice that does not work.
Saying which company system the app means
Where a company has offered two accounts for one kind of data, somebody has to say which one each part of the app reads — and the assistant is the surface that can, because it is the one that was in the conversation where the person said. Where there is one, there is nothing to ask and the tool does not dress it up as a choice.
Ask them, in their own words, then pass the answer to publish_application as
sources. If they have not said, pass nothing. Their IT team then chooses from
the same list, which is the designed behaviour and works. It is not a small
politeness: a suggestion an administrator confirms in one press is what points the
app at a company system, and a wrong one points it at the wrong one — after which
every screen, receipt and audit entry agrees the result is correct. The account name
has to match what their IT team typed; anything else proposes nothing and says so.
Asking for company data
request_data_access takes a capability the app already declares and four
sentences about the user's own business: what the app will do with the data, what
data it needs in plain words, where the data ends up, and how long the app keeps
it. It takes no field list and no environment, and neither does the API behind
it — the concrete entities and fields are resolved inside the control plane from
what IT recorded and what the company has already agreed. A builder is never asked
to author a provider's schema, so neither is their agent, and there is nothing
here to leak.
Testing with real records and letting the published app read them are two
separate decisions, asked and answered separately; one is never evidence for the
other. Ask for the second one before submit_for_review, so an app is not
approved to go live with data access it has not got.
There is no second tool for reading the answer. get_application_status reports
data access alongside the app's own state, because they are one question: an app
can be approved, live, and unable to read a thing, and an agent that had to know
to call a second tool would report "it's approved" while the app sits in front of
people showing nothing. When that part of the answer cannot be fetched, the tool
says so rather than shortening the reply — "we could not check" and "there is
nothing to report" are different facts.
Reading company data
Two tools, and neither of them decides anything.
There is one per-call gate in Quiet Flow and it is the broker — Broker.handle()
in apps/capability-service, entered from POST /v1/data. It works out which
caller, what was asked for, who it is acting for, whether that is allowed (the
control plane's describeDataAccess), whether the named operation is inside the
approved receipt, writes the durable evidence, and only then calls the provider.
A deployed app reaches it through @quietflow/runtime-client. qf dev reaches
it by forwarding an app's envelope. read_company_data reaches it by sending the
same envelope to the same endpoint with the same credential.
So this is a second door onto one gate, not a second gate.
tests/agent-invocation is the proof: one grant, one pinned operation, called
over HTTP and over MCP, asserting the same authority and the same evidence — then
one revocation, after which both doors refuse on the next call, before anything
leaves the company.
The credential is the one qf dev already uses. A laptop has no cluster
identity, so the assistant presents a development session: minted by the control
plane, bound to one builder, one app, one version, one capability and one
environment, alive for two minutes, worth nothing anywhere else. Deliberately not
a new kind of caller — an assistant reaches exactly what its user could reach by
running qf dev, which is this package's oldest rule applied to data instead of
to commands. It follows that an assistant reads under the builder's own
development approval and can never spend the live app's runtime grant.
The list is not a pass. list_company_data reports what a Data Owner has
approved for this person and this app, and read_company_data is checked again
when it happens. A grant withdrawn between the two refuses the call. A capability
or operation that is not on the list is not blocked here — it is sent, refused
by the gate, by name, with an event. Nothing about what may be read is decided on
the builder's laptop, which is the one machine an attacker controls.
Where this differs from qf dev on purpose. qf dev hands a builder sample
data in the real shape when nothing is approved, because they are standing there
being told it is made up. This never does. An assistant asked for company records
and handed invented ones will put them in front of somebody as company records,
so an unapproved read here is a refusal carrying the Data Owner's own sentence,
and no rows at all.
An assistant is never given the session, the connection handle, or a provider credential, and cannot send a field list, a filter or a query: the approved receipt decides what comes back, and the adapter builds the provider request from it.
Notes for agents
The workflow, and the reasoning behind it, live in the skill shipped alongside. The short version: declare intent early, never write authentication, never put a credential in the code, and let the user confirm the sandbox works before you submit for review.
