MCP server — agentic project, flow & QA authoring
The Flowline app ships a local MCP server that lets an AI assistant (Claude Desktop, Cursor, Copilot CLI, or any stdio MCP client) drive almost the entire QA workflow in natural language: create projects, author and refine flows, define scenarios (value sets) with per-field values and expected outcomes, build regression suites, run them against a build, record results, file bugs, and read pass-rate analytics.
New here? Read the hands-on MCP tutorial for an end-to-end walkthrough (natural-language ask → tool call → result) plus a QA management guide. This page is the reference.
How it works
The MCP server is a thin stdio process that calls the tool's existing REST
API on http://127.0.0.1:8787 — the same data the web app shows. An open editor
merges what it writes (08).
- Start the app/API first (
npm run devornpm start). - Point your assistant at the MCP server (below).
- Pick the workspace with
list_workspacesandselect_workspace(which workspace a session uses).
Every write tool reads the project, applies a pure transform, and writes only the changed files, retrying up to 3 times when another save changed them first (how saves resolve). Unknown ids fail fast with a clear error instead of silently succeeding.
Configure your assistant
The server is started with npm run mcp (which runs tsx server/mcp/index.ts).
Set FLOW_API_URL if your API is not on the default port. All stdout is
reserved for JSON-RPC; logs go to stderr.
Claude Desktop (claude_desktop_config.json)
{
"mcpServers": {
"flowline": {
"command": "npx",
"args": ["tsx", "server/mcp/index.ts"],
"cwd": "/absolute/path/to/flowline",
"env": { "FLOW_API_URL": "http://127.0.0.1:8787" }
}
}
}
Cursor (.cursor/mcp.json) / Copilot CLI / other MCP clients
Use the same command / args / cwd / env. Any stdio MCP client works.
Remote / hosted MCP (multi-user)
When the app is deployed with authentication enabled, anyone with an account can use the MCP over the public internet — no repo checkout, no local process.
Generate a token. Sign in to the web app, open the MCP access tokens panel in your profile, create a named token, and copy it (it is shown once).
Point your assistant at the hosted endpoint (
https://<your-host>/mcp), sending the token as a bearer header:{ "mcpServers": { "flowline": { "url": "https://<your-host>/mcp", "headers": { "Authorization": "Bearer fl_pat_…" } } } }Choose a workspace. Each MCP session is isolated to your account. Call
list_workspaces, thenselect_workspace { workspaceId }. The selection lives in the MCP session and never changes your web-app active workspace. Callcurrent_workspaceto see the current target.
Notes
- Tokens are shown once and stored only as a hash; revoke them anytime in the profile panel. Token management itself requires an interactive (cookie) login.
- The endpoint must be served over HTTPS. To enable DNS-rebinding protection, set
MCP_ALLOWED_HOSTSto your public host(s) (comma-separated).
Local folders (no server, no account)
You can also point an assistant straight at folders on your own machine — no sign-in, no token, no repo checkout:
{
"mcpServers": {
"flowline": {
"command": "npx",
"args": ["-y", "flowline-mcp", "mcp", "/absolute/path/to/your/flows"]
}
}
}
This boots the folders itself; nothing else needs to be running. Add
FLOWLINE_SITE/FLOWLINE_TOKEN to env to reach your hosted account from the
same entry:
{
"mcpServers": {
"flowline": {
"command": "npx",
"args": ["-y", "flowline-mcp", "mcp", "/absolute/path/to/your/flows"],
"env": {
"FLOWLINE_SITE": "https://<your-host>",
"FLOWLINE_TOKEN": "fl_pat_…"
}
}
}
}
list_workspaces then returns both, with ids qualified as hosted:<id> /
local:<id> so select_workspace can target either. Full walkthrough: Edit a
folder on your computer.
Tool reference
64 tools, grouped by area — the registerTool calls under server/mcp/ are
the source of truth. projectId and flowId are required by every
flow-scoped tool unless noted. Node ids, scenario ids, suite ids, etc. are the
real ids returned by the corresponding list_* / get_* / create_* tools.
Workspaces (session-scoped)
| Tool | Args | Description |
|---|---|---|
list_workspaces |
— | List your workspaces across every backend this session can reach (hosted account and/or local folders). Ids are qualified as hosted:<id> / local:<id>; a backend that can't be reached is reported under unavailable instead of failing the call. |
select_workspace |
workspaceId |
Target a workspace for this session by its qualified id (does not change your web-app active workspace). |
current_workspace |
— | Show the session's target workspace (selected, or active as a fallback) and which backend it lives on. |
create_local_workspace |
name, path |
Register path, an existing writable folder under the server's allowed root, as a Local workspace (where projects are stored). It does not select it: call select_workspace with its id from list_workspaces. |
Projects
| Tool | Args | Description |
|---|---|---|
list_projects |
— | List all projects in the session's target workspace. |
create_project |
name |
Create a new empty project. Returns the new id. |
get_project |
projectId |
Compact summary of a project and its flows. |
Flows
| Tool | Args | Description |
|---|---|---|
create_flow |
projectId, spec |
Build a whole flow from a high-level spec (see below). |
get_flow |
projectId, flowId |
Return a flow as a high-level spec (point/end key = real node id). |
rename_flow |
projectId, flowId, name |
Rename an existing flow. |
create_flow spec
Reference nodes by author keys (not UUIDs). The server assigns ids, wires
edges (fromHandle = branch), lays out the graph (BFS auto-layout), and returns
warnings (dangling_branch, unreachable_node, auto_added_branch).
{
"projectId": "<id from create_project>",
"spec": {
"name": "PIN entry",
"entry": "idle",
"points": [
{ "key": "idle", "title": "Idle screen", "branches": ["start"] },
{ "key": "pin", "title": "Enter PIN", "branches": ["correct", "wrong"] }
],
"ends": [
{ "key": "ok", "outcome": "Cell opens" },
{ "key": "locked", "outcome": "Account locked" }
],
"edges": [
{ "from": "idle", "branch": "start", "to": "pin" },
{ "from": "pin", "branch": "correct", "to": "ok" },
{ "from": "pin", "branch": "wrong", "to": "locked" }
]
}
}
A branch is either a bare name or an object. The object form carries the rest of the branch model:
{ "name": "Sign in", "id": "signin-wrong", "result": "Wrong password",
"kind": "validation", "staysOnCard": false,
"expected": [{ "label": "Red text under the password field" }] }
nameis the action;resultis what the system answers. The card renders them asSign in / Wrong password.idis whatedges[].branchrefers to; it defaults toname. Two branches on one card that share a name must each setid, or the edges cannot tell them apart.kindisui,network, orvalidation.staysOnCard: truemarks an outcome that records in place — an inline error — and must own no edge.expectedholds results the tester verifies for that outcome specifically, separate from the card-wideexpected.
get_flow returns branches in this same form.
spec.notes carries the canvas notes:
{ "text": "Ask design about the empty state", "position": { "x": 40, "y": 80 },
"pinnedTo": "pin", "color": "#ffd166" }
textis the note body;positiondefaults to0,0.pinnedTois a spec key — an author key here, a node id inget_flowoutput, where keys are ids. A key no node claims is anunknown_note_pinerror.get_flowemitsnotesin this same form, so aget_flow→create_flowround trip keeps every note and its pin.
Graph (incremental editing)
Operate on a live flow by id. Get node ids from get_flow. Mutations return
warnings for non-fatal notes (auto_added_branch, pruned_edges,
entry_cleared).
| Tool | Args | Description |
|---|---|---|
add_point |
projectId, flowId, point |
Add a card (test step) node. |
add_end |
projectId, flowId, outcome |
Add an end node with an outcome. |
add_edge |
projectId, flowId, from, to, branch? |
Connect two nodes; auto-adds the branch if missing. |
update_point |
projectId, flowId, … |
Patch a card's title / description / inputs / expected / branches. |
update_end_outcome |
projectId, flowId, nodeId, outcome |
Update an end node's outcome label. |
delete_node |
projectId, flowId, nodeId |
Delete a node and its edges; clears entry if it was the entry. |
remove_edge |
projectId, flowId, edgeId |
Remove an edge. |
set_entry_node |
projectId, flowId, nodeId |
Set the flow entry to a card node. |
move_node |
projectId, flowId, nodeId, position {x,y} |
Reposition a node on the canvas. |
add_branch |
projectId, flowId, nodeId, branch, result?, kind?, staysOnCard? |
Add a branch to a card. branch is the action, result what the system answers; kind is ui, network or validation; staysOnCard: true marks an outcome that records in place and owns no edge. |
remove_branch |
projectId, flowId, nodeId, branch |
Remove a branch and prune its edges. |
rename_branch |
projectId, flowId, nodeId, oldName, newName |
Rename a branch. |
update_branch |
projectId, flowId, nodeId, branch, patch |
Patch an existing branch: action, result, kind, staysOnCard, expected. Omitted keys are left untouched; result: null and kind: null clear them; expected: [] clears the branch's expected results. branch is an id, an action, or the full action / result label. Setting staysOnCard: true drops the branch's edge and returns a pruned_edges warning. |
set_branch_position |
projectId, flowId, nodeId, branch, side, offset |
Place a branch outlet. side is top, right, bottom or left; offset is 0–1 along that side. |
set_branch_color |
projectId, flowId, nodeId, branch, color |
Set a branch color. color is required; pass color: null to clear it. |
Card images & backgrounds
Attach images to point cards and end nodes, and control how they render. Images
may be PNG, JPEG, WebP, GIF, or SVG, up to 8 MB. Supply each image as either
image.base64 (a data: URL prefix is accepted) or image.url — exactly
one. URL sources are fetched server-side and must resolve to a public host
(loopback/private addresses are refused).
| Tool | Args | Description |
|---|---|---|
add_point_image |
projectId, flowId, nodeId, image, setAsBackground? |
Attach an image to a card. The first image becomes the background automatically; pass setAsBackground: true to force a specific one. |
set_point_background |
projectId, flowId, nodeId, assetId (nullable) |
Pick which attached image is the card background; pass assetId: null to clear (falls back to the first image). |
remove_point_image |
projectId, flowId, nodeId, assetId |
Detach an image from a point card; clears the background if it pointed there. |
set_end_background |
projectId, flowId, nodeId, image? |
Set an end node's background image; omit image to clear it. |
set_card_image_mode |
projectId, mode, overlayOpacity? |
Project-wide toggle between background and thumbnails rendering, with an optional darkening overlay opacity (0–1). |
Notes
A note is authoring commentary on the canvas. It sits beside the graph, not in
it: notes never appear in flow order, scenarios, runs, the table, or the player.
Pin a note to a card or end with pinnedTo and it travels with that node and is
deleted with it; its position is then relative to that node instead of the
canvas. get_flow returns the flow's notes, and create_flow accepts them.
| Tool | Args | Description |
|---|---|---|
add_note |
projectId, flowId, text, position? {x,y}, pinnedTo?, color? |
Add a note. position defaults to 0,0; pinnedTo is a card or end node id. Returns noteId. |
update_note |
projectId, flowId, noteId, patch |
Patch text, position, pinnedTo, or color. Omitted keys are left untouched; pinnedTo: null unpins the note and leaves it where it is. |
delete_note |
projectId, flowId, noteId |
Delete a note. |
Scenarios
A scenario is a named value set: per-field input/expected values, an optional expected outcome, a background image per card, and a pinned path used for guided runs.
The tools below reach the values, the expected outcome, the per-screen expected notes, and the per-card background overrides. The pinned path is UI-only — set by pinning a branch in the Table or by Update path / Save as new in the Player.
| Tool | Args | Description |
|---|---|---|
list_scenarios |
projectId, flowId |
List scenario summaries, each with its owner. |
create_scenario |
projectId, flowId, name |
Create a scenario. Returns scenarioId. |
duplicate_scenario |
projectId, flowId, scenarioId |
Deep-clone a scenario ("(copy)"). |
rename_scenario |
projectId, flowId, scenarioId, name |
Rename a scenario. |
delete_scenario |
projectId, flowId, scenarioId |
Delete a scenario (prunes it from suites + run results). |
set_scenario_expected_outcome |
projectId, flowId, scenarioId, outcome (nullable) |
Set/clear the expected outcome (null clears). |
set_scenario_value |
projectId, flowId, scenarioId, nodeId, fieldId, value |
Set a field override; empty value removes it. |
set_scenario_expected_note |
projectId, flowId, scenarioId, nodeId, note |
Set a per-screen freeform expected note; blank removes it. |
set_scenario_background |
projectId, flowId, scenarioId, nodeId, assetId (nullable) |
Set the card's background asset for this scenario; null clears the override. |
Suites
| Tool | Args | Description |
|---|---|---|
list_suites |
projectId, flowId |
List suite summaries, each with its owner. |
create_suite |
projectId, flowId, name |
Create a suite. Returns suiteId. |
update_suite |
projectId, flowId, suiteId, name?, description? |
Update name/description. |
delete_suite |
projectId, flowId, suiteId |
Delete a suite and its cycles (suite runs). |
duplicate_suite |
projectId, flowId, suiteId |
Deep-clone a suite definition. |
set_suite_scenarios |
projectId, flowId, suiteId, scenarioIds[] |
Replace the ordered scenario list (every id must exist). |
Suite runs & analytics
| Tool | Args | Description |
|---|---|---|
start_suite_run |
projectId, flowId, suiteId, environment, buildVersion, testDate, release? |
Start a cycle (one untested result per suite scenario). Each scenario goes to its owner, else to the suite's owner; the rest stay unassigned. Returns suiteRunId. |
set_suite_result |
projectId, flowId, suiteRunId, scenarioId, status, assignee? |
Record a scenario outcome. status is one of untested, pass, fail, blocked, skipped, retest. assignee is an email or a name; "" clears it. This is the agentic way to record outcomes. |
finish_suite_run |
projectId, flowId, suiteRunId |
Mark a run finished (stamps finishedAt). |
delete_suite_run |
projectId, flowId, suiteRunId |
Delete a run. |
list_suite_runs |
projectId, flowId, suiteId? |
Run summaries (counts, pass rate, progress) with startedBy and finishedBy, optionally per suite. |
get_suite_run |
projectId, flowId, suiteRunId |
One run with its full results + summary. |
list_suite_run_results |
projectId, flowId, suiteRunId, scenarioId? |
Every result recorded in a run, newest first, with recordedBy, recordedAt and latest (each scenario's current result); optionally one scenario's. |
get_suite_analytics |
projectId, flowId, suiteId |
byBuild / byRelease pass-rate series + latestFailing scenarios of the most recent run. |
Bugs & runs
| Tool | Args | Description |
|---|---|---|
list_bugs |
projectId, flowId, suiteRunId?, testSessionId? |
Bug summaries with reportedBy, optionally filtered by suite run or test session. |
file_bug |
projectId, flowId, title, description?, expected?, actual?, severity?, status?, suiteRunId?, buildVersion?, valueSetId? |
File a bug. severity is one of low, medium, high, critical; status is one of open, fixed, wontfix. Returns bugId. |
delete_bug |
projectId, flowId, bugId |
Delete a bug. |
export_bugs |
projectId, flowId?, suiteRunId?, testSessionId?, bugIds? |
Export bugs with their attachments as a zip — per-bug attachment folders, manifest.csv, and report.pdf. Filter by flow, suite run, test session, or explicit ids; omit every filter to export the whole project. Returns { filename, base64 }; decode the base64 and write it to a .zip. |
list_runs |
projectId, flowId |
Player-run summaries (id, scenario, status, build, started, tester). |
delete_run |
projectId, flowId, runId |
Delete a player run. |
owner, tester, reportedBy, startedBy and finishedBy are people as the project stores them (see the layout). A summary leaves the field out when the project doesn't record one.
Lifecycle (destructive)
| Tool | Args | Description |
|---|---|---|
delete_flow |
projectId, flowId |
Permanently delete a flow with its graph, scenarios, suites, runs, bugs and image files. The flow's folder goes too, unless it still holds files Flowline didn't write. |
delete_project |
projectId |
Permanently delete a project and all its flows. |
What's intentionally not exposed
These stay UI-only (by design) — the MCP surface is for authoring and QA orchestration, not binary/identity/transport concerns:
- Git / auth management — connecting a workspace to a git remote or Google Drive, and signing in, are UI + server concerns.
- Raw asset bytes & rendering — fetching or streaming stored image bytes is a multipart/transport concern handled in the player/table UI; MCP treats asset ids as opaque. (Attaching images and choosing backgrounds is now supported — see Card images & backgrounds above.)
- Full player-run recording — stepping a flow card-by-card and capturing a
Runwith steps is the player's job. The agent records manual suite outcomes viaset_suite_result; it never fabricates a full player run. (This keepsrecordSuiteScenarioRunthe single writer of suite-tagged runs.) - Project export / import & session PDF reports — generated from the UI.
(Bug export is available — see
export_bugsabove.) - Template instances & QA test sessions — separate features not part of the scenario/suite path.
Safety notes
Which workspace a session uses, permanent deletes, and what happens when you and an agent change the same item: Driving the tool with MCP → Safety notes.