Browser MCP
Give your AI agent a real browser it drives through residential proxies — and let the agent choose the exit country and city, the tier, and one sticky identity per task, all at call time. The browser (Playwright Chromium) runs locally on your machine; you bring your own proxy credentials. It defaults to Aethyn and works with any HTTP proxy.
Most browser tools fix the proxy once, at server startup — one static exit for the whole session. The Aethyn Browser MCP exposes geo and sticky identity as tools instead: the agent can open a Japanese exit for one task and a German exit for the next, pin an exit IP for the length of a task, and verify with aethyn_check_exit_ip that the exit actually landed where it asked before trusting a localized page. It speaks the Model Context Protocol over stdio, so any MCP client (Claude Desktop, Cursor, and others) can load it.
Install & configure
The server ships on npm as aethyn-browser-mcp and runs through npx— no global install needed. Add it to your MCP client's config: Claude Desktop's claude_desktop_config.json or Cursor's ~/.cursor/mcp.json.
{
"mcpServers": {
"aethyn-browser": {
"command": "npx",
"args": ["-y", "aethyn-browser-mcp"],
"env": {
"AETHYN_USERNAME": "aethyn-XXXXX",
"AETHYN_PASSWORD": "your-proxy-password",
"AETHYN_DEFAULT_TIER": "premium"
}
}
}
}Your credentials come from the Aethyn dashboard and stay in your MCP config on your machine — they are never sent anywhere but the proxy, and the proxy password is redacted from every error the server returns. Restart your MCP client after editing the config so it picks up the new server.
npx playwright install chromium.Tiers & ports
Aethyn exposes two residential tiers, each on its own HTTP port. Premium routes by country; Elite adds city and state targeting.
Premium proxy.aethyn.io:2099 country targeting Elite proxy.aethyn.io:5499 country + city + state targeting
Set AETHYN_DEFAULT_TIER to match your plan. The agent can also override it per call with tier: "elite" when a specific task needs city-level exits.
AETHYN_DEFAULT_TIER defaults to premium (port 2099). If your account is Elite, set it to elite (port 5499) — otherwise traffic hits the Premium port and the proxy rejects auth with a 407, even though the browser launched fine. City and state targeting are Elite-only; asking for a city on Premium is refused.Tool reference
Ten tools, all prefixed aethyn_. Every session is one sticky identity: a pinned exit IP for the whole task. Launch returns a session_id that the other tools take as their first argument.
aethyn_launch_browser Launch local Chromium through a residential exit.
country (required, 2-letter ISO) - city? / state? (Elite only)
- tier? (premium|elite, default premium)
- session? (your task id; reuse to pin the same exit IP;
letters/digits/underscore only) - lifetime_min? (1-1440,
default 10) - headless?
-> { session_id, country, tier, sticky:true, lifetime_min }
aethyn_navigate Go to a URL and wait for load.
session_id, url
-> { status, final_url, title }
aethyn_get_content Return the current page cleaned for reading.
session_id, format? (text|html|markdown, default markdown)
-> { url, format, content }
aethyn_snapshot Accessibility tree with [ref=eNN] handles to act on.
session_id
-> { url, snapshot }
aethyn_click Click an element by ref (from snapshot) or a selector.
session_id, ref? (e.g. 'e12') | selector? (CSS/role=/text=)
-> { ok, url }
aethyn_type Type into an input; optionally press Enter.
session_id, text, ref? | selector?, submit?
-> { ok, url }
aethyn_check_exit_ip Fetch IP info THROUGH the session proxy to verify the geo.
session_id
-> { ip, country, city, region, org, is_residential }
aethyn_new_identity Rotate to a fresh exit IP (same country) and clear cookies.
session_id
-> { session_id, rotated:true }
aethyn_close Close the session's browser context and free memory.
session_id
-> { closed:true }
aethyn_list_countries Discover available exit countries (and Elite cities) at runtime.
with_cities?
-> { count, countries:[{ code, name, cities? }] }aethyn_check_exit_ip returns an is_residentialflag derived from the exit's ASN/org string. It is a heuristic, not a guarantee — treat it as a signal, not a contract.Example: regional price index — discover exits, then loop countries into one table
This is the pattern to reach for when you need the same public datapoint (say, a localized price) read from several countries and rolled up into a comparison table or chart — without hardcoding which exits exist. The agent discovers the geo options at runtime, opens one sticky session per country, verifies each exit before trusting the page, and aggregates the results itself.
The natural-language ask that drives it:
Build me a regional price table for the Nova Buds Pro. First check which countries you can exit from, then read the localized public price on https://shop.novaaudio.com/p/nova-buds-pro from the US, UK, Germany, Japan, and Brazil. For each one, verify the browser actually exited in that country before trusting the page, then give me a table of price + currency by country so I can chart the spread.
The agent runs a discovery call, then repeats a small five-call block per country:
aethyn_list_countries { "with_cities": false }
# Discover geo options at RUNTIME instead of hardcoding a list. Confirm
# us/gb/de/jp/br are present, then drive the loop off these codes. Leave
# with_cities false — country-level exits work on the default Premium tier.
--- iteration 1: US ---
aethyn_launch_browser { "country": "us", "session": "priceidx_us",
"tier": "premium", "lifetime_min": 5, "headless": true }
# One session per country = one clean sticky identity, so geo never bleeds
# between exits. Session id is letters/digits/underscore only (no dashes).
aethyn_check_exit_ip { "session_id": "priceidx_us" }
# Verify BEFORE you trust the localized page. Assert country === 'US'. If it
# does not match, aethyn_new_identity and re-check rather than logging a
# wrong-geo price. is_residential is a best-effort ASN heuristic.
aethyn_navigate { "session_id": "priceidx_us",
"url": "https://shop.novaaudio.com/p/nova-buds-pro" }
# Public product page only. Check status is 200 and final_url did not bounce
# to a region gate before reading.
aethyn_get_content { "session_id": "priceidx_us", "format": "text" }
# Extract the one datapoint: displayed price + currency. Record
# { country:'US', price, currency } into your running aggregate.
aethyn_close { "session_id": "priceidx_us" }
# Free the context as soon as this country's datapoint is captured — keeps
# memory flat across a long loop.
--- iteration 2: GB (shown in full to make the pattern explicit) ---
aethyn_launch_browser { "country": "gb", "session": "priceidx_gb",
"tier": "premium", "lifetime_min": 5, "headless": true }
aethyn_check_exit_ip { "session_id": "priceidx_gb" } # assert country === 'GB'
aethyn_navigate { "session_id": "priceidx_gb",
"url": "https://shop.novaaudio.com/p/nova-buds-pro" }
aethyn_get_content { "session_id": "priceidx_gb", "format": "text" }
aethyn_close { "session_id": "priceidx_gb" }
--- iterations 3-5: de / jp / br repeat the exact same five-call block, ---
--- each with its own session id (priceidx_de / priceidx_jp / priceidx_br). ---
--- After the loop, the agent aggregates the collected rows into the final ---
--- table — that roll-up is agent-side synthesis, no MCP call. ---The result is a single comparison table the agent assembled from five verified per-country reads, ready to feed a chart. The exit IPs below are illustrative documentation ranges, not real allocations:
| Country | Verified exit (check_exit_ip) | is_residential | Price | Currency | |---------|-------------------------------|--------------------|---------|----------| | US | US - 198.51.100.24 | true (best-effort) | 149.00 | USD | | GB | GB - 203.0.113.61 | true (best-effort) | 129.00 | GBP | | DE | DE - 192.0.2.140 | true (best-effort) | 149.00 | EUR | | JP | JP - 198.51.100.203 | true (best-effort) | 21,800 | JPY | | BR | BR - 203.0.113.9 | true (best-effort) | 899.00 | BRL | Every row was confirmed to have landed in the requested country before its price was recorded; any exit that missed geo was rotated (aethyn_new_identity) and re-verified rather than logged wrong. This is public pricing read under the site's ToS and robots.txt.
The takeaway: aethyn_list_countriesplus a per-country sticky session with a verify-exit-IP gate turns "pick a geo at call time" into a repeatable, trustworthy pipeline that aggregates public datapoints across markets.
More examples
Seven more worked flows, each the same shape: the natural-language ask an agent receives, the compact tool sequence it runs, and the one idea to carry away. They all lean on the same primitives — a sticky session per identity, aethyn_check_exit_ip as a trust gate before reading, and public-data-only guardrails.
Example: compare a public price across US / UK / Germany
Storefronts localize prices by the visitor's exit IP, so read the same public listing from one sticky session per country. Verify each exit with aethyn_check_exit_ip before trusting the page, then close every session.
The prompt an agent receives:
Compare the public listing price of https://shop.example.com/p/aurora-anc-headphones as shown to shoppers in the US, the UK, and Germany. Open one premium proxied session per country, verify each exit IP landed in the right country, read the displayed price and currency, then close every session. Give me a markdown table of country to price. Public data only: no login, no CAPTCHAs, respect robots.txt and the site's terms.
Tool sequence:
aethyn_list_countries { "with_cities": false }
# Optional: confirm us, gb, de are offered before you launch.
aethyn_launch_browser { "country": "us", "session": "price_us",
"tier": "premium", "lifetime_min": 10 }
# session pins ONE exit IP for this whole read; premium covers country-level geo.
aethyn_check_exit_ip { "session_id": "price_us" }
# Assert country == US before trusting the page. is_residential is best-effort.
aethyn_navigate { "session_id": "price_us",
"url": "https://shop.example.com/p/aurora-anc-headphones" }
aethyn_get_content { "session_id": "price_us", "format": "markdown" }
# Pull the displayed price + currency, e.g. $129.00.
aethyn_close { "session_id": "price_us" }
# Repeat the exact same five-call block for gb (price_gb) and de (price_de),
# each with its own session id. EU locales use decimal-comma pricing
# (139,00 EUR), so normalize before comparing. The agent assembles the final
# table itself — there is no aggregate call.The takeaway: one country equals one sticky session, plus an aethyn_check_exit_ip gate right after launch — that is what makes a price-by-country table trustworthy. Premium covers country-level geo; normalize EU decimal-comma pricing before you compare.
Example: verify the exit landed in Japan before you trust the page
Any geo-sensitive read where a wrong-country or datacenter exit would silently poison your results. Use aethyn_check_exit_ip as a trust gate: aethyn_launch_browser tells you what you asked for, the check tells you what you actually got.
The prompt an agent receives:
Before reading anything, confirm my session is really exiting in Japan — the right country AND a residential ISP, not a datacenter or a leaked home-country IP. Launch a Japan session, verify the exit with aethyn_check_exit_ip FIRST, and only navigate once the geo checks out. If it did not land JP-residential, rotate to a fresh Japan exit and re-verify. Then load the public JP page and read it. Public data only.
Tool sequence:
aethyn_launch_browser { "country": "jp", "session": "jp_geo_qa",
"tier": "premium", "lifetime_min": 15 }
# country is lowercase ISO alpha-2. Premium is country-level; city/state are
# Elite-only, so do not pass them here.
aethyn_check_exit_ip { "session_id": "jp_geo_qa" }
# THE GATE — call this before any navigate. Assert country matches jp
# case-insensitively (this endpoint reports uppercase, e.g. JP), the org is a
# JP residential ISP, and is_residential is true. If any fails, rotate.
aethyn_new_identity { "session_id": "jp_geo_qa" }
# CONDITIONAL — only if the gate failed. Fresh exit, SAME country, cookies
# cleared; then re-run check_exit_ip. Never trust the page until it verifies.
aethyn_navigate { "session_id": "jp_geo_qa",
"url": "https://guide.example.jp/en/" }
# Reached only AFTER the exit verifies JP-residential.
aethyn_get_content { "session_id": "jp_geo_qa", "format": "markdown" }
aethyn_close { "session_id": "jp_geo_qa" }The takeaway: treat aethyn_check_exit_ip as a trust gate, not a formality — verify country and a residential ASN before the first navigate, compare country codes case-insensitively, and rotate with aethyn_new_identity if the exit missed.
Example: compare localized search results across two countries
See how a public search or listing page renders per country — currency, language, ranking, region-only items — with an apples-to-apples diff. One sticky session per country keeps the two identities' IPs and cookies isolated, so exit geo is the only variable.
The prompt an agent receives:
Compare what a public product-listing search shows to shoppers in Germany vs. Japan for the same query. Target: https://www.example-shop.com/search?q=wireless+earbuds — a public, robots-permitted listing that localizes currency, language, and availability by region (no login). For each country: launch a sticky session there, verify the exit IP landed in the right country, navigate, and pull the results as markdown. Then diff the two and tell me how currency, top listings, ranking, and region-only items differ.
Tool sequence:
aethyn_launch_browser { "country": "de", "session": "serp_de", "lifetime_min": 15 }
# tier omitted -> premium, all you need for country-only targeting.
aethyn_check_exit_ip { "session_id": "serp_de" }
# Confirm country == DE before trusting any localization.
aethyn_navigate { "session_id": "serp_de",
"url": "https://www.example-shop.com/search?q=wireless+earbuds" }
aethyn_get_content { "session_id": "serp_de", "format": "markdown" }
# Save the DE snapshot.
aethyn_launch_browser { "country": "jp", "session": "serp_jp", "lifetime_min": 15 }
# A second, independent session — a different id = a different pinned identity.
aethyn_check_exit_ip { "session_id": "serp_jp" }
aethyn_navigate { "session_id": "serp_jp",
"url": "https://www.example-shop.com/search?q=wireless+earbuds" }
# Same URL, so the only variable is exit geo.
aethyn_get_content { "session_id": "serp_jp", "format": "markdown" }
# Save the JP snapshot, then diff the two markdown bodies.
aethyn_close { "session_id": "serp_de" }
aethyn_close { "session_id": "serp_jp" }The takeaway: distinct session ids keep the two identities fully isolated, so the only difference between the two markdown snapshots is exit geo — always verify each exit with aethyn_check_exit_ip before reading.
Example: localized QA — does a .co.jp storefront render yen and Japanese?
When your own site localizes by visitor geo — currency, language, tax display, banners — verify from inside the target country that the right locale actually renders, with no VPN juggling. The exit IP is the input to the site's localization, so it is part of the test.
The prompt an agent receives:
QA our Japanese storefront the way a real shopper in Japan would see it. Launch through a Japanese residential exit, confirm the exit IP is genuinely in Japan, load https://www.example.co.jp/, read the rendered page, and give me PASS/FAIL: prices must be in yen (¥ / 円 / JPY) and the UI copy must be Japanese. Quote the exact currency symbol and a language snippet, and flag it if the page falls back to USD or English.
Tool sequence:
aethyn_launch_browser { "country": "jp", "session": "localized_qa_jp",
"tier": "premium", "lifetime_min": 15 }
# Country-level is all locale QA needs. One session id = one sticky exit, so
# you inspect exactly the render a single Japanese visitor gets.
aethyn_check_exit_ip { "session_id": "localized_qa_jp" }
# Do this BEFORE trusting the page. If the exit did not land in JP the locale
# test is invalid — a wrong-exit artifact, not a bug.
aethyn_navigate { "session_id": "localized_qa_jp",
"url": "https://www.example.co.jp/" }
# Expect 200. If final_url redirects to /us/ or ?lang=en, that redirect is
# itself the finding.
aethyn_get_content { "session_id": "localized_qa_jp", "format": "markdown" }
# Apply the PASS test: Japanese script AND yen pricing. FAIL on English + $/USD.
aethyn_close { "session_id": "localized_qa_jp" }The takeaway: the exit country is the input to the site's localization, so always aethyn_check_exit_ipbefore reading — otherwise you misattribute a wrong-exit to a locale bug — and let the page's own currency symbol and language be the evidence.
Example: hold one identity across a public catalog's pagination
Walk a multi-page public listing as one consistent visitor instead of a new IP per page. Pin a single sticky session with a lifetime_min long enough to cover the whole run, then thread the returned session_id through every call.
The prompt an agent receives:
Crawl the public book catalog at https://catalog.example.com starting at page 1 and follow the "next" link page by page, as ONE consistent visitor, and give me the listing text from each page. Pin a single residential identity for the whole run so the site sees one user, not a fresh IP per page. Stop when there is no "next" link left, then close the session.
Tool sequence:
aethyn_launch_browser { "country": "us", "session": "catalog_crawl_01",
"tier": "premium", "lifetime_min": 30 }
# Pin ONE identity for the whole crawl. Size lifetime_min (minutes) to outlast
# the run — there is no ttl arg. Use the returned session_id everywhere.
aethyn_check_exit_ip { "session_id": "catalog_crawl_01" }
# This identity should NOT change mid-crawl.
aethyn_navigate { "session_id": "catalog_crawl_01",
"url": "https://catalog.example.com/page-1.html" }
aethyn_get_content { "session_id": "catalog_crawl_01", "format": "markdown" }
# Page-1 capture.
aethyn_snapshot { "session_id": "catalog_crawl_01" }
# Find the 'next' control's [ref=eNN]. Refs are valid only for THIS page —
# re-snapshot each new page; never reuse a stale ref.
aethyn_click { "session_id": "catalog_crawl_01", "ref": "e42" }
# Advance on the SAME session so the pinned IP carries over. Do NOT relaunch
# or call aethyn_new_identity per page — either hands the site a new IP.
aethyn_get_content { "session_id": "catalog_crawl_01", "format": "markdown" }
# LOOP pages 3..N: snapshot -> read fresh 'next' ref -> click -> get_content.
# Stop when a snapshot has no 'next' ref.
aethyn_close { "session_id": "catalog_crawl_01" }The takeaway: stickiness is one session id plus a lifetime_min big enough to outlast the crawl; relaunching per page or rotating identity mid-run swaps the IP and defeats the point, and snapshot refs are per-page — re-snapshot before each click.
Example: rotate to a fresh same-country IP on a soft-block
When a public scrape returns a thin or "unusual traffic" interstitial, the MCP will not flag it — aethyn_navigate can return HTTP 200 on a soft-block. The agent reads the content, judges it a block, calls aethyn_new_identity on the same session, and retries. Never use this to grind a CAPTCHA or login wall.
The prompt an agent receives:
Collect the public product names and list prices from
https://shop.example.de/catalog/keyboards using a German residential exit.
After the page loads, read it and judge: if it comes back thin or looks like a
soft-block interstitial ("unusual traffic", "please try again") instead of the
real catalog, rotate to a fresh German IP and retry — up to twice — before
giving up. Public data only. If you hit a CAPTCHA or a login wall, stop and
report it rather than trying to get past it.Tool sequence:
aethyn_launch_browser { "country": "de", "session": "de_public_catalog",
"tier": "premium", "lifetime_min": 15, "headless": true }
aethyn_navigate { "session_id": "de_public_catalog",
"url": "https://shop.example.de/catalog/keyboards" }
# Returns status 200 with title "Please try again" — status alone is no verdict.
aethyn_get_content { "session_id": "de_public_catalog", "format": "markdown" }
# THE AGENT decides: an "unusual activity" interstitial with zero listings ->
# soft-block. (A CAPTCHA or login form instead -> STOP and report, per AUP.)
aethyn_new_identity { "session_id": "de_public_catalog" }
# Fresh exit IP, SAME country, cookies cleared, same session_id.
aethyn_check_exit_ip { "session_id": "de_public_catalog" }
# Recommended: confirm the new exit still landed in DE before retrying.
aethyn_navigate { "session_id": "de_public_catalog",
"url": "https://shop.example.de/catalog/keyboards" }
aethyn_get_content { "session_id": "de_public_catalog", "format": "markdown" }
# Real catalog this time. If still thin, loop back to new_identity — but cap
# rotations (here, twice) and give up gracefully rather than hammer the site.
aethyn_close { "session_id": "de_public_catalog" }The takeaway: the MCP never auto-detects blocks — a 200 is not a verdict, so read the content and let the agent decide, then aethyn_new_identity for a fresh same-country exit; bound your retries and stop-and-report on CAPTCHAs or login walls.
Example: Elite city targeting — read a NYC-localized listing from a New York exit
When a public page renders different content per city — "near you" listings, deals, events — you need to see exactly what a visitor in a specific city sees. City and state targeting is Elite-tier only, so this is the pattern whenever country alone is not precise enough.
The prompt an agent receives:
Check what a New York City visitor sees on a public, IP-localized listings page (swap in your real URL). Route through a US / New York City residential exit on the Elite tier — only Elite can target a city — confirm the exit actually landed in New York before reading anything, then extract the localized listings. Public pages only: no logins, no CAPTCHAs.
Tool sequence:
aethyn_list_countries { "with_cities": true }
# Optional: with_cities:true lists countries with known Elite cities, so you
# can confirm us and pick a city token the server recognizes.
aethyn_launch_browser { "country": "us", "city": "new york", "tier": "elite",
"session": "nyc_listings_01", "lifetime_min": 15 }
# city/state targeting is Elite-ONLY — omit tier:elite and the launch is
# rejected. session id is letters/digits/underscore only; reuse it to pin the IP.
aethyn_check_exit_ip { "session_id": "nyc_listings_01" }
# Verify FIRST: country US, city/region New York. If it landed in another
# metro, aethyn_new_identity and re-check. is_residential is best-effort.
aethyn_navigate { "session_id": "nyc_listings_01",
"url": "https://www.example-listings.com/near-me" }
# The site geo-detects the NY exit and renders the NYC variant.
aethyn_get_content { "session_id": "nyc_listings_01", "format": "markdown" }
# Confirm listings reference New York — proof city-level targeting drove the
# content, not just the country. (Optional: aethyn_snapshot + aethyn_type a ZIP
# into a public location field if the page exposes one — never a login.)
aethyn_close { "session_id": "nyc_listings_01" }The takeaway: city and state targeting is Elite-tier only — pass tier: "elite" with the city or the launch is rejected — and always verify the exit with aethyn_check_exit_ip before trusting the page, since the whole result hinges on landing in the right city.
Bring your own proxy
Aethyn is the default, but any HTTP proxy works. Point PROXY_HOST at your provider and describe its username format with a template. The placeholders {username}, {country}, {city}, {state}, {session}, and {lifetime}are filled from the agent's call, and any [ ... ] segment is dropped when its placeholder is empty (so city and state only appear when supplied).
{
"mcpServers": {
"browser": {
"command": "npx",
"args": ["-y", "aethyn-browser-mcp"],
"env": {
"PROXY_HOST": "gate.your-provider.com",
"PROXY_PORT": "7000",
"PROXY_USERNAME": "your-user",
"PROXY_PASSWORD": "your-pass",
"PROXY_USERNAME_TEMPLATE": "{username}-country-{country}[-city-{city}]-session-{session}-lifetime-{lifetime}"
}
}
}
}For a plain fixed proxy with no geo in the username, set PROXY_USERNAME_TEMPLATE to {username}. Note that the sticky-lifetime token is always lifetime (minutes) — never ttl. A custom provider uses the same port for both tiers and does not gate city/state to Elite; that gating is an Aethyn-specific behaviour.
Guardrails
This tool is for collecting public data. It has no capability to defeat access controls, and it should be used within the law and each site's terms:
Respect robots.txt, rate limits, and each site's terms — being able to reach a page does not mean you should scrape it. Pace requests; a residential IP firing dozens of requests per second is still obviously a bot.
Related & where to get it
Install & source: npm (aethyn-browser-mcp) · GitHub source · Glama · mcp.so.
Get credentials with the free trial (no card required): Create an Aethyn account. Then see the Quickstart, the full Targeting reference for the username grammar, and Authentication for credentials.
Compare the tiers behind the agent's tier argument: Premium residential proxies and Elite residential proxies. For the bigger picture, read the future of proxies in AI/ML integration.