For an AI agent driving Vflin over MCP. This is the method (deliverable 4 of
AGENT_TOOLING_STRATEGY.md (internal: docs/20-control-plane/AGENT_TOOLING_STRATEGY.md)) — the workflow, the safety rules, and the idioms — so an
agent uses the tools well instead of inventing workflows or tripping the danger gate. The tool list itself is
not here (it would drift): call tools/list for the live, authoritative set (each tool’s description carries
its danger level, required mode, and — for long ops — the status tool to poll). Setup + per-client config:
crates/vflin-mcp/README.md (internal: crates/vflin-mcp/README.md).
What Vflin is
A browser-profile OS: antidetect profiles (signed, encrypted blobs), launched on a browser core, with
automation (scripts/graphs, task groups, schedules, datasets), sync, and a hub. Every tool is a thin 1:1
wrapper over the Local API (ADR-0003) — if the vflin CLI can’t do it, neither can you.
The method: Audit → Plan → Act
- Audit — read before you write.
system.health,profile.list,automation.script.list,automation.run.listare read-only (green) and safe to call freely. Discover ids before acting on them. - Plan — state the few mutating calls you intend. Mutating tools are explicit, never a side effect of a read.
- Act — make the mutation, then verify (re-read / poll). Don’t assume success; check the envelope.
Safety: session mode + danger levels
Each tool has a danger level (in its tools/list description) and runs under a session mode
(VFLIN_MCP_MODE, set by the operator): read-only < normal < trusted.
| danger | needs mode | examples |
|---|---|---|
| green (read-only) | read-only |
system.health, *.list, *.get, *.status |
| yellow (mutating) | normal |
profile.create, profile.start, automation.run.start, cloud.push |
| red (dangerous) | trusted |
(none yet — reserved for irreversible/exfil ops) |
- A tool above the session mode is refused before any daemon call —
isError:true, with anextStepstelling the operator whichVFLIN_MCP_MODEto set. Don’t retry the same call; ask the operator to raise the mode, or pick a read-only alternative.initializetells you the active mode up front. - Treat a refusal as a policy signal, not a transient error.
Where the daemon is (the installed product, A88)
vflin-mcp reads the Local API discovery file $VFLIN_HOME/local-api.json, else ~/.vflin/local-api.json —
the same root the daemon writes and the shell and the CLI read; nothing uses %LOCALAPPDATA%\VFlin. The
installed product ships the server beside the app: %LOCALAPPDATA%\VFlin Antikflin-mcp.exe — that is the
command an MCP client’s mcpServers entry points at (the directory is not on PATH). The daemon itself is
started by the app on launch and stays up after the window closes; if no daemon answers, the app (or vflind.exe
from the same directory) starts one — the MCP server never starts a daemon.
Getting CDP on a profile (A89)
Do not look for a port: by default there is none. Start the profile, then vflin_profile_cdp_attach (needs
VFLIN_MCP_MODE=trusted — ask the operator; never retry the same call) and drive the returned wsEndpoint
with your CDP client; it serves one client and expires — vflin_profile_cdp_detach when done. A profile
whose detail says cdpTransport: port publishes its port to every local process; treat brokered:false
as that fact, not as a convenience.
Poll, don’t block
Long-running tools (e.g. automation.run.start, automation.taskgroup.run) return an id immediately and
queue the work. Their result’s summary/nextSteps name the status tool to poll (e.g. automation.run.status
with {id}). Poll that to a terminal state (succeeded/failed) — never assume completion, never busy-block
the operator. The tool’s tools/list description marks it long-running.
Reading results
Every result is JSON in the text block: { "summary": …, "envelope": { ok, data } } on success, or
{ "summary": …, "ok": false, "error": …, "nextSteps"? : … } on failure (isError:true). Read summary first,
then data for detail.
Failure playbook (the nextSteps hints)
On a recoverable error the result carries a nextSteps keyed on the daemon’s code:
| code | meaning | do |
|---|---|---|
NOT_FOUND |
the id doesn’t exist | call the matching *.list to get a valid id |
LOCKED |
the crypto session is locked | session.unlock first, then retry |
LOCKED_REMOTE |
the profile is held by another device | check its sync lock status, then retry |
UNAUTHENTICATED |
no cloud/session auth | cloud.login (or session.unlock) first |
POLICY_DENIED |
the daemon’s policy refused it | needs a higher policy mode |
TOTP_REQUIRED/TOTP_INVALID |
2FA needed | re-call with a current totpCode |
STALE/CONFLICT |
another device changed it | pull/refresh the latest, then retry |
Recipes (the live tool names are in tools/list)
- Run a script on one profile:
profile.list→automation.script.list→automation.run.start(id) → pollautomation.run.statusto terminal → read outputs (automation.run.artifactif any). - Fan a script across N profiles:
automation.taskgroup.runwith the profile ids (id) → pollautomation.taskgroup.getuntil every run is terminal. - Create + launch a profile:
profile.create→profile.start(returns the CDP endpoint).
Hard rules
- Never invent a tool — only what
tools/listreturns exists. - Never retry a policy refusal; escalate the mode or change approach.
- Never block on a long op — poll its status tool.
- A RUNNING profile is never synced; the daemon enforces this — don’t fight it.