Veilus

用於在一台電腦上管理多帳號的反偵測瀏覽器:每個設定檔都有獨立的指紋和代理,並可透過MCP讓您的AI助理編寫和執行自動化操作。

文件

These docs describe the Veilus release after v0.2.1, coming soon. If you have v0.2.1, some screens and features (Datasets, Trash, the new profile panel, many MCP tools) are not in your version yet.

MCP (Model Context Protocol) is the standard way LLM tools like Claude Code, Cursor, and Claude Desktop connect to external tools — Veilus speaks it over stdio, so an LLM can import proxies, create and launch profiles, drive a running profile’s page, write and run Veilus Flow scripts, and schedule them — on your behalf.

Like the REST API, this needs the local API turned on first, from the API & MCP page in the app sidebar — that page also mints the token you’ll need below.

Connecting

The Veilus app binary has an mcp subcommand that speaks MCP over stdio. The client needs the path to that binary and your token, passed as the VEILUS_API_TOKEN environment variable.

Claude Code

claude mcp add --scope user veilus -e VEILUS_API_TOKEN=<token> -- "<path to Veilus>" mcp

--scope user makes Veilus available in every folder you open Claude Code in; without it, Claude Code only sees Veilus in the folder where you ran the command.

Cursor / Claude Desktop

Add this to the client’s MCP server config:

{
  "mcpServers": {
    "veilus": {
      "command": "<path to Veilus>",
      "args": ["mcp"],
      "env": {
        "VEILUS_API_TOKEN": "<token>"
      }
    }
  }
}

The API & MCP page fills in the real binary path and token for you once you’ve minted one — copy the snippet from there instead of typing it by hand.

Tools

Profiles

Proxy

import_proxies

Create a static proxy pool from proxy lines (host:port, host:port:user:pass or user:pass@host:port). Prefix a line with socks5:// for a SOCKS5 proxy; http:// or no prefix means HTTP; https:// is rejected (TLS to the proxy is not supported). Bad lines are reported in `rejected` by line number (counting from 1) and do not block good ones; if no line is valid, nothing is created. Returns pool_id for create_profiles or assign_proxy_pool. Passwords are stored but never returned.

ParameterTypeRequiredDescription
linesstring[]yesOne proxy per element
namestringyes
typestringno

list_proxy_pools

List proxy pools: `pool_id`, `name`, `kind` (static or rotating), `entry_count` and `countries` detected for their exit IPs. Never returns credentials.

test_proxy

Test every proxy in a pool now: alive or dead, latency, exit IP and its country and timezone. Refreshes the pool's cached geo, which create_profiles uses. A dead proxy can take up to about 10 seconds.

ParameterTypeRequiredDescription
pool_idstringyesProxy pool UUID

assign_proxy_pool

Assign a proxy pool to existing profiles, one slot each. Refuses the whole call (nothing assigned) if any profile already has a proxy or pool, listing them; pass force: true to replace. Does NOT change the profiles' timezone: if the new proxy is in another country, launch_profile will be blocked by the geo check. New profiles should get their pool through create_profiles instead.

ParameterTypeRequiredDescription
forcebooleannoReplace an existing proxy. Default false
pool_idstringyesProxy pool UUID
profile_idsstring[]yes

Page driving

navigate

Load a URL in the tab of a RUNNING profile and wait for its load event (up to 15 s); on timeout it PROCEEDS ANYWAY and the result says the page is still loading - then call wait_for_element for what you need before reading the page. The profile must already be open - call launch_profile first; this never opens it.

ParameterTypeRequiredDescription
profile_idstringyesUUID of a running profile (launch_profile first)
urlstringyes

snapshot

Accessibility snapshot of the current page of a running profile: one line per element with role, name and [id=N]; pass N as node_id to click or type. Ids change when the page changes - take a new snapshot after navigating or clicking. Lists at most 150 elements and 64 KiB; truncated: true means the page has more - use wait_for_element with a CSS selector to reach an element that is not listed. Elements inside same-origin iframes follow a `--- iframe URL ---` line and work with click and type like any other; a cross-origin iframe gets a `(cross-origin …)` line with its URL but its elements are not listed - navigate to that URL to work inside it.

ParameterTypeRequiredDescription
profile_idstringyesUUID of a running profile (launch_profile first)

click

Click an element of a running profile's page by node_id with a real mouse: the pointer moves to a point inside the element, then presses and releases. Fails if the page changed since the snapshot (take a new one), if the element has no visible box (hidden or zero-size — click its visible label or parent), or if another element covers it (the error names it — close or scroll past it first). A JavaScript alert/confirm/prompt that the action opens is answered at once: `dialog` "dismiss" (default, i.e. Cancel) or "accept" (OK), with `prompt_text` as the typed answer to a prompt; the result says which dialog opened and how it was answered.

ParameterTypeRequiredDescription
dialogstringno
node_idintegeryesnode_id from snapshot ([id=N]) or wait_for_element
profile_idstringyesUUID of a running profile (launch_profile first)
prompt_textstringno

type

Focus an element by node_id and type text into it, one key event per character (at most 5000 characters). Replaces the field's current content: existing text is removed before typing. If the result notes that the value could not be confirmed (masked, length-limited or auto-formatted fields), verify the field with snapshot or evaluate_js.

ParameterTypeRequiredDescription
node_idintegeryesnode_id from snapshot ([id=N]) or wait_for_element
profile_idstringyesUUID of a running profile (launch_profile first)
textstringyes

press_key

Press one named key in a running profile's page: Enter, Tab, Escape, ArrowUp, ArrowDown, ArrowLeft, ArrowRight, Backspace, Delete or Space. If no input is focused, the LAST input on the page is focused first. For text use `type`. A JavaScript alert/confirm/prompt that the action opens is answered at once: `dialog` "dismiss" (default, i.e. Cancel) or "accept" (OK), with `prompt_text` as the typed answer to a prompt; the result says which dialog opened and how it was answered.

ParameterTypeRequiredDescription
dialogstringno
keystringyes
profile_idstringyesUUID of a running profile (launch_profile first)
prompt_textstringno

scroll

Scroll a running profile's page by `amount` pixels (1 to 20000) in direction up, down, left or right, with mouse-wheel steps at the pointer's position. Like a real wheel, a scrollable area under the pointer scrolls instead of the page — after a click inside such an area, scroll there, or click outside it first. On a macOS profile the distance varies a few percent around `amount`, like a real wheel; the result reports the pixels actually scrolled.

ParameterTypeRequiredDescription
amountintegeryes
directionstringyes
profile_idstringyesUUID of a running profile (launch_profile first)

wait_for_element

Wait until a CSS selector matches an element in a running profile's page (polls every 200 ms; timeout_ms 1 to 30000, default 5000) and return its node_id for click or type. Use after navigate or click to wait for the next page.

ParameterTypeRequiredDescription
profile_idstringyesUUID of a running profile (launch_profile first)
selectorstringyesCSS selector
timeout_msintegerno

evaluate_js

Run a JavaScript expression in a running profile's page and return its value as `text`, JSON-encoded (strings come back quoted); promises are awaited. `undefined` comes back as the text `(undefined)`, not JSON `null`. A thrown error comes back as text starting with [JS Error] — this is a normal call result, not a tool failure. Results over 64 KiB are cut and flagged truncated: true. A synchronous loop is stopped by the PAGE ITSELF after about 9 s; its result also comes back as ordinary [JS Error] text. A promise that never settles instead makes the call itself fail after 10 s with a timeout error. This is NOT a sandbox: it runs with the full rights of the logged-in page (reads cookies, sends requests as the user). A JavaScript alert/confirm/prompt that the action opens is answered at once: `dialog` "dismiss" (default, i.e. Cancel) or "accept" (OK), with `prompt_text` as the typed answer to a prompt; the result says which dialog opened and how it was answered.

ParameterTypeRequiredDescription
dialogstringno
expressionstringyes
profile_idstringyesUUID of a running profile (launch_profile first)
prompt_textstringno

set_file

Attach a file to a file input (, often shown as a "Choose File" button) of a running profile's page by node_id, instead of clicking it - a click opens the system file picker, which cannot be controlled. Only files inside the app's uploads folder (~/.veilus/data/uploads) are allowed: pass the file name, or an absolute path inside that folder; anything else is refused. Ask the user to copy the file there first.

ParameterTypeRequiredDescription
node_idintegeryesnode_id from snapshot ([id=N]) or wait_for_element
pathstringyesfile name inside ~/.veilus/data/uploads
profile_idstringyesUUID of a running profile (launch_profile first)

Script

list_scripts

List every script, newest first, without its source: `scriptId`, `name`, `description`, `version`, `mode` (`raw` or `dsl`), `origin` (`mcp` if an agent wrote it, `app` if the user did), `approved` and `updatedAt`. Check here before save_script so you update an existing script instead of saving a duplicate.

save_script

Save a Playwright (TypeScript) script into Veilus Flow. Without script_id: create a new script; with script_id: replace its source. Contract - violations are rejected with line numbers: (1) connect with chromium.connectOverCDP(`http://127.0.0.1:${process.env.VEILUS\_DEBUG\_PORT}\`); (2) no chromium.launch/launchPersistentContext; (3) use browser.contexts()[0], not newContext(); (4) no setUserAgent/setViewportSize - identity belongs to the profile; (5) errors must exit non-zero: main().catch((e) => { console.error(e); process.exit(1); }); (6) import only playwright and Node built-ins; (7) end main() with `await browser.close()` - it only disconnects from the profile's browser and lets the script exit; without it the run hangs. Read variables via process.env.VEILUS_VAR_<NAME> (names are UPPER_SNAKE_CASE); print what you need to see with console.log to stdout. A newly saved script is not approved: run_script can run it (on at most 3 profiles), schedules wait until the user approves it in the app. `name` is used only when creating; updating (with script_id) does not rename, and only scripts saved by an agent can be updated. Returns `scriptId` (this script's id — pass as script_id to get_script or to save_script again), `version` and `approved`.

ParameterTypeRequiredDescription
descriptionstringno
namestringyes
script_idstringnoUUID of the script whose source to replace; omit to create a new script
sourcestringyesThe full TypeScript source

get_script

Read the source and approval state of a Raw script, to fix it and save_script it again. Returns `scriptId`, `name`, `description`, `version`, `source`, `approved` and `origin`.

ParameterTypeRequiredDescription
script_idstringyes

run_script

Run a script on the given profiles. Returns the run IMMEDIATELY, without waiting for it to finish; its `id` is the run_id for get_run_result.

ParameterTypeRequiredDescription
headlessbooleannoLaunch the run's profiles headless, without a window — the same launch as launch_profile's `headless` (see there): the profile's fingerprint is unchanged. Default false.
profile_idsstring[]yesUUIDs of the profiles to run on
script_idstringyesScript UUID
variablesobjectnoVariables for the script, read in the script as process.env.VEILUS_VAR_<NAME>

compile_script

Compile a drag-and-drop DSL script into runnable code. Does not run it. Agents writing scripts should use save_script instead.

ParameterTypeRequiredDescription
dslobjectyesDSL script body
targetstringnoCompile target

Batch

run_batch

Run an APPROVED script on many profiles for real. Returns the run IMMEDIATELY; its `id` is the run_id for get_run_result. Unlike run_script (a trial on at most 3 profiles, no approval needed), the script must have been approved by the user in Veilus → Flow, otherwise the call is refused. A profile id that does not exist is counted as a failed profile of the run, not rejected (unlike create_schedule, which refuses unknown ids with 404). concurrency (default 3, 1-16, capped by get_capacity's `max`) is how many profiles run at once; stagger_ms (default 1500, 0-600000 i.e. 10 minutes) is the pause between launches; use get_capacity to choose.

ParameterTypeRequiredDescription
concurrencyintegerno
profile_idsstring[]yes
script_idstringyesUUID of an approved script
stagger_msintegerno
variablesobjectnoVariables for this run, read as process.env.VEILUS_VAR_<NAME>

get_capacity

How many browsers are open (active), waiting for a slot (queued) and allowed at once (max) on this machine. Use it to choose run_batch concurrency.

Schedules

create_schedule

Create a schedule that runs an APPROVED script on the given profiles at set times. The script must already be approved by the user in Veilus → Flow: an unapproved script is refused now, not skipped later. schedule_type: interval (interval_minutes, with interval_unit minutes or seconds, at least 5 seconds), daily (daily_hour and daily_minute, the computer's local time), weekly (daily_hour, daily_minute and weekly_day, 0 = Sunday), cron (cron_expr, 5 fields) or once (run_at, a future RFC 3339 time with offset; the schedule runs once and then turns itself off). concurrency (default 1, 1-16) and stagger_ms (default 0, 0-600000 i.e. 10 minutes) apply to every run. Returns the schedule, including nextRunAt.

ParameterTypeRequiredDescription
concurrencyintegerno
cron_exprstringno
daily_hourintegerno
daily_minuteintegerno
enabledbooleanno
headlessbooleannoLaunch the run's profiles headless, without a window — the same launch as launch_profile's `headless` (see there): the profile's fingerprint is unchanged. Default false.
interval_minutesintegerno
interval_unitstringno
namestringyes
profile_idsstring[]yes
run_atstringnoRFC 3339 with offset, e.g. 2026-10-04T09:00:00+07:00; required for once
schedule_typestringyes
script_idstringyesUUID of an approved script
stagger_msintegerno
weekly_dayintegerno

list_schedules

List every schedule: its script, target profiles, timing, enabled flag, and next and last run times.

set_schedule_enabled

Turn a schedule on or off. Turning it on recomputes the next run from now and requires its script to still be approved; turning it off keeps the schedule and its run history.

ParameterTypeRequiredDescription
enabledbooleanyes
schedule_idstringyesSchedule UUID

run_schedule_now

Run a schedule once right now, without waiting for its timer. Returns the run IMMEDIATELY; its `id` is the run_id for get_run_result. Refused while the schedule already has a run in progress, or if its script is not approved.

ParameterTypeRequiredDescription
schedule_idstringyesSchedule UUID

Monitoring

list_runs

List the most recent runs, newest first: status and profile counts, without per-profile detail (get_run_result has that). Without schedule_id it lists runs of every schedule and of run_script and run_batch. limit defaults to 20, at most 100.

ParameterTypeRequiredDescription
limitintegerno
schedule_idstringnoOnly this schedule's runs

get_run_result

Result of a run by `run_id` — use after calling run_script.

ParameterTypeRequiredDescription
run_idstringyesUUID returned by run_script

Datasets

create_dataset

Create a dataset: a table stored in Veilus that approved scripts read per profile. mode `fixed`: each profile gets ONE row for good (accounts); its columns arrive as process.env.VEILUS_VAR_<COLUMN>. mode `consume`: each run takes `rows_per_run` (1-50, default 1) unused rows; a failed run gives them back, a successful run marks them used; they arrive as VEILUS_VAR_ROWS (JSON array of {COLUMN: value}) and, when rows_per_run is 1, also one variable per column. VEILUS_VAR_ROW_INDEX is the row's number. Columns are UPPER_SNAKE_CASE; ROWS, ROW_INDEX, PROFILE_ID, RUN_ID and DEBUG_PORT are reserved. Mark a column `secret: true` for passwords: it is stored and given to approved scripts, but no tool ever returns it. Bad rows are reported in `rejected` by row number (from 1). Returns `datasetId`.

ParameterTypeRequiredDescription
columnsobject[]yes
modestringyes
namestringyes
rowsobject[]noOne object per row, keyed by column name; every value must be a string — convert numbers and dates to text before sending
rows_per_runintegerno

append_dataset_rows

Add rows to a dataset, in the same shape as create_dataset's `rows`. Bad rows are reported in `rejected` by row number and do not block good ones.

ParameterTypeRequiredDescription
dataset_idstringyesDataset UUID
rowsobject[]yesOne object per row, keyed by column name; every value must be a string — convert numbers and dates to text before sending

list_datasets

List datasets: id, name, mode, columns with their secret flag, rowsPerRun, row counts (total and assigned for fixed; available, reserved and used for consume) and how many profiles use each. Never returns row values.

get_dataset_rows

Read a dataset's rows in order: `limit` rows (default 100, at most 500) from `offset`. Each row has its index, its values, and for a fixed dataset the profile it is assigned to, for a consume dataset its state. Secret columns are always left out of `values`.

ParameterTypeRequiredDescription
dataset_idstringyesDataset UUID
limitintegerno
offsetintegerno

assign_dataset

Assign a dataset to existing profiles. A fixed dataset goes to each profile's Identity slot and gives it one unassigned row (lowest index first); profiles left without a row are named in `withoutRow` and get nothing — never share one account between profiles. A consume dataset goes to the Content slot. Refuses the whole call if a profile already has a different dataset in that slot, unless force: true; refuses column names that also exist in the dataset in the profile's other slot. Every later run of an APPROVED script on those profiles carries the data; schedules and run_batch need no new field.

ParameterTypeRequiredDescription
dataset_idstringyesDataset UUID
forcebooleannoReplace a dataset already in the slot. Default false
profile_idsstring[]yes

unassign_dataset

Remove a dataset from profiles. For a fixed dataset their rows become unassigned again. Profile ids that no longer exist are accepted, so rows held by deleted profiles can be freed.

ParameterTypeRequiredDescription
dataset_idstringyesDataset UUID
profile_idsstring[]yes

reset_dataset_rows

For a consume dataset: put every used row back to available, so runs take them again. Returns how many rows changed.

ParameterTypeRequiredDescription
dataset_idstringyesDataset UUID