# Mosaic MCP tools reference

> Every tool Mosaic’s MCP server gives an AI assistant, 168 in all, with its arguments: profiles, proxies, cookies, warm-up and driving a live page.

Source: https://mosaicbrowser.com/docs/mcp

**Docs** MCP server

## The MCP server, every tool

Claude, or any client that speaks the Model Context Protocol, gets the same reach as the Mosaic window: it can make profiles, start them, and read and click the page in front of them. This list is read from the server’s own tool table at every build.

## Connecting an assistant

1. In Mosaic, open Settings › MCP and turn the server on. It is off until you do, because its tools start browsers that hold logged-in sessions.
2. Make an API key on the API screen. The MCP server takes the same keys as the REST API.
3. A client that speaks HTTP posts JSON-RPC to `http://127.0.0.1:39218/mcp` with `Authorization: Bearer <key>`. A client that only launches a local command, such as Claude Desktop, runs the stdio bridge Mosaic writes, with the key in `MOSAIC_API_KEY`; Settings › MCP shows the config to paste.

The server listens on `127.0.0.1` only, so the assistant runs on the same computer as Mosaic. It is part of Pro, like the [REST API](https://mosaicbrowser.com/docs/api).

The page tools read a page from an isolated world and never enable the DevTools `Runtime` domain, which we measured turning a detector’s verdict to “bot”. Clicks and keys go through the same human-input code the warm-up robot uses.

- [Driving a page 29](#pages)
- [Tabs and windows 12](#tabs)
- [Fingerprints 8](#fingerprints)
- [Warm-up and checks 12](#checks)
- [Trash and backups 8](#trash)
- [Cookies, logins and bookmarks 19](#data)
- [Presets and extensions 9](#presets)
- [Proxies and providers 26](#proxies)
- [Profiles and projects 26](#profiles)
- [The app 19](#app)

## Driving a page 29

Read a running profile’s page as a tree of refs and act on it: clicks, typing with human timing, waits, the network and the console. None of it enables CDP Runtime.

### [`type_text`](#type_text)

Type into whatever is focused in a running profile, as a person would — per-key delays, occasional typos that are then corrected, and real key events rather than a paste. It matters because a profile that browses like a person for a month and then fills a login in one `input` event has told the site something no fingerprint work takes back. For a saved password or a 2FA code use `fill_credential`, which never returns the secret. Click the field first with `click_element`: typing goes to whatever the page has focused, and a click that lands on a wrapper leaves it going to `document.body`.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id; it has to be running |
| `text`required | `string` | What to type |
| `charactersPerMinute` | `number` | Mean speed. 200–400 is ordinary, 500+ is a fast typist. |
| `typoRate` | `number` | Chance per character of a wrong key that is then corrected, 0–0.1 |
| `thinkingPauses` | `boolean` | Longer pauses now and then, as if reading |

### [`page_snapshot`](#page_snapshot)

The page as a tree of roles, names and states, with a `ref` on everything you can act on. Start here: it is a few hundred lines where the HTML is tens of thousands, and every other page tool takes the refs it mints. Refs belong to the snapshot that made them — navigate or re-snapshot and the old ones are refused by name rather than silently hitting the wrong element.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `tabId` | `string` | Tab id from list_tabs; omit for the tab with content on it |
| `maxNodes` | `number` | Cap on nodes, 50-5000 (default 1500) |
| `everything` | `boolean` | Include layout wrappers with no role of their own (default false) |

### [`page_find`](#page_find)

Find elements by CSS selector, visible text, ARIA role, accessible name, or any combination, and get a ref for each. Text matching returns the innermost match, so searching for "Sign in" gives you the button rather than the <body> that also contains it.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `selector` | `string` | CSS selector to search within |
| `text` | `string` | Visible text the element contains |
| `role` | `string` | ARIA role, e.g. button, link, textbox |
| `name` | `string` | Accessible name |
| `exact` | `boolean` | Require the name to match exactly rather than contain |
| `limit` | `number` | Maximum matches, 1-200 (default 20) |
| `tabId` | `string` | Tab id; omit for the tab with content on it |

### [`page_dom`](#page_dom)

The page’s actual HTML, whole or one subtree by ref or selector. Use it when you need the markup itself — an attribute, a data- value, a hidden input. For deciding what to click, `page_snapshot` is smaller and more useful.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `ref` | `string` | Ref from a snapshot; the subtree under it |
| `selector` | `string` | CSS selector; the subtree under the first match |
| `maxChars` | `number` | Cap on characters returned (default 40000) |
| `tabId` | `string` | Tab id; omit for the tab with content on it |

### [`page_screenshot`](#page_screenshot)

A PNG of the viewport, the full page, or one element by ref or selector. Returned base64.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `ref` | `string` | Ref of an element to photograph on its own |
| `selector` | `string` | CSS selector of an element to photograph on its own |
| `fullPage` | `boolean` | Capture the whole page rather than the viewport |
| `tabId` | `string` | Tab id; omit for the tab with content on it |

### [`page_click`](#page_click)

Click an element by ref, CSS selector or visible text. Scrolls to it with a real wheel first, then moves the pointer along a human trajectory and presses — not `element.click()`, which dispatches an untrusted event with no preceding move and is what behavioural detection separates from a person. Reports `forced: true` on the rare element only a programmatic scroll could reach.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `ref` | `string` | Ref from page_snapshot or page_find |
| `selector` | `string` | CSS selector |
| `text` | `string` | Visible text; refuses if more than one element matches |
| `first` | `boolean` | With text, act on the first match instead of refusing |
| `tabId` | `string` | Tab id; omit for the tab with content on it |

### [`page_hover`](#page_hover)

Move the pointer onto an element and leave it there, for menus and tooltips that open on hover.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `ref` | `string` | Ref from page_snapshot or page_find |
| `selector` | `string` | CSS selector |
| `text` | `string` | Visible text |
| `first` | `boolean` | With text, act on the first match |
| `tabId` | `string` | Tab id; omit for the tab with content on it |

### [`page_type`](#page_type)

Type text as a person types it: real keydown, keypress, input and keyup per character, human inter-key timing, occasional typos corrected with a backspace. With a ref or selector it clicks the field first; with neither it types into whatever already has focus. Never a paste — a field that goes from empty to forty characters in one event fires no keydown at all, which is among the cheapest bot signals a site can collect.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `text`required | `string` | The characters to type |
| `ref` | `string` | Ref of the field |
| `selector` | `string` | CSS selector of the field |
| `clear` | `boolean` | Select the existing value first, so this replaces it |
| `tabId` | `string` | Tab id; omit for the tab with content on it |

### [`page_fill`](#page_fill)

Clear a field and type a value into it. The same human typing as `page_type`; the clear uses the select-all shortcut for the platform the page believes it is on, so a profile presenting as Windows sends Control+A rather than contradicting its own fingerprint.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `text`required | `string` | The value to put in the field |
| `ref` | `string` | Ref of the field |
| `selector` | `string` | CSS selector of the field |
| `tabId` | `string` | Tab id; omit for the tab with content on it |

### [`page_press`](#page_press)

Press one named key, optionally with modifiers held. Keys: …. Modifiers: …. For ordinary characters use page_type.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `key`required | `string` | Key name, e.g. Enter, Tab, Escape, ArrowDown |
| `modifiers` | `string[]` | Alt, Control, Meta, Shift |
| `tabId` | `string` | Tab id; omit for the tab with content on it |

### [`page_select`](#page_select)

Choose one or more options in a native <select>, by value or by visible label. A native dropdown is drawn by the operating system and is not in the page, so there is nothing to click; this sets the selection and fires the input and change events a browser would. For a custom dropdown, click it and then click the option.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `values`required | `string[]` | Option values or labels to select |
| `ref` | `string` | Ref of the select |
| `selector` | `string` | CSS selector of the select |
| `tabId` | `string` | Tab id; omit for the tab with content on it |

### [`page_scroll`](#page_scroll)

Scroll the page with a real wheel, or scroll an element into view. The wheel deltas match the pointing device this profile was assigned — a trackpad and a notched mouse produce different event streams, and mixing them inside one session describes two different computers.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `amount` | `number` | Pixels to scroll; positive is down (default 400) |
| `ref` | `string` | Ref of an element to bring into view instead |
| `selector` | `string` | CSS selector of an element to bring into view instead |
| `tabId` | `string` | Tab id; omit for the tab with content on it |

### [`page_drag`](#page_drag)

Drag from one element to another, as a press, a human-shaped move and a release.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `from`required | `object` | Where to start: { ref } or { selector } or { text } |
| `to`required | `object` | Where to drop: { ref } or { selector } or { text } |
| `tabId` | `string` | Tab id; omit for the tab with content on it |

### [`page_upload`](#page_upload)

Attach files to a file input, by absolute path on this machine. Point it at the <input type="file"> itself, which is often hidden behind a styled button — find it with a selector rather than by clicking the button.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `paths`required | `string[]` | Absolute file paths on this machine |
| `ref` | `string` | Ref of the file input |
| `selector` | `string` | CSS selector of the file input |
| `tabId` | `string` | Tab id; omit for the tab with content on it |

### [`page_navigate`](#page_navigate)

Navigate the controlled tab and wait for it to load. Returns the URL it ended on, which is not always the one you asked for.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `url`required | `string` | Absolute URL |
| `timeoutMs` | `number` | How long to wait for load, default 30000 |
| `tabId` | `string` | Tab id; omit for the tab with content on it |

### [`page_reload`](#page_reload)

Reload the controlled tab and wait for it to load.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `tabId` | `string` | Tab id; omit for the tab with content on it |

### [`page_history`](#page_history)

Go back or forward through the tab’s history. Uses the tab’s real history rather than `history.go()`, so it crosses origin boundaries the way the back button does.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `delta`required | `number` | -1 for back, 1 for forward, and so on |
| `tabId` | `string` | Tab id; omit for the tab with content on it |

### [`page_wait`](#page_wait)

Wait for a selector to appear or disappear, for text to show up, or for the network to go quiet. Returns `met: false` on a timeout rather than failing, because "it never appeared" is often the answer you wanted — `why` says which it was. Network idle needs page_network_start running first, since without it there are no events to be idle between.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `selector` | `string` | CSS selector to wait for |
| `text` | `string` | Visible text to wait for |
| `gone` | `boolean` | Wait for it to disappear instead of appear |
| `networkIdleMs` | `number` | Wait until no request for this many ms |
| `timeoutMs` | `number` | Give up after this long, default 10000 |
| `tabId` | `string` | Tab id; omit for the tab with content on it |

### [`page_evaluate`](#page_evaluate)

Run JavaScript against the page. `world: "isolated"` is the default and the page cannot see it — that is also where refs resolve, so pass a `ref` and the expression receives it as `element`. `world: "page"` runs in the page’s own realm, which is the only way to reach a framework’s globals and is the one mode the page could notice.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `expression`required | `string` | A JavaScript expression; an await is fine, the result is awaited |
| `world` | `string` | "isolated" (default, invisible) or "page" |
| `ref` | `string` | A ref to hand the expression as `element`; isolated world only |
| `timeoutMs` | `number` | Give up after this long, default 20000 |
| `tabId` | `string` | Tab id; omit for the tab with content on it |

### [`page_console`](#page_console)

Read what the page has logged, from two sources with two different costs. Free and invisible, always on: the browser’s own entries — failed requests, CSP violations, deprecations — which arrive with a `browser:<source>` kind. Off by default because the page could detect it: the page’s own console.log calls AND its uncaught errors and unhandled rejections, which need page_console_mode. That split is measured: an uncaught exception is reported only to the world it was thrown in, so nothing outside the page’s own world can see it without a CDP domain that makes the profile detectable. `note` always says which sources you are getting.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `limit` | `number` | Return at most this many, newest last |
| `kinds` | `string[]` | Filter: uncaught, rejection, log, info, warn, error, debug, trace |
| `clear` | `boolean` | Empty the buffer after reading it |
| `tabId` | `string` | Tab id; omit for the tab with content on it |

### [`page_console_mode`](#page_console_mode)

Turn on capture of everything in the page’s own world: its console.log calls, its uncaught errors and its unhandled rejections. This is the ONE page-control feature a page could detect — seeing a console.log call means being the function that gets called, so console is wrapped in the page’s own world and the error listeners live there too. Off by default. There is no invisible alternative for these three, and that is measured rather than assumed. Turning it off restores the original console exactly. Turn it off when you are done.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `mode`required | `string` | "off" or "page" |
| `tabId` | `string` | Tab id; omit for the tab with content on it |

### [`page_network_start`](#page_network_start)

Start recording every request this tab makes, and keep recording across navigations until you stop it. Unlike watch_network, which records for a fixed window and returns, this one runs in the background so you can act and then read. Requests from before you started are not in it — reload if you need the ones that built the page.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `tabId` | `string` | Tab id; omit for the tab with content on it |

### [`page_network`](#page_network)

Read the recorded requests with their method, type, status and failure reason. Reports `total`, how many were `dropped` when the buffer filled, and `stoppedBecause` when the log simply ends — a truncated list that reports itself as complete is worse than no list.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `limit` | `number` | Return at most this many, newest last |
| `filter` | `string` | Substring of the URL, or an exact resource type or method |
| `clear` | `boolean` | Empty the log after reading it |
| `tabId` | `string` | Tab id; omit for the tab with content on it |

### [`page_network_stop`](#page_network_stop)

Stop recording this tab’s requests. What has already been recorded stays readable.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `tabId` | `string` | Tab id; omit for the tab with content on it |

### [`page_network_body`](#page_network_body)

The response body of one recorded request, by URL. Fetched on demand rather than buffered, so recording a busy page does not hold every response in memory. Needs the recording still running.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `url`required | `string` | The exact URL from page_network |
| `tabId` | `string` | Tab id; omit for the tab with content on it |

### [`page_dialogs`](#page_dialogs)

Read what alert, confirm and prompt have asked, and set how they are answered. Driving a page makes this necessary: Chromium dismisses a dialog itself when nothing is attached, but once page control attaches, the page blocks until we answer. The default is to dismiss and record, so attaching never hangs a page. Call with no policy to just read the log.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `policy` | `string` | "dismiss" (default) or "accept" |
| `promptText` | `string` | What to type into a prompt when accepting |
| `tabId` | `string` | Tab id; omit for the tab with content on it |

### [`page_attachments`](#page_attachments)

Every tab page control currently holds open, with whether it is recording and how long it has been idle. Attachments are made on first use and dropped when the profile stops or after ten idle minutes; this exists so one that outlives its usefulness is visible rather than merely forgotten.

No arguments.

### [`page_detach`](#page_detach)

Release the page-control attachment for a tab, or for every tab of a profile. Rarely needed — they expire on their own and a stopped profile drops all of its own — but it is here for when you want the page left with nothing attached to it. Refs from previous snapshots die with it.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `tabId` | `string` | Tab id; omit to release every tab of this profile |

### [`typing_defaults`](#typing_defaults)

The default timings human typing uses — the delays, the error rate, the correction behaviour. Read them to know what you are overriding before passing `options` to `type_text`.

No arguments.

## Tabs and windows 12

The tabs a running profile already has open, read and steered as they are, and its windows arranged on the screen.

### [`list_tabs`](#list_tabs)

List the tabs a running profile has open, with their ids, titles and URLs. Every other browser tool takes a `tabId` from here; omit it and they act on the first tab, which is what you want when the profile has one.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |

### [`open_tab`](#open_tab)

Open a URL in a new tab of a running profile and return the new tab. The browser navigates it itself, so this returns as soon as the tab exists rather than when the page has loaded — read it with `read_page`, which waits.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `url`required | `string` | Absolute URL to open |

### [`close_tab`](#close_tab)

Close one tab of a running profile. Closing the last tab does not stop the profile; use `stop_profile` for that.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `tabId`required | `string` | Tab id from list_tabs |

### [`navigate_tab`](#navigate_tab)

Point an existing tab at a URL and wait for its load event. Use this rather than `open_tab` when the profile is already somewhere and you want to keep the tab — a site that set a cookie on this tab keeps it either way, but a new tab is a new entry in the session history and some flows notice.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `url`required | `string` | Absolute URL |
| `tabId` | `string` | Tab id; omit for the first tab |

### [`read_page`](#read_page)

Read a tab: its URL, title, visible text and first hundred links. This is `innerText`, which is what a person sees — not `textContent`, which would include the contents of every `<script>` on the page and, on a detection site, the collector’s own source. Pass `html` for the markup as well when the text is not enough.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `tabId` | `string` | Tab id; omit for the first tab |
| `html` | `boolean` | Also return the page markup |
| `maxChars` | `number` | Cap on the text and markup returned (default 20000) |

### [`screenshot_tab`](#screenshot_tab)

A PNG of the tab, base64-encoded. For when the answer is visual — a CAPTCHA, a layout, a detection site whose verdict is a coloured card rather than text.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `tabId` | `string` | Tab id; omit for the first tab |
| `fullPage` | `boolean` | Capture past the viewport rather than only what is visible |

### [`run_script`](#run_script)

Evaluate a JavaScript expression in the tab and return its value. Runs in the page’s own world, so it sees the page’s variables and the fingerprint exactly as the page does — which is what makes it useful for checking what a site is reading. The value is returned by value, so return something JSON can carry.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `expression`required | `string` | A JavaScript expression. An await is fine; the result is awaited. |
| `tabId` | `string` | Tab id; omit for the first tab |

### [`click_element`](#click_element)

Click the element a CSS selector names, as a real mouse event at its centre. Deliberately not `element.click()`: that dispatches an untrusted event with no preceding move and `isTrusted === false`, which is precisely what a behavioural detector separates from a person.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `selector`required | `string` | CSS selector for the element to click |
| `tabId` | `string` | Tab id; omit for the first tab |

### [`watch_network`](#watch_network)

Record every request a tab makes for a number of seconds and return them with their status, type and size. Answers the questions a screenshot cannot: which third party a page is really calling, whether a request went out at all, what a detector posted its verdict to. The recording domain is enabled only for the window you ask for and turned off again.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `seconds` | `number` | How long to record, 1-60 (default 8) |
| `tabId` | `string` | Tab id; omit for the first tab |

### [`read_browser_storage`](#read_browser_storage)

Read what the site has stored in this profile: its cookies, localStorage and sessionStorage. The cookie jar is the profile’s real identity — more of what re-identifies a returning visitor lives here than in any fingerprint field.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `tabId` | `string` | Tab id; omit for the first tab |
| `urls` | `string[]` | Restrict cookies to these URLs |

### [`write_browser_storage`](#write_browser_storage)

Set or delete localStorage / sessionStorage keys for the tab’s origin. A value of `null` removes the key. Storage is same-origin, so this writes to whatever the tab is currently on — navigate first if that is not where you mean.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `which`required | `"local" \| "session"` | Which store to write |
| `entries`required | `object` | Key to value; null deletes the key |
| `tabId` | `string` | Tab id; omit for the first tab |

### [`arrange_windows`](#arrange_windows)

Lay the running browsers out on screen: `grid`, `cascade` or `stack`. For watching a batch work at once. It moves windows and nothing else — no profile is started or stopped.

| Argument | Type | What it is |
| --- | --- | --- |
| `ids` | `string[]` | Running profile ids; omit for all of them |
| `layout` | `"grid" \| "cascade" \| "stack"` | Default grid |

## Fingerprints 8

Draw, check and repair the machine a profile claims, and match it to its exit.

### [`match_profile_to_exit`](#match_profile_to_exit)

Resolve where a profile actually comes out — through its proxy, or the direct connection when it has none — and bring the fingerprint into line with it. Timezone and geolocation are moved whenever they disagree, because neither is visible to a site that already knows the profile. The language is moved only for a profile that has never run: Accept-Language rides every request, so changing it under a session with history tells that site something true about itself, and the mismatch is reported in `skipped` instead. Pass `language: "move"` to move it anyway — correct when the profile is not signed in anywhere, and a change the sites it is signed into would see when it is. Use after attaching a proxy to profiles that already exist.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `language` | `"auto" \| "move"` | What to do with Accept-Language on a profile that has already run. "auto" (the default) leaves it and reports the mismatch in `skipped`. "move" changes it to the exit country’s language — visible to every site the profile is already signed in to, and the only way to fix a profile drawn for one country and now browsing out of another. |

### [`preview_fingerprint`](#preview_fingerprint)

Generate a fingerprint without saving it, to inspect what a given set of options produces. The hardware half comes out of one catalogue row — the GPU and its renderer string, the thread counts that model ships with, the displays it actually has — so the parts always belong to a machine somebody can buy. Pass `deviceId` from `list_devices` to pin the machine and let the locale, the seed and the rest vary.

| Argument | Type | What it is |
| --- | --- | --- |
| `seed` | `string` | Reuse a seed to reproduce an exact identity |
| `deviceId` | `string` | A machine from list_devices. A phone id implies android, so it is enough on its own. An unknown id falls through to a normal draw rather than failing. |
| `os` | `string` |  |
| `brand` | `string` | Chrome, and there is one. `Edge` is refused: the engine builds its User-Agent suffix and its client-hint version lists from the same switch, so it can produce a correct Edge User-Agent or a correct Sec-CH-UA-Bitness, never both. Omit it. |
| `countryCode` | `string` | ISO-3166 alpha-2 |
| `timezone` | `string` | IANA zone, e.g. Europe/Vienna. Normally the proxy exit’s, and omitting it draws one that agrees with the country. |
| `languages` | `array` | Accept-Language list, most preferred first. Drawn from the locale when omitted. |
| `latitude` | `number` | The exit’s own latitude, when it is known. Jittered before use. |
| `longitude` | `number` | The exit’s own longitude, when it is known. Jittered before use. |

### [`audit_fingerprint`](#audit_fingerprint)

Ask whether an identity contradicts itself or the computer it will run on. Two separate answers. `problems` is what is impossible on any machine — a Metal renderer on Windows, a browser version that disagrees with its own User-Agent — and is always a bug. `warnings` is what is possible somewhere but not here: a platform this engine cannot back below JavaScript, a GPU vendor that contradicts what navigator.gpu reports from a worker realm, a chip different from the one in this machine, or a thread count or display the claimed model never shipped with. The generator never produces either, so anything reported came from editing by hand. Neither blocks anything; each names what was measured so the trade can be judged.

| Argument | Type | What it is |
| --- | --- | --- |
| `profileId` | `string` | Audit a stored profile. Omit to audit a freshly generated one instead. |
| `os` | `string` | … Only used when generating a fingerprint to audit. |
| `countryCode` | `string` | ISO-3166 alpha-2 — only used when generating one to audit |

### [`list_devices`](#list_devices)

The device catalogue a profile can claim. Each entry is one machine somebody can buy — its GPU as the driver actually names it, the `hardwareConcurrency` values that model ships with, the memory it comes in, and the displays it has. Every renderer string here has been observed in the wild rather than composed by hand; a hand-written one is missing things like the PCI device id real Chrome reports, which is a tell rather than a disguise. Pass an `id` back to `preview_fingerprint` or the create call to draw that exact machine.

`backed` says whether *this* computer can stand behind the claim, and it turns on things a page reads directly: the OS, because fonts and their metrics come from the real machine; the GPU vendor, because navigator.gpu reports the real adapter from a worker realm; and on a machine that names its own chip, the chip itself, which was measured rather than reasoned about. The thread count is deliberately not one of them — the browser applies that override itself, so it holds in every realm. Machines that fail `backed` are listed rather than hidden, because a profile may be destined for a different computer; choosing one raises a warning from audit_fingerprint rather than an error.

The `android` rows are captured phones rather than catalogue entries assembled from tables, and `backed` asks a different question of them: one flag, whether this engine moves the whole mobile record below JavaScript, rather than the host OS, GPU vendor and chip a desktop claim turns on.

| Argument | Type | What it is |
| --- | --- | --- |
| `os` | `string` | windows, macos, linux or android. Omit for all. |
| `backedOnly` | `boolean` | Only devices this computer can back. |

### [`repair_hardware`](#repair_hardware)

Make a profile’s claimed machine constructible again. Two kinds of wrong, repaired differently: a machine nobody sells — an M3 Max beside a 13-inch Air panel, a thread count that model never shipped — is patched in place from the catalogue row that carries its chip, and a machine *this* computer cannot back is redrawn from the profile’s own seed with its locale, timezone and every noise setting held. The seed is the point: the canvas and audio hashes survive, so the profile keeps everything a site re-identifies it by and only the hardware underneath moves. Idempotent, and refuses a running profile.

| Argument | Type | What it is |
| --- | --- | --- |
| `ids`required | `string[]` | Profiles to repair |

### [`regenerate_fingerprint`](#regenerate_fingerprint)

Redraw a stored profile’s identity. Pass nothing and it draws a whole new machine from a new seed — which is a different profile wearing the same name, so every site that knew it will see a stranger. Pass `deviceId` to move it onto one exact machine from list_devices while keeping the seed, which is the safe version: the canvas and audio noise, the fonts and everything a site re-identifies it by survive and only the hardware moves.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `deviceId` | `string` | A machine from list_devices; keeps the seed |
| `os` | `string` |  |
| `seed` | `string` | Reuse an exact seed. The same seed rebuilds the identical machine on any computer. |

### [`validate_fingerprint`](#validate_fingerprint)

Audit a profile’s stored identity for contradictions — a GPU that does not belong to the platform, a timezone that disagrees with the language, hardware no real machine ships. Reads what is stored rather than drawing anything, so it is the call for "is this profile still coherent" after an edit.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |

### [`new_seed`](#new_seed)

A fresh identity seed. Everything about a drawn machine follows from one of these, so passing the same seed twice to `preview_fingerprint` gives the same machine — which is how a profile is reproducible without being shared.

No arguments.

## Warm-up and checks 12

Warm a profile up before it meets the site that matters, and run it past the leak test and the public detectors.

### [`run_leak_test`](#run_leak_test)

Launch a profile and verify from inside the page that the exit IP, WebRTC, timezone and geolocation match what it claims. Reports any leak.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |

### [`list_warmup_plans`](#list_warmup_plans)

List saved warm-up plans. A plan is a named list of sites plus how to behave on them — dwell range, scrolling, how many same-origin links to follow — and is the thing to pass to `run_warmup` when warming more than one profile. Includes every option, so a plan can be read before it is run.

No arguments.

### [`save_warmup_plan`](#save_warmup_plan)

Create or replace a warm-up plan. The options are checked now rather than at run time: a dwell range the wrong way round, a site that is not a site or a `siteCount` of zero is refused here rather than discovered forty profiles into a batch nobody is watching. Omit `options` fields to keep what the plan already had.

| Argument | Type | What it is |
| --- | --- | --- |
| `id` | `string` | Existing plan to replace; omit to create |
| `name` | `string` | What the plan is called |
| `description` | `string` | What it is for, in your own words |
| `options` | `object` | The run settings: preset, sites, siteCount, minSecondsPerSite, maxSecondsPerSite, scroll, followLinks, maxLinksPerSite, acceptCookieBanners, watch, headless. `sites` replaces the preset entirely. |

### [`delete_warmup_plan`](#delete_warmup_plan)

Delete a warm-up plan. A delete that removed nothing is a 404, never a cheerful success.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Plan id |

### [`run_warmup`](#run_warmup)

Warm several profiles with one plan, one after another. Sequential rather than parallel on purpose: each run launches that profile's own browser, so a fan-out would race the concurrency cap and silently exceed it. Long-running — a plan of eight sites at 12-35s each is roughly three minutes per profile. Every profile gets a row whether or not it worked; a dead proxy on one does not stop the rest. Pass `planId`, or the options inline, but not both.

| Argument | Type | What it is |
| --- | --- | --- |
| `profileIds`required | `string[]` | Which profiles to warm |
| `planId` | `string` | A saved plan from list_warmup_plans |
| `options` | `object` | Inline options instead of a plan. See save_warmup_plan. |

### [`cancel_warmup`](#cancel_warmup)

Stop the profile that is warming up and every one still queued behind it in a batch.

No arguments.

### [`list_detection_tests`](#list_detection_tests)

List the catalogue of third-party sites that test a browser fingerprint: consistency scanners built to catch anti-detect browsers, bot and headless detectors, uniqueness and entropy measures, network leak tests, and live commercial bot gates. Each entry says what it measures and what a pass looks like.

| Argument | Type | What it is |
| --- | --- | --- |
| `category` | `string` | consistency, automation, uniqueness, leak or waf. Omit for all. |

### [`open_detection_test`](#open_detection_test)

Open one of the detection test sites in a new tab of a profile, starting the profile if it is not running. Returns no verdict — read the page yourself, because scraping another site's results panel is a heuristic that silently goes wrong when they redesign it.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `testId`required | `string` | A test id from list_detection_tests, e.g. creepjs |

### [`library_health`](#library_health)

One pass over the library: what is wrong with it, with the profile ids and names behind every count so you can act without going looking. The findings worth acting on are a profile whose timezone contradicts its proxy’s measured exit — the first comparison every consistency scanner makes, and it drifts on its own whenever a pool re-homes a subnet — a profile whose `Accept-Language` contradicts it, which rides every single request, a profile describing a machine nobody sells, and one claiming something this computer cannot back. `no-proxy` and `never-run` are `info` because neither is a fault. Scope it with `projectId` and `os`, or omit both for the whole library. Auditing reads every profile off disk, so call it when you need it rather than on a timer.

| Argument | Type | What it is |
| --- | --- | --- |
| `projectId` | `string` | Only this project. Omit for every project. |
| `os` | `string[]` | Only these platforms: windows, macos, linux, android. |

### [`list_warmup_presets`](#list_warmup_presets)

The browsing plans the cookie robot can run, and the defaults it runs them with. A brand-new profile with an empty cookie jar is itself a signal; a warm-up gives it a history of ordinary sites before it is used for anything.

No arguments.

### [`warm_up_profile`](#warm_up_profile)

Browse real sites in a profile so its cookie jar looks lived-in. It runs in the same headed browser the profile uses, never headless — a headless Chrome announces `HeadlessChrome` in its own User-Agent and drops the real GPU, so warming up there writes a bot’s history into the jar and calls it a reputation. Scrolling, dwell time and clicks go through the same human input the rest of the product uses. Long-running; `list_warmup_presets` reports which profiles are mid-run.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `preset` | `string` | A preset id from list_warmup_presets: general, social, shopping or tech |
| `sites` | `string[]` | Browse these instead of a preset’s list; anything here replaces the preset entirely |
| `siteCount` | `number` | How many sites to visit from the pool, sampled at random |
| `minSecondsPerSite` | `number` | Shortest dwell on a page |
| `maxSecondsPerSite` | `number` | Longest dwell on a page |
| `followLinks` | `boolean` | Click through to a few pages within each site |
| `acceptCookieBanners` | `boolean` | Click through consent banners, which gate most of the cookies worth having |

### [`list_warmup_runs`](#list_warmup_runs)

Which profiles are warming up right now. A warm-up run holds a browser, so this is what to read before starting one on a profile that may already be busy.

No arguments.

## Trash and backups 8

What was deleted, what can come back, and the library backups.

### [`list_trash`](#list_trash)

What is in the trash and how long each entry has left. Deleting a profile or a project moves it here rather than destroying it; `expiresInDays` counts down to the automatic purge, and `kind` says whether restoring it brings back one profile or a project and everything that went with it.

No arguments.

### [`restore_from_trash`](#restore_from_trash)

Bring a deleted profile or project back, with its cookies, logins and browser data untouched. The `kind` has to match what `list_trash` reported: restoring a project also restores every profile that was deleted along with it.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile or project id, from list_trash |
| `kind`required | `string` | "profile" or "project" |

### [`empty_trash`](#empty_trash)

Destroy what is in the trash. This is the irreversible one: every profile it removes takes its cookies, logins and history with it, and nothing here can bring them back.

**It refuses to guess how much you meant.** Calling it with no arguments is an error, not a full purge — name a `projectId`, name an `os` list, or pass `all: true` to mean every row. The old default cost 586 profiles to a script that meant "tidy up after myself". A platform filter deliberately spares trashed *projects* — a project is not in a platform, and purging one because "android" was named would take desktop profiles nobody asked about. Call `list_trash` first if you are about to run this on somebody’s behalf.

| Argument | Type | What it is |
| --- | --- | --- |
| `projectId` | `string` | Only this project’s trash |
| `os` | `string[]` | Only these platforms: windows, macos, linux, android. Spares trashed projects. |
| `all` | `boolean` | Every row, with nothing spared. Cannot be combined with the other two. |

### [`list_backups`](#list_backups)

Snapshots of the store, newest first — the undo for `empty_trash` above. One is taken daily and one immediately before anything irreversible, so there is almost always something to go back to.

A snapshot holds the library’s **metadata**: every profile’s fingerprint, proxy binding, locale, notes and tags, plus the projects, proxies and settings. It does **not** hold `profile-data/` — the browser directories are 570 times the size and mostly cache. So a restore brings back the identity and not the session: the profile returns with its fingerprint and its proxy, and logged out. Say that to the user rather than letting them assume cookies are coming back.

No arguments.

### [`create_backup`](#create_backup)

Take a snapshot now. Worth doing before a bulk edit, an import or a mass delete — the operations that change many profiles at once and have no undo of their own. Costs about twelve kilobytes per profile, so there is no reason to weigh it.

No arguments.

### [`preview_restore`](#preview_restore)

What restoring a snapshot would do, without doing it. Returns `missing` — profiles the snapshot has and the library no longer does, which is what recovery means — and `present`, which exist in both.

Read that split before restoring anything. Bringing a missing profile back is recovery; restoring a present one **writes over** every change made to it since the snapshot, which is a second destructive act hiding inside the undo for the first.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The snapshot id from list_backups |

### [`restore_backup`](#restore_backup)

Restore profiles from a snapshot. **Additive by default**: profiles that still exist are skipped unless `overwrite` is true, because the ones that were not deleted have been edited since the snapshot was taken and replacing them loses that work. `ids` narrows it to specific profiles.

A snapshot of the current state is taken first, so the restore is itself undoable. Restored profiles come back live rather than in the trash, and one whose project no longer exists lands in the active project. Tell the user that cookies and browsing data are not included — a restored profile is logged out.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The snapshot id from list_backups |
| `ids` | `string[]` | Only these profile ids. Omit for every profile the snapshot has and the library does not. |
| `overwrite` | `boolean` | Also replace profiles that still exist, discarding changes made since the snapshot. Default false. |

### [`purge_from_trash`](#purge_from_trash)

Destroy one trashed item permanently, by id and by kind. This is irreversible: a purged profile takes its cookies, logins, history and downloaded files with it. **One id, never a list** — the unscoped version of this cost 586 real profiles, and a loop built from what a listing returned is how that happened. Call `list_trash` first and name what you meant.

| Argument | Type | What it is |
| --- | --- | --- |
| `kind`required | `"profile" \| "project" \| "warmupPlan"` | What the id names |
| `id`required | `string` | The trashed item’s id |

## Cookies, logins and bookmarks 19

Everything one profile carries: cookies, saved logins and 2FA codes, bookmarks, history and downloads.

### [`list_credentials`](#list_credentials)

List the logins saved on a profile. Returns labels, sites and usernames only — never a password or a two-factor secret, which stay encrypted in the OS keychain and leave the app only as keystrokes into the browser.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |

### [`add_credential`](#add_credential)

Save a login on a profile. The password and any two-factor secret are encrypted through the OS keychain before they are written. A two-factor secret may be given as base32 or as a whole otpauth:// URI.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `label` | `string` | What this login is for |
| `site` | `string` | The site it belongs to |
| `username` | `string` | Username or email |
| `password` | `string` | Password, in plain text — it is encrypted before storage |
| `totpSecret` | `string` | Two-factor secret, base32 or an otpauth:// URI |
| `notes` | `string` | Anything else worth remembering |

### [`list_browser_logins`](#list_browser_logins)

List the logins the browser itself has saved for a profile — what Chromium stored while somebody used it, as opposed to what Mosaic holds for the profile (see list_credentials). Passwords come back decrypted; a row that could not be decrypted reports password: null, which is not an empty password. passwordManagerOff means the profile has Chromium's password manager disabled, so an empty list there is a setting rather than a fact about the browser.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |

### [`add_browser_login`](#add_browser_login)

Save a login into the browser itself, as though Chromium had saved it — so the browser will offer to fill it. The profile must be stopped: Chromium keeps its password store in memory while it runs and rewrites the file on exit, so a login written underneath a live browser is discarded when that browser closes. Refuses rather than doing nothing.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `url`required | `string` | The sign-in page — its origin becomes the realm |
| `username`required | `string` | Username or email |
| `password`required | `string` | Password, in plain text — Chromium encrypts it with the profile's own key |

### [`remove_browser_login`](#remove_browser_login)

Remove one login the browser has saved, named by realm and username — both required, because an unscoped delete on an irreversible route is how a cleanup becomes a loss. Needs the profile stopped, for the same reason adding one does.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `realm`required | `string` | The realm, as list_browser_logins reports it |
| `username`required | `string` | Username of the row to remove |

### [`list_browser_bookmarks`](#list_browser_bookmarks)

The bookmarks the browser itself holds, flattened out of Chromium's tree with the folder each sits in. Distinct from the profile's own bookmarks field, which is what Mosaic writes into the bar before every launch. A profile that has never run answers with an empty list rather than an error.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |

### [`list_browser_history`](#list_browser_history)

Domains this profile has visited, most visited first, with counts — domains rather than individual URLs. Enough to answer whether a profile has actually been used without reading somebody's browsing.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `limit` | `number` | How many domains. Default 40. |

### [`get_totp_code`](#get_totp_code)

Generate the current two-factor code for a saved login, with the seconds it stays valid. Check expiresIn before using it — a code with a second left will be rejected by the time it is submitted.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `credentialId`required | `string` | Credential id |

### [`fill_credential`](#fill_credential)

Type one field of a saved login into the focused field of a running profile, as real keystrokes with human timing rather than a paste. Focus the field in the browser first: this fills whatever is focused and deliberately does not guess which input is the password box. Returns the number of characters typed, never the value.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `credentialId`required | `string` | Credential id |
| `field` | `string` | username, password or totp. Default password. |

### [`list_downloads`](#list_downloads)

What each profile has downloaded, when `downloadMode` is "perProfile": the folder, the file count, the bytes and when the newest one landed, plus any folder left behind by a profile that no longer exists. In "shared" mode the browser writes to a folder this app does not own — the response says which mode it is in rather than returning an empty list that reads as "no files".

No arguments.

### [`clear_downloads`](#clear_downloads)

Permanently delete downloaded files. **It refuses to guess how much you meant**: name `ids`, or pass `all: true` for every profile’s files including folders orphaned by deleted profiles. There is no trash for these and nothing to restore from — the same rule `empty_trash` carries, for the same reason. Reports per profile with the file count and bytes that went, because a single number over a list is not an answer.

| Argument | Type | What it is |
| --- | --- | --- |
| `ids` | `string[]` | Profile ids whose files to delete. |
| `all` | `boolean` | Every profile’s files, and the orphaned folders. Cannot be combined with ids. |

### [`list_cookies`](#list_cookies)

A profile’s cookies. Read from the live browser when one is running, which is the only place the current session lives, and otherwise from the profile’s own jar — the answer says which, because the two can legitimately differ while a browser is open and writing.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |

### [`import_cookies`](#import_cookies)

Put cookies on a profile, replacing its imported list. Either a `cookies` array or a pasted `text` blob (Netscape cookies.txt, JSON, or header form) with a `domain` for the lines that do not carry one. An imported cookie reaches the browser once — at its next launch, or now if it is running — and after that only if you import it again with a different value or attributes; an unchanged one never overwrites a newer cookie the site has set since. The answer says how many were `applied` now, how many are `pending` for the next launch and how many were `unchanged`; `liveError` says why a running browser did not take them. Rows that could not be read come back in `errors` rather than being dropped — a cookie silently skipped is a login that silently does not work.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `cookies` | `object[]` | Cookie records, as `list_cookies` returns them. |
| `text` | `string` | A pasted blob instead: cookies.txt, JSON, or header form |
| `domain` | `string` | The domain to assume for pasted lines that do not name one |

### [`clear_cookies`](#clear_cookies)

Empty one profile’s cookie jar. By id, always — there is no form of this that means every profile. It logs that persona out of everything it was signed into, and nothing restores it.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |

### [`list_bookmarks`](#list_bookmarks)

The bookmarks Mosaic holds for a profile — the ones written into the bar at launch, which a preset can fill in. Not the same as `list_browser_bookmarks`, which reads what Chromium itself saved while somebody used the browser.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |

### [`add_bookmarks`](#add_bookmarks)

Set a profile’s bookmarks, replacing the list. Either a `bookmarks` array of `{ title, url }` or a pasted `text` blob of URLs, one per line.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `bookmarks` | `object[]` |  |
| `text` | `string` | URLs one per line instead, titles read off the lines |

### [`clear_profile_cache`](#clear_profile_cache)

Empty profiles’ browser caches — HTTP cache, code cache, GPU and shader caches — and keep everything else: cookies, logins, local storage, history and extensions stay, so every profile is still signed in. For shrinking profiles and clearing a stale cached script without logging out. A running profile is skipped by name, never stopped for you. By id — there is no form of this that means all of them.

| Argument | Type | What it is |
| --- | --- | --- |
| `ids`required | `string[]` | Profile ids |

### [`update_credential`](#update_credential)

Change a saved login. PATCH-shaped: an absent field is no change, and that matters more here than anywhere — omitting `password` keeps the stored one rather than blanking it. A new password or two-factor secret is encrypted through the OS keychain before it is written, and neither is ever returned.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `credentialId`required | `string` | The saved login’s id |
| `label` | `string` | What this login is for |
| `site` | `string` | The site it belongs to |
| `username` | `string` | Username or email |
| `password` | `string` | A new password, in plain text — encrypted before storage |
| `totpSecret` | `string` | A new two-factor secret, base32 or an otpauth:// URI; null removes it |
| `notes` | `string` | Anything else worth remembering |

### [`delete_credential`](#delete_credential)

Remove one saved login from a profile, by id. The secret goes with it and nothing restores it; the site’s own cookies are a separate thing and are untouched.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `credentialId`required | `string` | The saved login’s id |

## Presets and extensions 9

Presets (templates in the API) that make profiles, and the extensions a profile loads.

### [`list_extensions`](#list_extensions)

Unpacked extensions in the library, which a profile can be given by id. Separate from the companion Mosaic installs in every profile — that one is generated per launch and declares no page-visible surface at all.

| Argument | Type | What it is |
| --- | --- | --- |
| `projectId` | `string` | Only this project’s, plus the global ones |

### [`add_extension`](#add_extension)

Register an unpacked extension directory so profiles can load it. The path must be a folder containing a `manifest.json`; Mosaic loads it as unpacked rather than from a store, which is why there is no id to look up.

| Argument | Type | What it is |
| --- | --- | --- |
| `name`required | `string` | What to call it |
| `path`required | `string` | Absolute path to the unpacked extension directory |
| `projectId` | `string` | Scope it to one project; omit for every project |

### [`remove_extension`](#remove_extension)

Take an extension out of the library. Profiles that listed it stop loading it at their next launch; the directory on disk is left alone unless Mosaic unpacked it itself.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Extension id |

### [`list_templates`](#list_templates)

Profile presets: saved settings — proxy, privacy, start pages, extensions, tags — that a new profile starts from. Each one describes either a desktop or a phone; filter with `kind` rather than applying one to the other. A preset never carries an identity; every profile draws its own fingerprint whatever the preset says, because two profiles sharing one is two profiles a site reads as one.

| Argument | Type | What it is |
| --- | --- | --- |
| `projectId` | `string` | Only this project’s, plus the global ones |
| `kind` | `"desktop" \| "mobile"` | Only presets describing this kind of machine. A preset saved from a phone applied to a desktop profile produces a machine that is neither — the screen, touch points, GPU and capability record all come from a captured device record rather than the desktop catalogue. |

### [`create_template`](#create_template)

Author a preset from scratch — what every profile made from it starts with. Bookmarks, a default note, which extensions to load, which tabs to open, which tags to wear, and which *provider* to mint a proxy from. A provider and never an address: one gateway line mints a different session, and so a different exit, per profile, whereas an address stored here would put every profile the preset ever makes on one IP. The identity is not in here at all — each profile draws its own fingerprint from `fingerprintOptions`.

| Argument | Type | What it is |
| --- | --- | --- |
| `name`required | `string` | Preset name |
| `description` | `string` | What it is for, shown beside the name |
| `projectId` | `string` | Scope it to one project; omit for every project |
| `fingerprintOptions` | `object` | What each profile’s identity is drawn from — `os`, `brand`, `timezone`, `languages`. The `os` here is also what decides whether this preset describes a desktop or a phone. |
| `privacy` | `object` | Privacy switches, as on a profile. |
| `startOptions` | `object` | Launch mode and window geometry, as on a profile. |
| `startPages` | `string[]` | Tabs to open on launch. |
| `extensions` | `string[]` | Absolute paths to unpacked extensions. |
| `tags` | `string[]` | Tags every profile made from this preset wears. |
| `bookmarks` | `object[]` | Bookmarks in the bar of the first window. Each is `{ title, url }`; an http:// or https:// address is required, and a missing title shows as the URL, which is what Chromium does. |
| `note` | `string` | The profile’s note, pre-filled. Markdown — the same field the companion edits. |
| `proxy` | `object` | A provider to mint a fresh proxy from, per profile. Never an address: `{ providerId, pick }`, where the pick is `{ country, state, city, minutes }` in that seller’s own words. A pick the vendor cannot spell is refused by name rather than dropped — a dropped token authenticates and puts the profile somewhere else without saying so. |

### [`update_template`](#update_template)

Change a preset. PATCH-shaped: an absent field is no change, and `proxy: null` takes the provider off. There was no way to do this — a preset could only be made and deleted, so editing one meant rebuilding it from memory.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Preset id |
| `name` | `string` | New name; it cannot be emptied |
| `description` | `string` | What it is for |
| `projectId` | `string` | Scope it to one project; null for every project |
| `fingerprintOptions` | `object` | What each profile’s identity is drawn from. |
| `privacy` | `object` | Privacy switches. |
| `startOptions` | `object` | Launch mode and window geometry. |
| `startPages` | `string[]` | Tabs to open on launch. |
| `extensions` | `string[]` | Absolute paths to unpacked extensions. |
| `tags` | `string[]` | Tags every profile made from this preset wears. |
| `bookmarks` | `object[]` | Replaces the list. Each is `{ title, url }`. |
| `note` | `string` | The profile’s note, pre-filled. |
| `proxy` | `object` | `{ providerId, pick }`, or null to mint nothing. |

### [`delete_template`](#delete_template)

Delete a preset. The profiles already made from it are untouched — a preset is a starting point, not a link.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Preset id |

### [`save_profile_as_template`](#save_profile_as_template)

Turn a profile’s settings into a reusable preset. The identity is deliberately not copied — see list_templates.

| Argument | Type | What it is |
| --- | --- | --- |
| `profileId`required | `string` | Profile to take the settings from |
| `name`required | `string` | Preset name |
| `description` | `string` | What it is for |
| `projectId` | `string` | Scope it to one project; omit for every project |

### [`update_extension`](#update_extension)

Rename an extension in the library, or re-point it at a different folder. Profiles reference the *path*, so moving the path is how a shared extension is updated everywhere at once.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Extension id |
| `name` | `string` | New display name |
| `path` | `string` | New absolute path to the unpacked folder |
| `autoAdd` | `boolean` | Attach it to every new profile in its project |

## Proxies and providers 26

Saved proxies, provider accounts that mint a session per profile, checks, rotation and reputation.

### [`list_providers`](#list_providers)

List proxy providers — the seller accounts kept as templates, each of which mints a fresh proxy per profile. Each row says which of country, state, city, session and duration that seller's gateway can actually be asked for (`supports`), what its pasted line already asks for (`current`), one real example of the credential it produces (`preview`), and how many profiles were minted from it. Providers are global: there is no project to restrict them to.

No arguments.

### [`add_provider`](#add_provider)

Add a proxy provider from one pasted gateway line — the line a seller's dashboard gives you, in any of the forms `add_proxies` reads. The line is the specification: the vendor, the grammar, the account prefix and whatever it already asks for are all read back off it, so a seller that spells its keys differently still round-trips. Refuses a line that is the same gateway as one already stored, because five lines off one dashboard are five examples of one gateway differing in a session id this app rewrites anyway.

| Argument | Type | What it is |
| --- | --- | --- |
| `line`required | `string` | One gateway line, e.g. host:port:user-country-us-session-abc:pass |
| `name` | `string` | What to call it. Defaults to the vendor's own name. |
| `notes` | `string` | Anything worth remembering about this account |
| `type` | `string` | Default type when the line has no scheme: http, https, socks5 or socks4 |
| `defaults` | `object` | What every profile minted from this provider asks for unless it says otherwise: country, state, city, minutes. |

### [`read_provider_line`](#read_provider_line)

Say what a pasted gateway line means without storing it: which vendor it belongs to, which options that vendor can be asked for, what the line already asks for, and one real example of the credential this app would produce from it. Use before `add_provider` when you want to confirm the line was read the way you meant — particularly for a seller with more than one pool, where only the hostname tells them apart and the grammars differ.

| Argument | Type | What it is |
| --- | --- | --- |
| `line`required | `string` | One gateway line |
| `type` | `string` | Default type when the line has no scheme |

### [`update_provider`](#update_provider)

Change a provider's name, notes, defaults — or its line. A new line is re-read from scratch rather than merged, because a seller that moves an account to a different host is a different grammar and possibly a different carrier, and a merged parse would match neither.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Provider id |
| `line` | `string` | A replacement gateway line |
| `name` | `string` | New name |
| `notes` | `string` | New notes |
| `type` | `string` | Default type for a replacement line |
| `defaults` | `object` | Replacement defaults: country, state, city, minutes. |

### [`delete_provider`](#delete_provider)

Delete a provider. Profiles minted from it keep the proxy they were given and go on browsing from it — the credential lives on the profile, not behind a reference — so nothing loses its exit. What is lost is the ability to mint more. `keptProfiles` says how many are in that position.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Provider id |

### [`check_provider`](#check_provider)

Check a provider by minting one proxy from it and sending a real request through it. There is nothing else to check: a template is not an address, and nothing is listening until a credential names a session. The result is recorded on the provider, which is evidence about the account — has the balance run out, has the session limit been hit — rather than about any exit a profile will later use.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Provider id |

### [`mint_proxy`](#mint_proxy)

Mint one proxy from a provider without attaching it to anything, and return the credential. Every call mints a new session id, because that is what minting is — two profiles sharing a session id share an exit address, which is the single thing a proxy per profile exists to prevent. Refuses, naming the field, a pick this seller's gateway has no word for: writing one anyway produces a proxy that authenticates and then quietly ignores where you asked it to be.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Provider id |
| `country` | `string` | ISO country code, or "any" to take the option off |
| `state` | `string` | Subdivision name or code |
| `city` | `string` | City name |
| `minutes` | `number` | How long the exit holds, 0 to 1440 |

### [`list_proxy_vendors`](#list_proxy_vendors)

Every proxy seller this app recognises by hostname, and which options each one's gateway can be asked for. A seller not in this list still works — its line is read and used exactly as pasted — but nothing is added to it, because a guessed key name produces a proxy that authenticates and comes out in the wrong country without saying so.

No arguments.

### [`list_exit_places`](#list_exit_places)

Where an exit can be asked to be. With no argument: every country code and name. With a country: its subdivisions and its larger cities. Cities are suggestions rather than a closed list — every vendor takes a city as a free string — but the subdivisions matter, because sellers disagree about whether a state is written as a code or spelled out and this app converts between the two using exactly this table.

| Argument | Type | What it is |
| --- | --- | --- |
| `country` | `string` | ISO country code |

### [`list_proxies`](#list_proxies)

List saved proxies, including the result of their last connectivity check.

| Argument | Type | What it is |
| --- | --- | --- |
| `projectId` | `string` | Restrict to one project |

### [`add_proxies`](#add_proxies)

Bulk-add proxies from pasted text or the contents of a .txt/.csv file. Reads every common format and detects it per line: host:port, host:port:user:pass, user:pass:host:port, user:pass@host:port, scheme://user:pass@host:port (http, https, socks4, socks5), IPv6 in brackets, whitespace-separated fields, a trailing |US for the country, and a table with a header row separated by commas, tabs or semicolons (columns found by name: host/ip, port, username/login, password, type/protocol, country, name, rotation_url, tags, notes, or one proxy column). A line already saved — same type, host, port and credentials — is skipped rather than added twice, and every line not imported comes back in `skipped` or `errors` with its reason. Call `parse_proxy_list` first to preview. With `check: true` every new proxy is checked, a few at a time, and `checked` names which answered and which did not.

| Argument | Type | What it is |
| --- | --- | --- |
| `text`required | `string` | The list: one proxy per line, or a table with a header row |
| `type` | `string` | The type for a line that does not say (default http) |
| `projectId` | `string` | Restrict these proxies to one project; omit for every project |
| `namePrefix` | `string` | Name them "<prefix> 1", "<prefix> 2", … continuing after the highest number already used |
| `tags` | `string[]` | Tags added to every imported proxy |
| `check` | `boolean` | Check every new proxy after importing it (default false) |
| `concurrency` | `number` | How many checks run at once, 1–16 (default 6) |

### [`check_proxy`](#check_proxy)

Test a saved proxy: where it exits (IP, country, city, ISP, ASN, whether the range looks like a datacentre), whether it will `CONNECT` — a gateway can answer plain HTTP perfectly and refuse to tunnel, and that proxy loads nothing in a browser — and whether two connections come out of the same place, which decides whether any profile behind it can hold a timezone. It also scores the exit against blocklists, because an exit on one loses the account before the fingerprint is ever examined. The result is cached on the proxy and a successful check corrects the country it was declared with.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Saved proxy id |

### [`list_proxy_checks`](#list_proxy_checks)

Which proxies have a check in flight this second. A check is owned by the app rather than by whoever started it, so it keeps running after the caller goes away — which means a fresh caller cannot tell forty running checks from none, and would start them all again. Ask this before `check_proxy` on anything you did not start yourself. An empty list means nothing is running now, not that nothing has been checked: the last result is on the proxy.

No arguments.

### [`save_profile_proxy`](#save_profile_proxy)

Promote a profile’s pasted, not-yet-saved proxy into the Proxies library and link the profile to it, so the same address can be reused and edited in one place. Refused by name when there is nothing to save: a profile already on a saved proxy, a profile with no proxy, or a provider-minted session — that last one is one launch’s session id, not a reusable address, and saving it would pin many profiles behind one exit. Returns the new saved proxy.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `name` | `string` | Name for the saved proxy; defaults to "host:port" |

### [`score_proxy_reputation`](#score_proxy_reputation)

Score what a site sees when this proxy connects: whether the exit is a datacentre or looks residential, whether it is on public blocklists, and whether repeated samples come out of the same address. Refuses on a proxy that has never answered rather than quietly running a connectivity check first — there would be no exit address to score, and an errand the caller did not ask for is one they cannot see. Run check_proxy first.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Saved proxy id |

### [`update_proxy`](#update_proxy)

Change a saved proxy. PATCH-shaped: an absent field is no change. Editing the address clears its last check, because a result recorded against the old host says nothing about the new one.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Saved proxy id |
| `name` | `string` | What it is called in the list |
| `type` | `string` | http, https, socks4 or socks5 |
| `host` | `string` | Hostname or address |
| `port` | `number` | Port |
| `username` | `string` | Username |
| `password` | `string` | Password |
| `notes` | `string` | Anything worth remembering |
| `countryCode` | `string` | Where the seller says it exits, as a two-letter code |
| `city` | `string` | Where the seller says it exits |
| `tags` | `string[]` | Tags on the proxy row |
| `rotateOnLaunch` | `boolean` | Ask for a fresh session on every launch |
| `rotationUrl` | `string` | A URL the seller rotates the session on |
| `sessionId` | `string` | A session token to pin, which rotation then leaves alone |
| `provider` | `string` | The dialect id, from list_proxy_dialects |
| `projectId` | `string` | Scope it to one project; null for every project |

### [`delete_proxy`](#delete_proxy)

Remove a saved proxy from the library, by id. Profiles already carrying it keep the address they have — it is a working exit, and taking it out from under a configured profile would be a surprise nobody asked for. What they lose is the link back to the row.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Saved proxy id |

### [`assign_proxy`](#assign_proxy)

Point profiles at saved proxies, in one of four modes. `one`: every profile on `proxyId`, and `null` detaches. `roundRobin`: `proxyIds` dealt out in order, wrapping when there are more profiles than proxies. `unique`: one proxy per profile that no other profile holds — refused, naming the shortfall, when there are not enough, because the point of owning forty proxies is forty exits. `country`: proxies that come out in `country` (measured where checked, declared otherwise), unused first. The pool for `unique` and `country` is `proxyIds` when given, otherwise every saved proxy the profile’s project can see. The mode defaults to `roundRobin` when `proxyIds` is given and `one` otherwise. The answer says whether any proxy is now shared (`reused`, with a `shared-exit` entry in `warnings` naming the most profiles on any one) and names every assignment.

| Argument | Type | What it is |
| --- | --- | --- |
| `profileIds`required | `string[]` | The profiles to change |
| `mode` | `"one" \| "roundRobin" \| "unique" \| "country"` | How to choose; see the description |
| `proxyIds` | `string[]` | The pool, dealt out in order for roundRobin |
| `proxyId` | `string` | Mode one: one proxy for all of them, or null to detach |
| `country` | `string` | Mode country: a two-letter code, like "US" |

### [`scope_proxy`](#scope_proxy)

Move saved proxies between projects, or make them global. `projectId: null` means every project. By id — there is no form of this that means all of them.

| Argument | Type | What it is |
| --- | --- | --- |
| `ids`required | `string[]` | Saved proxy ids |
| `projectId` | `string` | The project to scope them to, or null for every project |

### [`parse_proxy_list`](#parse_proxy_list)

Read a pasted blob of proxy lines — or a .csv/.txt file’s contents — and say what each one came out as, without saving anything. Every line comes back in `lines` with a status: `ok`, `duplicate` (the same proxy earlier in the list), `exists` (already saved; pass `projectId` to compare against what that project can see) or `error`, and every one that will not be imported carries its reason by name — the port that is not a number, the scheme that is not a proxy type. Use it before `add_proxies` on a list you have not imported before.

| Argument | Type | What it is |
| --- | --- | --- |
| `text`required | `string` | The pasted lines |
| `type` | `string` | The type to assume where a line does not say (default http) |
| `projectId` | `string` | Compare against the proxies this project can see |

### [`export_proxies`](#export_proxies)

The saved proxies as text another tool can read — including this one. `url` is scheme://user:pass@host:port, one per line; `colon` is host:port:user:pass, what most sellers print (it cannot carry a type); `csv` has a header row with name, type, host, port, username, password, country, rotation_url, tags and notes. Pass `ids` for a selection or `projectId` for one project; with neither, every saved proxy. The passwords are in the output, because a list without them imports nothing.

| Argument | Type | What it is |
| --- | --- | --- |
| `format` | `"url" \| "colon" \| "csv"` | Default url |
| `ids` | `string[]` | Only these proxies |
| `projectId` | `string` | Only the proxies this project can see |

### [`rotate_proxy`](#rotate_proxy)

Fetch a saved proxy’s change-IP link (`rotationUrl`) from Mosaic, wait for the modem to re-dial, then check the proxy: `{ status, before, after, changed, check }`. `changed` is true or false only when both exits are known. A link that answers anything but 2xx is refused naming the status — nothing was rotated — and a proxy with no link is refused by name. Pass `ids` to rotate several, one after another; the answer names every one that failed.

| Argument | Type | What it is |
| --- | --- | --- |
| `id` | `string` | One saved proxy |
| `ids` | `string[]` | Several, rotated in sequence |

### [`check_proxies`](#check_proxies)

Check several saved proxies at once, a few in flight at a time, and say how each one went by name: `{ checked, ok: [{ id, name, ip, countryCode }], failed: [{ id, name, reason }] }`. Each result is also recorded on its proxy, as `check_proxy` does. By id — there is no form of this that means all of them.

| Argument | Type | What it is |
| --- | --- | --- |
| `ids`required | `string[]` | Saved proxy ids |
| `concurrency` | `number` | How many checks run at once, 1–16 (default 6) |

### [`list_proxy_dialects`](#list_proxy_dialects)

The proxy *dialects* this build understands — the per-seller rules for how a session id and a country are spelled into a credential. Not the same as `list_proxy_vendors`, which is the catalogue of sellers; a dialect is the grammar, and it is what decides whether a targeting token reaches the gateway or is silently dropped.

No arguments.

### [`check_proxy_line`](#check_proxy_line)

Check an address that is not in the library: pass host, port and credentials and get the same result a saved proxy’s check returns — reachable, the exit address, and where the seller says it is. For deciding whether to keep a proxy before adding it.

| Argument | Type | What it is |
| --- | --- | --- |
| `host`required | `string` | Hostname or address |
| `port`required | `number` | Port |
| `type` | `string` | Default http |
| `username` | `string` | Username |
| `password` | `string` | Password |

### [`check_profile_proxy`](#check_profile_proxy)

Check the proxy a profile is actually carrying, whatever it is — a saved row, a pasted line, or one minted from a provider. The result is written back onto the profile, which is what a launch reads to decide whether it may start, so this is the call that turns "never checked" into a profile that will open.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |

## Profiles and projects 26

Create, start, stop, move and organise profiles, projects and tags.

### [`list_projects`](#list_projects)

List all projects with their profile counts. Profiles always belong to exactly one project.

No arguments.

### [`create_project`](#create_project)

Create a project to group profiles.

| Argument | Type | What it is |
| --- | --- | --- |
| `name`required | `string` | Project name |
| `icon` | `string` | Emoji icon |
| `color` | `string` | Hex colour |

### [`get_project`](#get_project)

One project, with how many profiles it holds and how many of those are phones.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Project id |

### [`update_project`](#update_project)

Rename a project or change its icon or colour. Only the fields you send are changed. Exactly one project carries `system: true` — the one Mosaic made, where every profile lands before it is filed — and its name, icon and colour are fixed; sending any of them is refused with the field named. Its `sortOrder` is still yours to set.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Project id |
| `name` | `string` | New name |
| `icon` | `string` | Emoji or icon name |
| `color` | `string` | Hex colour, e.g. #4a6cc4 |

### [`delete_project`](#delete_project)

Delete a project. **Every profile in it goes to the trash with it**, recoverable for the retention period, and restoring the project brings back exactly those profiles. Refuses when it is the last project, and for the `system` project — nothing re-creates that one. This is a destructive call on everything the project holds — check list_profiles for that project first if you are not certain what is in it. **Any browser open in the project is closed first**, and a browser that will not close keeps the whole project rather than hiding a running window behind a deleted row.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Project id |

### [`list_profiles`](#list_profiles)

List profiles, optionally filtered by project, search text or tag. Each summary carries `status`, the live run state.

| Argument | Type | What it is |
| --- | --- | --- |
| `projectId` | `string` | Restrict to one project |
| `search` | `string` | Match against name, note and tags |
| `tag` | `string` | Exact tag match |
| `os` | `string` | Keep only this platform: windows, macos, linux or android |
| `limit` | `number` | How many to return. Default 100, max 500. |
| `offset` | `number` | Skip this many. `total` counts every match, not the page, so it is what says whether another page exists. |
| `sort` | `string` | Order the whole result before paging: …. Omit for most-recently-used first, falling back to when the profile was made. Sorting matters here precisely because the answer is paged — without it, "the five profiles I used most recently" needs every row fetched. |
| `order` | `string` | `asc` or `desc`. Defaults to `desc` for the two dates and `asc` for the rest. A profile that has never run sorts last either way. |

### [`get_profile`](#get_profile)

Fetch one profile in full, including its complete fingerprint, proxy and privacy settings.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |

### [`create_profile`](#create_profile)

Create one or more profiles. Each gets a fresh, internally coherent fingerprint drawn from real-device distributions. Nothing is required: `create_profile` with no arguments works. Two defaults exist so you never need a setup call — the profile lands in the system project when you name none, and `proxy` takes a pasted proxy string that is attached to this profile and saved nowhere.

**A profile created with a proxy is drawn for that proxy’s exit.** The proxy is measured first and the country, timezone, languages and geolocation come from the address it really leaves by — you do not have to pass `countryCode`, and you should not guess one. Every result carries `exitMatch`, which says whether that measurement happened and, when it did not, why: a dead gateway still creates the profiles, with an identity from the table, and says so rather than reporting a clean batch.

| Argument | Type | What it is |
| --- | --- | --- |
| `projectId` | `string` | Target project. Optional — omitting it uses the system project, the one Mosaic made at first launch and the one that cannot be deleted. The same answer every run, unlike “whichever project the window is showing”. |
| `name` | `string` | Profile name. {n} is the number in a batch, {n:3} pads it to three digits — Shop-{n:3} |
| `count` | `number` | How many to create (default 1, max 500) |
| `startAt` | `number` | The first number a batch is named with; default is after the highest that name already has |
| `note` | `string` | Free-text note |
| `tags` | `string[]` | Tag names |
| `os` | `string` |  |
| `deviceId` | `string` | Draw one exact machine from list_devices rather than a weighted draw. A phone id such as `sm-s918u` implies android. |
| `countryCode` | `string` | ISO-3166 alpha-2; sets timezone, languages and geolocation coherently |
| `proxyId` | `string` | Attach a proxy from the saved list by id. Use this for an exit you keep and share; use `proxy` for anything else. |
| `proxy` | `value` | A proxy for this profile alone, as a plain string — the field you almost always want. Any of scheme://user:pass@host:port, user:pass@host:port, host:port:user:pass, host:port@user:pass, or a bare host:port (scheme defaults to http). It is NOT added to the saved proxies list, so a run through a hundred rotating sessions leaves that list exactly as it found it. If the exit is already saved, the profile links to that entry instead of duplicating it. An unreadable line is refused, quoting it. Or, instead of a line, an object naming a provider and where its exit should be: {"providerId": "…", "country": "US", "state": "California", "city": "Los Angeles", "minutes": 30}. That mints a proxy from that seller’s template with a session id nothing else holds, which is how two profiles are kept off one exit address. Every field but providerId is optional; one the seller’s gateway cannot spell is refused by name rather than quietly dropped. See list_providers. With count above 1, a line or a proxyId puts the whole batch on one address: allowed, not recommended, and answered with a `shared-exit` entry in `warnings`; a provider mints one exit per profile. |
| `matchExit` | `boolean` | Measure the attached proxy and draw the identity for the address it actually exits from. On by default, which is what you want: a profile whose timezone and language contradict its exit fails the first comparison every consistency scanner makes. Pass false to skip the lookup and draw from the table instead — faster, and correct only when you do not care where the profile appears to be. |

### [`update_profile`](#update_profile)

Update a profile. Only the fields you pass change. `proxy` is the quickest way to move a profile onto a new exit: pass the line and it replaces whatever was there, saved proxy included.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `name` | `string` | New name |
| `note` | `string` | New note |
| `tags` | `string[]` |  |
| `proxyId` | `string` | Saved proxy id, or empty string to detach |
| `proxy` | `value` | A proxy for this profile alone, as a plain string — the field you almost always want. Any of scheme://user:pass@host:port, user:pass@host:port, host:port:user:pass, host:port@user:pass, or a bare host:port (scheme defaults to http). It is NOT added to the saved proxies list, so a run through a hundred rotating sessions leaves that list exactly as it found it. If the exit is already saved, the profile links to that entry instead of duplicating it. An unreadable line is refused, quoting it. Pass an empty string to remove the proxy entirely, which also clears proxyId. Or, instead of a line, an object naming a provider and where its exit should be: {"providerId": "…", "country": "US", "state": "California", "city": "Los Angeles", "minutes": 30}. That mints a proxy from that seller’s template with a session id nothing else holds, which is how two profiles are kept off one exit address. Every field but providerId is optional; one the seller’s gateway cannot spell is refused by name rather than quietly dropped. See list_providers. |
| `oneTime` | `boolean` | Mark it a throwaway: its browser data is discarded when it stops |
| `pinned` | `boolean` | Keep it at the top of the list, in every order |

### [`delete_profile`](#delete_profile)

Move a profile to the trash. Reversible: it keeps its cookies, logins and browser data and waits out the retention window, and `restore_from_trash` brings it back whole. This said "Irreversible" and was wrong — `store.deleteProfile` has always trashed rather than purged, and an assistant reading that description would refuse the safe operation and reach for something worse. `purge` on `empty_trash`, or the REST `DELETE /trash/profiles/:id`, is the one that actually destroys. **If the profile is running its browser is closed first**; a browser that will not close is refused by name and the profile stays on the list, because a trashed row has no Stop button.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |

### [`start_profile`](#start_profile)

Launch a profile and return its DevTools endpoint for Puppeteer, Playwright or raw CDP.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `headless` | `boolean` | Run without a visible window. Changes the User-Agent to HeadlessChrome unless the profile sets one; prefer `offscreen`. |
| `offscreen` | `boolean` | Run headed but parked off-screen, with no Dock identity — for a run nobody is going to watch. Keeps the real User-Agent and GPU. |
| `url` | `string` | Open this URL instead of the start pages |

### [`stop_profile`](#stop_profile)

Stop a running profile. The browser flushes its cookies and storage to disk first, rather than being killed mid-write.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |

### [`list_running`](#list_running)

List every currently running profile with its process id and DevTools endpoint.

No arguments.

### [`list_tags`](#list_tags)

List the tags in a project with their colours and how many live profiles carry each. Tags cut across projects — a profile has any number of them. Use the `tag` argument of `list_profiles` to filter by one.

| Argument | Type | What it is |
| --- | --- | --- |
| `projectId` | `string` | Restrict to one project |

### [`rename_tag`](#rename_tag)

Rename a tag, or change its colour, or both. A rename reaches every profile carrying it — profiles store tags by name, so this is the only correct way to change one. Renaming onto a name that already exists in the project is refused rather than merging, because merging two tags is a bigger decision than a rename.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Tag id, from `list_tags` |
| `name` | `string` | The new name; omit to change only the colour |
| `color` | `string` | A hex colour like `#2b7fbd`; omit to change only the name |

### [`delete_tag`](#delete_tag)

Delete a tag. The tag is removed from every profile that carried it; no profile is deleted or otherwise changed. Call `list_tags` first if you want to tell the user how many profiles it is on.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Tag id, from `list_tags` |

### [`start_profiles`](#start_profiles)

Start several profiles, one at a time with a gap between them.

**Prefer this over calling `start_profile` in a loop**, and the reason is not politeness. The concurrency cap counts sessions that are still `starting` as well as `running`, so parallel starts all read the count before any has registered, all pass the check, and exceed the cap the operator set. This awaits each launch, which is what makes that setting mean what it says. It is also what keeps the machine usable: every launch is a browser with its own renderer, GPU process and network service.

A profile already running is `skipped`, not a failure. One that cannot start — a dead proxy, a missing engine — costs that row and nothing else.

| Argument | Type | What it is |
| --- | --- | --- |
| `ids`required | `string[]` | Profile ids to start. |
| `staggerMs` | `number` | Gap between launches in milliseconds, 0–60000. Default 1000. |

### [`stop_profiles`](#stop_profiles)

Stop several profiles. Each is closed through `Browser.close` so Chromium flushes its cookie jar rather than being killed mid-write — a profile killed instead of closed can lose the session it just built. Concurrent, unlike starting: nothing is being competed for. A profile that is not running is `skipped`.

| Argument | Type | What it is |
| --- | --- | --- |
| `ids`required | `string[]` | Profile ids to stop. |

### [`bulk_update_profiles`](#bulk_update_profiles)

Apply the same change to many profiles at once. Only the fields you pass move; everything else is left alone on every profile, including where they differ from each other. Tags add and remove rather than replace, so a profile keeps tags you did not name. Running profiles are skipped rather than half-edited, and the answer says which. Putting the selection on one saved proxy (`proxyId`) or one pasted line (`proxy`) is allowed and answered with a `shared-exit` warning; a provider gives each profile its own exit.

| Argument | Type | What it is |
| --- | --- | --- |
| `ids`required | `string[]` | Profiles to change |
| `proxyId` | `string` | Attach this saved proxy, or empty string to detach |
| `proxy` | `value` | Recommended: mint from a proxy provider, { providerId, country, state, city, minutes } — each selected profile gets its own session id, and so its own exit address. Also accepted: one proxy for the whole selection, as a pasted line (scheme://user:pass@host:port, host:port:user:pass, …) or a config object, or null to detach. One address for many profiles is allowed and is not recommended: sites can link accounts that share an address, so the answer carries a `shared-exit` entry in `warnings` naming how many now share it. |
| `oneTime` | `boolean` | Mark them throwaways, or take the mark off |
| `pinned` | `boolean` | Pin them to the top of the list, or unpin them |
| `addTags` | `string[]` | Tag names to add |
| `removeTags` | `string[]` | Tag names to remove |

### [`duplicate_profile`](#duplicate_profile)

Copy a profile’s settings — proxy, privacy, start pages, extensions, tags — into a new one. The identity is drawn fresh by default, because two profiles sharing a seed share a canvas and audio fingerprint and are one profile to any site that measures them; `newFingerprint: false` copies it verbatim instead, which is only right when the original is being replaced rather than joined. Cookies, storage and history are never copied: a duplicate starts with an empty jar, and `export_profile` over REST is what moves a session.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `name` | `string` | Name for the copy; defaults to "<name> copy" |
| `newFingerprint` | `boolean` | Draw a new identity (default true). False copies the original’s, which makes two profiles one to a fingerprinter. |

### [`create_profile_shortcut`](#create_profile_shortcut)

Put a shortcut to one profile on this computer: a small app on macOS, a .lnk on Windows, a .desktop file on Linux. Opening it starts that profile’s browser without opening the Mosaic window — Mosaic runs in the background for as long as the browser is open, because the proxy relay and the cookie flush live there, and quits when it closes. `place` is `desktop` (default) or `applications` (~/Applications, the Start menu, or ~/.local/share/applications). Returns the path written. Creating one again replaces the earlier one for that profile in that place.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | Profile id |
| `place` | `string` | Where to put it. Default desktop. |

### [`move_profiles`](#move_profiles)

Move profiles into another project, by id. Its own tool rather than a field on `bulk_update_profiles`, which is where this was *mapped* and does not accept a `projectId` — so until now there was no way to move a profile from this surface at all. Nothing about a profile changes except which project it belongs to: the fingerprint, proxy, cookies, logins and history are untouched and a running browser keeps running. Tag names travel and are registered in the destination project, because a tag’s colour belongs to a project and a moved profile would otherwise keep the words and lose the colours. Reports what moved **by name**, and separately any id whose profile could not be read.

| Argument | Type | What it is |
| --- | --- | --- |
| `ids`required | `string[]` | Profile ids to move |
| `projectId`required | `string` | The project to move them into, from list_projects |

### [`delete_profiles`](#delete_profiles)

Move several profiles to the trash, by id. Each one’s browser is closed first, at this door like every other — a hidden row with a live window still on its proxy and still writing cookies is the state this rule exists to prevent. A profile whose browser would not close stays on the list, where it still has a Stop button, and comes back named rather than counted.

| Argument | Type | What it is |
| --- | --- | --- |
| `ids`required | `string[]` | Profile ids |

### [`export_profiles`](#export_profiles)

Write profiles to a Mosaic archive on disk, and return where it went. A path rather than bytes, because an archive is megabytes of zip and a tool result is text — and the file is what `import_profiles` takes. `includeBrowserData` carries the cookies, logins and history; leave it off for a library you are handing to somebody else.

| Argument | Type | What it is |
| --- | --- | --- |
| `ids`required | `string[]` | Profile ids to export |
| `path`required | `string` | Absolute path to write the .zip to |
| `includeBrowserData` | `boolean` | Carry cookies, logins and history (default true) |

### [`import_profiles`](#import_profiles)

Read a Mosaic archive off disk into this installation. A library exported from another platform is carried rather than refused — the profiles land flagged, which `library_health` counts and Repair redraws, because refusing somebody’s whole library at the door is worse than carrying it and saying what is wrong with it.

| Argument | Type | What it is |
| --- | --- | --- |
| `path`required | `string` | Absolute path to the .zip |
| `projectId` | `string` | Where to put them; omit for the system project |

### [`create_tag`](#create_tag)

Create a tag with a colour. Tags are also created implicitly by naming one on a profile or a preset; this is for choosing the colour up front rather than taking whatever was assigned.

| Argument | Type | What it is |
| --- | --- | --- |
| `name`required | `string` | Tag name |
| `color` | `string` | Hex colour, e.g. #c9a227 |
| `projectId` | `string` | Scope it to one project; omit for every project |

## The app 19

Settings, the engine, updates, the account, support and what the API has been doing.

### [`submit_support_request`](#submit_support_request)

Write to Mosaic’s support: `kind` is `support` (a question or a problem — this one also emails the support inbox), `feature` (a request) or `bug` (a report). Works signed out, in which case `email` is required — it is where the reply goes. Up to 4 screenshots as base64 PNG, JPEG or WebP, 3 MB each; their metadata is stripped before they are stored. `diagnostics: true` attaches the app and engine versions, the operating system and the last log lines with credentials, tokens and addresses scrubbed — call `preview_support_diagnostics` first to read exactly what that sends, and only send it if the operator has agreed. The answer is the new request; `list_support_requests` shows replies as they arrive.

| Argument | Type | What it is |
| --- | --- | --- |
| `kind` | `"support" \| "feature" \| "bug"` | What this is. Default support |
| `subject` | `string` | One line, up to 140 characters. Optional — the first line of the message when absent |
| `message`required | `string` | What happened, or what is wanted. Up to 10,000 characters |
| `email` | `string` | Where to reply. Required when Mosaic is not signed in; ignored otherwise |
| `screenshots` | `object[]` | Up to 4 of { name, type: "image/png"\|"image/jpeg"\|"image/webp", data: base64 } |
| `diagnostics` | `boolean` | Attach the scrubbed diagnostics. Default false |

### [`list_support_requests`](#list_support_requests)

Every support request this person has filed — the account’s, and any filed from this install while signed out — newest activity first, each with its `status` (open, in_progress, answered, fixed, closed), how many replies it has and who wrote last (`lastFrom: "staff"` means an answer is waiting).

No arguments.

### [`get_support_request`](#get_support_request)

One support request with its whole conversation, oldest first, and the list of its screenshots.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The request id, from list_support_requests |

### [`reply_support_request`](#reply_support_request)

Add a message to a support request. A reply to one that was answered, fixed or closed opens it again.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The request id |
| `message`required | `string` | The reply, up to 10,000 characters |

### [`get_support_screenshot`](#get_support_screenshot)

One screenshot of a support request, as base64 with its type.

| Argument | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The request id |
| `screenshotId`required | `string` | The screenshot id, from get_support_request |

### [`preview_support_diagnostics`](#preview_support_diagnostics)

Exactly what `diagnostics: true` would attach to a support request, scrubbed, so it can be read before anything is sent. Sends nothing.

No arguments.

### [`get_account`](#get_account)

The plan this Mosaic is on — Free or Pro — what it allows, and how much of it is in use: `limits.profiles`, `limits.creations` (profiles made in all, counted by the account server per account and per computer — deleting one does not give it back), `limits.projects` and `limits.devices` (null is unlimited; on Free they are this account’s own — `freeTier` is `current`, or `legacy` for an account made before the Free plan shrank, which keeps 5 profiles and 20 creations), whether the API and MCP are included, `usage` (`usage.creations` is the server's last count), and `source`, which says why (a paid subscription, a Free account, or a build with no account server, which limits nothing). `billing` is `card` for a Stripe subscription or `crypto` for Pro paid ahead, where `periodEnd` is the paid-until and nothing renews. `interval` is `month` or `year`; `status` `paused` with `resumesAt` is a break in billing (Pro withheld, profiles kept). Never returns a token. Read-only: signing in and paying happen in the Mosaic window.

No arguments.

### [`delete_account`](#delete_account)

Delete the Mosaic account this app is signed in to — at the account server, for good — and sign this Mosaic out. Irreversible: the account, its sign-in sessions on every computer and its support history go; a live subscription is cancelled first so the card is not charged again. Every browser is stopped first (one that will not close is named and nothing is deleted), then Mosaic restarts on its sign-in screen. The account's profiles can never be opened again: they stay on this disk, encrypted, but their key belonged to the account and is deleted with it — and they were never on the server, so no copy exists anywhere. This computer keeps its count of profiles made, so a new account here starts where the old one stopped. Requires the account `password`, every time; a wrong one deletes nothing. The root (operator) account cannot be deleted from the app.

| Argument | Type | What it is |
| --- | --- | --- |
| `password`required | `string` | The account's password, typed again: the confirmation |

### [`api_activity`](#api_activity)

What the API is doing right now: which keys have driven Mosaic in the last five minutes, how many profiles they are running, every profile any of them has touched — each a full profile row carrying its project and the key that touched it — and a feed of recent actions in plain words ("Created profile — Ads scraper 4"). `profiles` and `actions` cover the same retained events, so a profile named in the feed is always in the list. The same picture the interface shows on its API activity page. Useful before you start work: if another key is already driving a profile, you are about to fight it. Never returns a key, only the name its owner gave it.

No arguments.

### [`set_traffic_inspection`](#set_traffic_inspection)

Turn traffic inspection on or off, and read where it points. With it on, **every profile launched from now on is routed through an inspecting proxy** — Proxyman, Charles, mitmproxy, Fiddler, Burp — so its requests can be read. Call with no arguments to read the state without changing it.

**Put it back the way you found it.** The returned `was` is the state before your call. If it was off and you turned it on, turn it off again when you are finished; if it was already on, leave it on — the operator has something open and watching, and switching it off mid-session takes their tooling away with nothing saying why. This is the whole discipline of this tool: it is a global switch on a machine somebody else is using.

Only launches are affected, and only from the next one: a browser already running keeps the chain it started with, so turning this on does not begin recording a session in progress — restart the profile.

`address` is `host:port`, stored normalised — and on a machine with Proxyman you can leave it alone, because Mosaic asks the inspector where it listens. A profile **with a proxy of its own** needs the inspector forwarding back to Mosaic (browser → inspector → Mosaic → the exit), and **Mosaic arranges that itself** through the inspector’s own command line, putting it back when inspection is switched off. There is nothing to ask the operator to configure. The chain is measured before each window opens and a launch is refused with a 502 rather than sent out of this computer’s address — so a start that succeeds is a start that is correctly chained.

**A profile that is already running is not affected, in either direction.** Chromium is handed its proxy on the command line and there is no way in from outside, so switching this on leaves every open browser going out exactly as it was, and switching it off leaves an open one still going through the inspector. `GET /running` reports `inspectedBy` per profile — what that browser was actually launched through — so the disagreement is readable rather than guessed at. Restarting the profile is what moves it.

HTTPS also needs the inspector’s CA certificate trusted on this machine, which is something only the operator can do. If pages fail to load in an inspected profile, that is the first thing to ask about.

| Argument | Type | What it is |
| --- | --- | --- |
| `enabled` | `boolean` | Turn inspection on or off. Omit to read the current state without changing it. |
| `address` | `string` | Where the inspecting proxy listens, host:port — e.g. 127.0.0.1:9090, which is Proxyman’s default. Omit to keep the stored address. |

### [`get_settings`](#get_settings)

Mosaic’s own settings: the API and MCP ports, the default engine, the concurrency cap, the trash retention window and where profile data lives. Read-only on purpose — changing a port or the data directory from here would move the ground under the client that is talking to it.

No arguments.

### [`list_engines`](#list_engines)

List browser engines: whether the browser is installed, its version, and whether it is the copy bundled with the app or a newer downloaded one. `install` reports a download in progress and the last failure, and `install.update` whether a newer engine is published (`available`, `latest`, `current`) — an optional update, offered and never installed on its own; `engine_install` takes it.

No arguments.

### [`engine_available`](#engine_available)

What the published browser is, which build this machine can take, and whether it is newer than the one in use (`updateAvailable`). Reads only — nothing is downloaded. The source is `engineManifestUrl` when set, otherwise the Mosaic account server (sent this install’s session). A manifest with any field missing, unknown, not https or loopback is refused rather than repaired, because it decides which executable holds every cookie jar on this machine.

No arguments.

### [`engine_install`](#engine_install)

Download and install the published browser for this machine — an engine update, independent of the app. About a gigabyte; the checksum is verified before anything is unpacked and the binary is asked its version before it is used. The copy bundled with the app is never touched; the download wins from the next launch because it is newer. Refuses while a profile is running on a previously downloaded copy, because that is the one being replaced.

No arguments.

### [`update_settings`](#update_settings)

Change Mosaic’s settings. PATCH-shaped: an absent field is no change. An unknown key is refused and named rather than dropped, because a misspelled setting and a stored one are byte-identical from out here. Read `get_settings` first for the field names — until now exactly one setting was reachable from this surface, through `set_traffic_inspection`.

No arguments.

### [`check_for_update`](#check_for_update)

Is there a newer Mosaic. Reads the release manifest at downloads.mosaicbrowser.com — public, no credential — and returns the updater's whole state: `phase` (idle, checking, up-to-date, available, downloading, ready, installing, error), `current`, `latest` with its release notes, download `progress`, `runningProfiles`, `restartWhenIdle`, and `replaceable`/`blocked` for a copy that cannot replace itself (a development build, one outside /Applications, or a Windows copy from the portable zip rather than the installer). A check that could not run is reported in `error` with the reason, never as "no update"; `error.quiet` is true when it was only the network, which is retried by itself. With `updateAutoDownload` on, a found update starts downloading in the background. `cached: true` reads the state without contacting anything.

| Argument | Type | What it is |
| --- | --- | --- |
| `cached` | `boolean` | Read the state without checking. Default false. |

### [`download_update`](#download_update)

Fetch the newer version and verify it, without installing anything. The archive is checked against the SHA-256 in the manifest before it is unpacked or run, the download is asked its own version and has to agree (the bundle on macOS, the installer's version resource on Windows), and when this copy is Developer-ID signed the new one must be signed by the same team and pass `codesign --verify`. Downloading is allowed while profiles run — it touches nothing they use; restarting is where they are counted. Returns `{ version, bytes }` once the update is ready.

No arguments.

### [`apply_update`](#apply_update)

Restart Mosaic into the downloaded version. **Never restarts under a running profile:** with browsers open, `when: "now"` is refused with the count unless `stopProfiles: true` asks for them to be stopped first (each through the browser's own exit, so cookies are written); `when: "when-idle"` records the wish and restarts by itself once the last profile closes; `when: "cancel"` withdraws that. A restart **ends the process this tool is served from**, so it is the last call in a session: the answer is returned first and the restart follows a moment later. On macOS the old application is moved aside rather than deleted, and a swap that fails puts it back; on Windows the verified installer runs silently and starts the new version.

| Argument | Type | What it is |
| --- | --- | --- |
| `when` | `"now" \| "when-idle" \| "cancel"` | Default now. |
| `stopProfiles` | `boolean` | With when: now, stop running profiles first. Default false. |

### [`app_info`](#app_info)

What this installation is: the version, the platform and architecture it is running on, where its data lives, and whether the REST and MCP servers are up. The first call to make when a report has to say which build it came from.

No arguments.

## Run the API on your own profiles

The REST API and the MCP server are part of Pro: $49 a month, with the first 14 days free. Free has every browser feature and no API key.

Mosaic is a desktop app

Email yourself the link and open it on your computer.
