dolphin-anty-mcp
An MCP server for the Dolphin{anty} anti-detect browser that exposes 71 tools for managing profiles, proxies, cookies, and more, plus page-level browser automation and a raw request escape hatch.
README
dolphin-anty-mcp
An MCP server for the Dolphin{anty} anti-detect browser, built from its public OpenAPI document (v1.0.7).
71 tools covering browser profiles, proxies, folders, statuses, fingerprints, cookies, local storage, extensions, homepages, bookmarks and team management — plus page-level automation that drives the Anty browser over the DevTools protocol, and a raw-request escape hatch for anything not modelled explicitly.
Setup
npm install && npm run build
Generate a JWT at https://dolphin-anty.com/panel/index.html#/api. It is shown once, so copy it before closing the page.
Register the server with your MCP client:
{
"mcpServers": {
"dolphin-anty": {
"command": "node",
"args": ["C:/Users/meow/Documents/Dolphin/dist/index.js"],
"env": { "DOLPHIN_API_TOKEN": "your-jwt-here" }
}
}
}
| Variable | Default | Purpose |
|---|---|---|
DOLPHIN_API_TOKEN |
— | JWT for the remote APIs. Without it only Local API tools work. |
DOLPHIN_LOCAL_API_URL |
http://localhost:3001 |
Local API origin. |
DOLPHIN_TIMEOUT_MS |
60000 |
Per-request timeout. |
If port 3001 was busy when Dolphin started, it silently picks the next free port. Check
Settings → Health in the app and set DOLPHIN_LOCAL_API_URL to match.
The two APIs
This matters more than anything else in the setup:
- Remote — profiles, proxies, folders, statuses, fingerprints, remote cookies. Works from anywhere with a valid JWT.
- Local —
start_profile,stop_profile, cookie robot, local storage. Only answers while the Dolphin{anty} desktop app is running on the same machine, bound to loopback.
Three remote hosts are involved (dolphin-anty-api.com, apiv2.…/api/v2,
darkwing.…/api/v1); each tool is pinned to the right one, so you never pick.
Using a profile by name
People say "spin up jannytest", not "start profile 832201155". start_profile and stop_profile
therefore accept name and resolve it themselves:
{ "name": "jannytest", "automation": true }
This exists because the obvious design failed in practice. Every lifecycle endpoint is keyed by
numeric id, so the only tool that took a profile name was create_profile — and a model handed
a name followed the path of least resistance and created a duplicate, silently losing the cookies,
logins and history the user actually wanted.
Three things close that off:
start_profile/stop_profiletakenameand report the id they resolved.create_profilerefuses a name that already exists, naming the existing id and pointing atstart_profile. Override withallowDuplicateName: truewhen a separate profile really is intended.- An unresolvable name fails loudly and says not to create a replacement, instead of quietly producing one.
Driving the browser
create_profile → coherent fingerprint attached automatically
start_profile → automation endpoint cached; browser_* tools attach on their own
browser_snapshot → see the page as an accessibility tree
browser_click/type → act on it
stop_profile → syncs the data directory back to the cloud
Skipping stop_profile leaves the session's cookies and storage unsynced.
Targeting. browser_snapshot prints lines like button "Log in" — the role and quoted name
are exactly what target takes:
{ "profileId": 123, "target": { "role": "button", "name": "Log in" } }
Fall back to text, label, placeholder, testId or raw css; add nth when several match.
There are no opaque element refs to keep in sync, because ariaSnapshot({ ref: true }) does not
actually emit them in playwright-core 1.62 — role+name is what the snapshot gives you, so it is
what the tools consume.
Snapshot first, act second. browser_screenshot exists but costs far more context; reach for it
only when the visual rendering itself matters.
Tabs
browser_tabs lists, opens, switches and closes tabs. Every other browser tool also takes a
tabIndex.
{ "profileId": 123, "action": "list" } → [0] Inbox — https://…
{ "profileId": 123, "action": "select", "index": 1 } → switch for good
{ "profileId": 123, "action": "new", "url": "https://…" }
{ "profileId": 123, "action": "close", "index": 1 }
The two are deliberately different. action: "select" changes the active tab for every later
call; tabIndex acts on one tab for a single call and leaves the active tab alone. That split
matters — a read-only browser_snapshot { tabIndex: 2 } silently redirecting all subsequent
clicks to tab 2 is the kind of thing a model never recovers from.
Popups are reported on the action that caused them. A target="_blank" link leaves the
current page looking unchanged, so a click that spawns a tab appends:
🔗 1 new tab(s) opened — the snapshot above is still the original tab:
[1] https://example.com/b
Use browser_tabs { action: "select", index: 1 } to switch to it, or pass tabIndex to peek.
The new tab is not auto-selected — the model is told and decides. Detection uses the context's
page event rather than comparing tab counts, because the popup registers a moment after the
click resolves and a count check races it.
CAPTCHAs and human handoff
The server detects human-verification challenges and stops. It does not solve or bypass them — automated attempts fail and get the profile flagged.
Every navigation, click and snapshot is checked for reCAPTCHA, hCaptcha, Cloudflare Turnstile and its interstitial, Arkose/FunCaptcha, DataDome, PerimeterX, GeeTest, and generic "verify you are human" text. When one is found the tool result carries an explicit instruction:
⚠️ HUMAN VERIFICATION DETECTED: reCAPTCHA (widget present and visible on the page).
Do NOT try to solve, click or type your way through this … Call `browser_await_human` …
browser_await_human brings the window to the front, shows the operator your reason, and blocks
until the challenge clears — then returns a fresh snapshot so the run continues. It also covers
login walls, 2FA/OTP prompts, SMS codes and payment steps.
{ "profileId": 123, "reason": "Solve the reCAPTCHA on the login page", "timeoutSeconds": 300 }
Two things make this reliable with weaker models. The instruction is embedded in the tool output, where a model that has just walked into a captcha will actually read it, rather than only in a description it saw once. And on timeout the tool says "ask the operator, do not loop" instead of returning something retryable.
Where the host supports MCP elicitation, the operator gets a real prompt and confirms when
done. Where it does not — most harnesses today — the tool falls back to polling the page, so the
behavior is the same either way. until accepts challengeGone (default), urlChanged,
textPresent, textGone, or manual.
Handoff requires a headed profile; the tool refuses immediately on a headless one rather than blocking on a window nobody can see.
Connecting your own client instead
const url = `ws://127.0.0.1:${port}${wsEndpoint}`;
await chromium.connectOverCDP(url); // Playwright
await puppeteer.connect({ browserWSEndpoint: url }); // Puppeteer
Both fields are needed. The OpenAPI document describes wsEndpoint as a full ws:// URL, but a
live build returns a bare path — see below.
The MCP resource dolphin://guide/automation carries the full workflow, including hand-tuning a
fingerprint instead of letting create_profile do it.
Fingerprints
Dolphin does not generate fingerprints server-side, and an incoherent one defeats the point of
an anti-detect browser. create_profile therefore pulls a real fingerprint from Dolphin's dataset
and attaches it by default. Pass your own fingerprint object (from get_fingerprint) to
override, or autoFingerprint: false to send none.
One asymmetry worth knowing: create_profiles_bulk ignores a nested fingerprint object.
Bulk creation needs the fields flattened to the top level (useragent, uaFullVersion,
screenWidth, cpu, webglInfo, …). The tool description repeats this.
Tools
| Group | Tools |
|---|---|
| Profiles | list_profiles list_profiles_cursor get_profile create_profile create_profiles_bulk update_profile delete_profiles transfer_profiles share_profile_access |
| Lifecycle | start_profile start_temporary_profile stop_profile local_api_login |
| Proxies | list_proxies create_proxy update_proxy delete_proxy |
| Folders | list_folders create_folder update_folder delete_folder get_folder_profile_ids move_profiles_to_folder reorder_folders |
| Statuses | list_statuses create_status update_status delete_statuses assign_status |
| Fingerprints | get_fingerprint get_useragent list_webgl list_fonts |
| Cookies | export_cookies import_cookies export_cookies_local import_cookies_local run_cookie_robot stop_cookie_robot |
| Local storage | export_local_storage import_local_storage |
| Extensions | list_extensions add_extension delete_extensions |
| Homepages | list_homepages create_homepages update_homepage delete_homepages |
| Bookmarks | list_bookmarks create_bookmark update_bookmark delete_bookmarks |
| Team | list_team_users create_team_user update_team_user delete_team_user |
| Browser | browser_await_human browser_connect browser_disconnect browser_navigate browser_snapshot browser_screenshot browser_click browser_type browser_select_option browser_press_key browser_scroll browser_wait_for browser_evaluate browser_tabs |
| Escape hatch | dolphin_request |
Deletes and transfers carry destructiveHint annotations, so clients that gate on those will
prompt before running them.
Some tools collapse several endpoints where the API split them arbitrarily: delete_profiles
routes single-id deletes through the per-profile endpoint (the only one that accepts a profile
password), move_profiles_to_folder attaches or detaches depending on whether you pass a
folderId, and assign_status either references an existing status or creates one inline.
Error handling
HTTP status codes get translated into something actionable rather than surfaced as bare numbers:
| Code | Meaning |
|---|---|
| 401 | JWT missing or expired |
| 402 | Paid-plan feature — automation, cookie robot, remote cookies |
| 429 | 500 req/min rate limit, or too many concurrent launches |
| 499 | Free plan: three profiles already running |
Rate-limit headers are read on every response; when fewer than 25 requests remain in the current
minute, the tool result says so. Connection failures against the Local API explain the desktop-app
and port requirements instead of returning a bare ECONNREFUSED.
Responses are capped at 60k characters — a page of 100 profiles carries a full fingerprint each, which would otherwise flood the context window.
Not covered
POST /extensions/upload-zipped— multipart file upload.dolphin_requestsends JSON only, so this endpoint is genuinely unreachable through this server. Use the app or a direct HTTP call.getFolder,getBrowserProfileStatus, the single-profileshareAccessToBrowserProfile, and the multi-profile local-storage export are reachable viadolphin_request— they were left out because a dedicated tool would duplicate one that already exists.
Real-world run
node examples/bot-detection-check.mjs
Drives a real Anty profile through bot.sannysoft.com and then a live DuckDuckGo search, using only MCP tools. Result: 58 detection tests, 0 failures or warnings.
✅ WebDriver (New) missing (passed)
✅ WebDriver Advanced passed
✅ Chrome (New) present (passed)
✅ Permissions (New) prompt
✅ Plugins Length (Old) 5
✅ HEADCHR_UA / _CHROME_OBJ / _PERMISSIONS / _PLUGINS ok
✅ WebGL Renderer ANGLE (Intel, Intel(R) Iris(R) Xe Graphics …)
The interactive half found the search box by role from a snapshot, typed, submitted, and read the
results back — which included API - Dolphin {Anty}. So CDP automation through this server does
not break the profile's anti-detect properties.
Findings from a live build
Measured against Dolphin{anty} running Anty 150 on Windows 11, using temporary profiles.
wsEndpoint is a path, not a URL. The OpenAPI document says "Full ws:// URL" and gives
ws://127.0.0.1:38927/devtools/browser/abc123 as its example. The actual response is
{"port": 59618, "wsEndpoint": "/devtools/browser/8e0a4ca0-…"} — a bare path, matching Dolphin's
own automation blog post rather than its spec. This server accepts either shape.
A nested fingerprint object produces a broken profile. The docs say you can pass the whole
fingerprint to POST /browser_profiles and "the backend flattens its derived fields for you". It
does not. The profile is created, but GET /browser_profiles/{id} and the Local API's start
endpoint both then return HTTP 500. create_profile flattens it here instead, into the
{mode, value} shape a profile actually stores:
useragent {"mode":"manual","value":"Mozilla/5.0 …"}
cpu {"mode":"manual","value":8}
webglInfo {"mode":"manual","vendor":"Google Inc. (NVIDIA)","renderer":"ANGLE (NVIDIA, …)"}
A profile without spoofing-mode objects cannot launch. Create accepts a body with no webrtc,
canvas, webgl, clientRect, timezone, locale, ports or geolocation and stores them as
null. The profile reads back fine and then fails to start with HTTP 500. create_profile now
sends the same defaults a UI-built profile carries (PROFILE_MODE_DEFAULTS in
src/fingerprint.ts).
browser_version is required on /fingerprints/fingerprint, and there is no fallback. The
spec marks it optional and says omitting it (or passing a version above stable) falls back to an
internal default. Neither is true: omitting it returns HTTP 422 validation.required, and a
too-high value returns HTTP 200 with an empty object. create_profile's default path was broken by
this. It now discovers the dataset's current maximum by binary search and caches it
(src/fingerprint-version.ts) rather than hard-coding a version that
would rot — requesting a year-old Chrome is itself a fingerprinting signal. At the time of writing
the dataset covers up to Chrome 150.
fonts silently does not persist. Sending a 214-entry fonts array stores []. Pairing that
with fontsMode: "manual" yields a browser reporting zero fonts — a louder signal than not
spoofing at all. Fonts are therefore left at Dolphin's auto, which is also what UI-built profiles
use.
Headless leaks HeadlessChrome — but only on temporary profiles.
| profile | headed | headless: true |
|---|---|---|
| temporary | Chrome/150.0.0.0 |
HeadlessChrome/150.0.0.0 ⚠️ |
saved (via create_profile) |
Chrome/138.0.0.0 |
Chrome/138.0.0.0 ✅ |
Temporary profiles use default fingerprint preferences and carry no user-agent of their own, so
the headless token shows through. A saved profile's manual user-agent overrides it — verified
byte-identical in both modes. start_temporary_profile warns when headless is set;
start_profile does not, because it does not need to.
navigator.webdriver is false and no cdc_/$cdc_ CDP artifacts are injected in either mode.
page.setContent fails on Anty's default page with a TrustedHTML error — its internal pages
enforce Trusted Types. Navigate to a real URL instead. No tool here uses setContent.
Detaching does not kill the browser. browser.close() on a CDP-attached connection releases
the client while Dolphin keeps the process alive, so browser_disconnect is safe and
stop_profile remains the only thing that ends a session and syncs its data.
Tests
npm run build
npm test # everything below
npm run smoke # mock Local API, no app required
npm run e2e # real Dolphin{anty}; skips itself if the app is not running
smoke runs the built server over stdio against a mock and asserts on the tool surface, routing,
query serialization, error translation and input validation.
e2e starts a real temporary profile through the Local API and drives the actual Anty browser:
navigate, snapshot, type, select, click, evaluate (page and element scope), scroll, tabs,
screenshot, ambiguous-target error handling, and a real request to example.com. The fixture page
is served locally so assertions are deterministic, but the browser under test is genuinely the one
Dolphin launched.
test/challenge.e2e.test.js covers the handoff path and tab handling: each vendor's widget markup is detected,
an ordinary page does not false-positive, browser_await_human returns as soon as a challenge
clears (~6s, not the full timeout), it times out without looping, and a live Google reCAPTCHA
demo page is recognised. It never attempts to solve one.
test/profile-lookup.e2e.test.js pins the name-resolution behavior against a live account:
duplicate names are refused, the refusal is overridable, start_profile resolves a name to the
existing id, and an unknown name fails without creating anything.
33 tests total, all passing. Run them serially — npm test sets --test-concurrency=1, because
the two e2e files each launch a browser and collide over profile data directories in parallel.
Both API halves are now verified against a live account:
node examples/remote-check.mjs # read-only sweep of the cloud endpoints
node examples/fingerprint-verify.mjs # create → launch → assert fingerprint → clean up
remote-check confirms list_profiles, list_proxies, list_folders, list_statuses,
get_fingerprint, get_useragent, list_fonts and list_team_users against real data.
fingerprint-verify creates a profile from a dataset fingerprint, launches it headless and headed,
and asserts the spoof actually applied — user-agent, core count, memory, screen resolution and
WebGL renderer all matched the requested values exactly. It deletes the profile it created.
Still unverified: cookie import/export, local storage, homepages, bookmarks, extensions, folder
and team mutations. dolphin_request is the workaround if one of those has a field mismatch.
Local API session
Starting a saved profile needs the desktop app to hold a valid session. If start_profile
returns HTTP 500 while start_temporary_profile works, run local_api_login — it stores your JWT
in the app. Note that GET /v1.0/browser_profiles returning invalid session token is not a
signal of this; that endpoint uses the app's own internal session and 401s regardless.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。