We use cookies to enhance user experience, personalize content, and analyze traffic. Cookie Policy

← Back to all articles

Puppeteer MCP: Proxy Setup for Agent Browser Sessions

Puppeteer MCP proxy setup: put --proxy-server in PUPPETEER_LAUNCH_OPTIONS, add credentials with a local forwarder, and stop the agent from changing it.

by Unknown Proxies

14 min read

October 11, 2026

Puppeteer MCP: Proxy Setup for Agent Browser Sessions

The Puppeteer MCP server (@modelcontextprotocol/server-puppeteer) lets Claude, Cursor, or another MCP client drive Chrome through tools like puppeteer_navigate, puppeteer_click, and puppeteer_evaluate. It has no proxy setting of its own. Everything it passes to puppeteer.launch() comes from one environment variable, PUPPETEER_LAUNCH_OPTIONS, which holds serialized JSON. The proxy goes in as a --proxy-server item inside that object's args list.

Two limits shape the rest of the setup. Chrome won't accept a username and password in --proxy-server, and this server has no way to answer a proxy's 407 challenge, so authenticated proxies need IP allowlisting or a small forwarder on 127.0.0.1. The model can also pass its own launchOptions to puppeteer_navigate. That restarts Chrome with whatever proxy the model chose, including none at all.

Every result below comes from a small Node harness that sent JSON-RPC tool calls to the server's stdin while two local test proxies logged each CONNECT. The browser was the one "headless": true launches by default: full Chrome for Testing 131.0.6778.204, found through Puppeteer's normal cache lookup with no executablePath. The only extra flag was --no-sandbox, which this Linux host needs. The sections cover the env config, the credential workaround, keeping the model from rerouting Chrome, a WebRTC leak that --proxy-server leaves open, and a status-aware exit IP check.

Puppeteer MCP proxy path from the MCP client to the Node server, Chrome, a local credential forwarder, the upstream proxy, and the target site

Which Puppeteer MCP server you are running

Several packages answer to "Puppeteer MCP," and they don't take proxies the same way. Check which one your client config launches before you change anything.

Package Status How it gets a proxy
@modelcontextprotocol/server-puppeteer 2025.5.12 Archived reference server, marked deprecated on npm, still about 24,000 weekly downloads --proxy-server in PUPPETEER_LAUNCH_OPTIONS, or in launchOptions on a tool call
@hisma/server-puppeteer 0.6.5 Community fork of the reference server, updated to Puppeteer 24 Same env var and same tool arguments
puppeteer-mcp-server 0.7.2 Separate project with an extra puppeteer_connect_active_tab tool No launch config at all. Start your own Chrome with --proxy-server and --remote-debugging-port=9222, then let the agent attach

We repeated the proxy override, --no-proxy-server, and 407 tests on the Hisma fork, and it behaved the same as the reference server. The third package hardcodes its launch arguments, including --disable-web-security, so the only way to proxy it is to attach it to a Chrome you started yourself.

The archived repo states that no security guarantees are provided for its servers. If you're choosing fresh, Playwright MCP is actively maintained and accepts a proxy username and password in its config file. If you already rely on Puppeteer MCP's tool set or prompts, the setup below works.

Puppeteer MCP proxy config

For an unauthenticated endpoint, such as an IP-allowlisted proxy or a local forwarder, add the env var to the server entry. This is the same mcpServers shape Claude Desktop, Cursor, and a project .mcp.json use:

{
  "mcpServers": {
    "puppeteer": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-puppeteer"],
      "env": {
        "PUPPETEER_LAUNCH_OPTIONS": "{\"headless\":true,\"args\":[\"--proxy-server=http://127.0.0.1:8000\",\"--webrtc-ip-handling-policy=disable_non_proxied_udp\"]}"
      }
    }
  }
}

In Claude Code, the same server is one command. Single quotes keep the inner JSON intact. Claude Code needs another option, such as --transport stdio, between --env and the server name:

claude mcp add --env 'PUPPETEER_LAUNCH_OPTIONS={"headless":true,"args":["--proxy-server=http://127.0.0.1:8000","--webrtc-ip-handling-policy=disable_non_proxied_udp"]}' --transport stdio puppeteer -- npx -y @modelcontextprotocol/server-puppeteer

Three details in that config are easy to get wrong:

The first npx run installs Puppeteer 23.11.1, which downloads Chrome for Testing 131.0.6778.204 into ~/.cache/puppeteer. If you point executablePath at a different Chrome in the same JSON, the proxy flags work the same way.

Proxy credentials: what fails and what works

If your proxy endpoint expects a login, nothing in the env JSON can carry it. In plain Puppeteer you would call page.authenticate(), as the Puppeteer proxy guide shows. The MCP server never calls it and exposes no tool that can, and puppeteer_evaluate runs inside the page, where Puppeteer's API isn't available.

We pointed the server at a test proxy on port 18931 that rejects any request without a Proxy-Authorization header. Each attempt returned this:

Setting in args What the agent sees What actually happened
--proxy-server=http://user:pass@127.0.0.1:18931 JSON-RPC error -32603: net::ERR_NO_SUPPORTED_PROXIES at https://example.com Chrome rejected the proxy URL before connecting
--proxy-server=socks5://user:pass@127.0.0.1:18931 The same ERR_NO_SUPPORTED_PROXIES error Chrome doesn't support SOCKS5 authentication either
--proxy-server=http://127.0.0.1:18931 (no credentials) JSON-RPC error -32603: net::ERR_INVALID_AUTH_CREDENTIALS at https://example.com The proxy answered 407, and Chrome had nothing to send back
--proxy-server=http://127.0.0.1:8000 (local forwarder) Navigated to https://example.com The forwarder added credentials, and the page loaded with status 200

The third row is a 407 Proxy Authentication Required that Chrome can't answer, and full Chrome turns it into a navigation error. That isn't true of every build. When we ran the same config earlier on chrome-headless-shell 155, the 407 came back as Navigated to https://example.com with an empty page, so an agent on "headless": "shell" may decide the site is blank and move on. Whatever the binary, puppeteer_navigate never reports the status code. A 403 from the site itself also returns Navigated to <url>.

The fix is to make sure Chrome never meets the 407 at all. There are two ways to do that.

Drop the login with IP authentication. Some plans let you register the public IP of the host that runs the MCP server. The proxy then accepts that host without a login, and the env JSON can point straight at http://host:port. Check what curl https://api.ipify.org returns on that host before you register it, and re-register whenever the host's address changes, for example after a VPS migration or on a home connection with a dynamic IP.

Run a local forwarder. proxy-chain is the Node package most Puppeteer users already know. This file listens on 127.0.0.1:8000 (or FORWARDER_PORT) without authentication and forwards every request upstream with your credentials:

// forwarder.mjs
import { Server } from "proxy-chain";

const port = Number(process.env.FORWARDER_PORT ?? 8000);

const server = new Server({
  host: "127.0.0.1",
  port,
  prepareRequestFunction: () => ({ upstreamProxyUrl: process.env.UPSTREAM_PROXY_URL }),
});

await server.listen();
console.log(`Forwarding 127.0.0.1:${port} to the upstream proxy`);
npm install proxy-chain
UPSTREAM_PROXY_URL="http://YOUR_PROXY_USERNAME:YOUR_PROXY_PASSWORD@proxy.example.com:8080" node forwarder.mjs

We ran this with proxy-chain 3.0.1, which needs Node 20.11 or later. The host: "127.0.0.1" line matters. Change it to 0.0.0.0 and anyone who can reach port 8000 gets free use of your paid plan, which is the classic open proxy mistake.

Note where the secret ends up. UPSTREAM_PROXY_URL lives in the shell that starts forwarder.mjs, while the mcpServers entry contains only http://127.0.0.1:8000. That means a .mcp.json you commit, or a Cursor config you sync between machines, carries no credentials. puppeteer_evaluate doesn't expose them either, since its scripts execute in the page's JavaScript context and can't see environment variables from any process.

The agent can change the Puppeteer MCP proxy

The puppeteer_navigate tool schema includes two optional arguments the model can fill in: launchOptions and allowDangerous. When launchOptions differs from the last value, the server closes Chrome and launches a new one with your env config merged with the model's. In the args array, a model-supplied flag replaces any env flag with the same name.

We started the server with --proxy-server=http://127.0.0.1:8000 in the env var and then sent tool calls with different launchOptions. The two test proxies logged which CONNECT requests reached them:

launchOptions sent by the model Result
{"args":["--proxy-server=http://127.0.0.1:18932"]} Chrome restarted, and later pages went through the model's proxy instead of yours
{"args":["--proxy-server=direct://"]} Chrome restarted with no proxy, so pages loaded from the host's real IP
{"args":["--no-proxy-server"]} Chrome restarted with both flags, and --no-proxy-server won, so pages loaded from the host's real IP
{"headless":true} Chrome restarted on your proxy, but cookies set before the restart were gone
{"args":["--no-sandbox"]} Rejected with Dangerous browser arguments detected: --no-sandbox, until the model added "allowDangerous": true to the same call

The --no-proxy-server row needs a closer look. The merge only replaces a flag with the same name, and --no-proxy-server is a different name, so your --proxy-server stays in the list next to it. Full Chrome checks --no-proxy-server first and goes direct. So the model has a second way to bypass your proxy besides direct://, and this one doesn't touch your --proxy-server value at all.

The last row matters because the danger list is the server's only guard, and the model can switch it off. Neither --proxy-server nor --no-proxy-server is on that list in the first place. The Hisma fork appends the model's args instead of replacing matching flags, but Chrome uses the last --proxy-server it sees, so the override still works there.

A model rarely does this unprompted. The risk is a page that tells it to, since anything the agent reads can carry a prompt injection. Three controls, from weakest to strongest:

  1. Read tool approvals. Claude Code and Cursor show the tool's arguments before running it. Don't auto-approve puppeteer_navigate if you rely on the proxy. A launchOptions key in the approval prompt is your cue to deny.
  2. Fix the cookie side. Set userDataDir in your env JSON. A restart then reopens the same profile, and in our test the cookie survived. Without it, every restart starts a fresh temporary profile.
  3. Lock egress below the browser. If Chrome can only reach the forwarder, a model-supplied direct://, --no-proxy-server, or rogue proxy fails to connect. The next section shows how.

How Puppeteer MCP merges PUPPETEER_LAUNCH_OPTIONS with tool-call launchOptions, and what each model-supplied value does to the proxy route

Lock egress for Puppeteer MCP in Docker

The reference server also ships as the mcp/puppeteer Docker image. With DOCKER_CONTAINER=true, it launches headless Chromium with --no-sandbox, --single-process, and --no-zygote added. Put that container on a Docker network created with --internal, which has no route out, and attach a forwarder to both that network and a normal user-defined one:

docker network create --internal agent-net
docker network create egress-net

docker run -d --name agent-egress --network egress-net --restart unless-stopped gogost/gost:3.3.0 \
  -L http://:8080 \
  -F "http://YOUR_PROXY_USERNAME:YOUR_PROXY_PASSWORD@proxy.example.com:8080"

docker network connect agent-net agent-egress

The forwarder is GOST, a 23 MB compressed image, so it doesn't need Node. Don't publish its port with -p, and keep it off the default bridge. It should only be reachable from containers on agent-net and egress-net. Then point the MCP server at it:

{
  "mcpServers": {
    "puppeteer": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm", "--init",
        "--network", "agent-net",
        "-e", "DOCKER_CONTAINER=true",
        "-e", "PUPPETEER_LAUNCH_OPTIONS={\"args\":[\"--proxy-server=http://agent-egress:8080\",\"--webrtc-ip-handling-policy=disable_non_proxied_udp\"]}",
        "mcp/puppeteer"
      ]
    }
  }
}

We checked the network pattern with test containers on the same --internal network. A direct request failed to resolve the hostname, a request to a raw IP failed to connect, and the same request through agent-egress:8080 returned 200 with credentials added upstream. We didn't pull the mcp/puppeteer image itself (484 MB compressed), but Chrome inside it gets the same network, so direct:// or a proxy the model invents has nowhere to go. The internal network has no route out for UDP either, which closes the WebRTC path even if the flag is removed.

Docker egress lock where the Puppeteer MCP container sits on an internal network and can only reach a forwarder container that connects to the upstream proxy

Close the WebRTC leak

--proxy-server covers HTTP, HTTPS, and WebSocket traffic. WebRTC can use UDP, which an HTTP proxy doesn't carry. With the default policy, any page can ask a public STUN server for the browser's address.

We ran a short RTCPeerConnection script through puppeteer_evaluate on a proxied session in Chrome 131 and compared flags:

Flag added to args Result
None A srflx candidate with the host's real public IP, not the proxy's
--force-webrtc-ip-handling-policy=disable_non_proxied_udp The same leak. Full Chrome ignores this switch
--webrtc-ip-handling-policy=disable_non_proxied_udp No candidates at all

The --force- switch does work in chrome-headless-shell, where an earlier run on version 155 returned no candidates with it. Full Chrome ignores it and reads --webrtc-ip-handling-policy instead. That covers the Chrome for Testing build Puppeteer downloads, and the Debian Chromium in mcp/puppeteer should behave the same way, though we didn't run that image. If you switch to "headless": "shell", rerun the script above rather than assuming either flag works. Chrome's WebRtcIPHandling policy documents the same values for managed browsers.

The model can override this flag too, by sending --webrtc-ip-handling-policy=default in launchOptions. The Docker egress lock is the only control here that the model can't undo.

Keep the flag in every config in this guide. It doesn't break normal browsing, and an agent rarely needs WebRTC.

Check the agent's exit IP and response status

Puppeteer MCP has no snapshot tool. The agent reads pages with puppeteer_evaluate or screenshots, so use those for the check. Before involving the agent, confirm the upstream works on its own:

curl -x http://YOUR_PROXY_USERNAME:YOUR_PROXY_PASSWORD@proxy.example.com:8080 https://api.ipify.org

Then give the agent this task:

Use puppeteer_navigate to open https://api.ipify.org?format=json.
Then use puppeteer_evaluate to return document.body.innerText.

The IP should match the curl result. For a geo-targeted proxy, repeat with https://ipinfo.io/json and check the country. Navigated to never includes a status code, so also have the agent check the status after any navigation that looks empty or wrong:

performance.getEntriesByType("navigation")[0].responseStatus

In our tests this returned 200 through the forwarder, 403 for a test URL that answers 403, and 407 when the forwarder was missing. The responseStatus property is available in Chrome 109 and later.

Sessions, profiles, and proxy choice

A Puppeteer MCP server drives one Chrome and one page at a time, so one server equals one network identity. When an agent needs two locations, register two servers and run two forwarders, for example with FORWARDER_PORT=8001 and FORWARDER_PORT=8002:

{
  "mcpServers": {
    "puppeteer-us": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-puppeteer"],
      "env": {
        "PUPPETEER_LAUNCH_OPTIONS": "{\"headless\":true,\"userDataDir\":\"/home/you/.puppeteer-mcp/us\",\"args\":[\"--proxy-server=http://127.0.0.1:8001\",\"--webrtc-ip-handling-policy=disable_non_proxied_udp\"]}"
      }
    },
    "puppeteer-de": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-puppeteer"],
      "env": {
        "PUPPETEER_LAUNCH_OPTIONS": "{\"headless\":true,\"userDataDir\":\"/home/you/.puppeteer-mcp/de\",\"args\":[\"--proxy-server=http://127.0.0.1:8002\",\"--webrtc-ip-handling-policy=disable_non_proxied_udp\"]}"
      }
    }
  }
}

Give each server its own userDataDir and never point two proxies at the same profile. Saved cookies that say "returning visitor" from a new IP are a common trigger for login checks.

Pick the upstream behind each forwarder by how long one identity has to last. Puppeteer MCP keeps a single page open and reuses its connections, so a gateway that hands out a new IP per connection can switch the exit between two clicks of the same task:

The session trade-off is covered in sticky vs rotating proxies, and the reasons agents get blocked at all are in proxies for AI agents. On the Unknown Proxies side, the ISP plans support IP authentication, so a fixed host can point the env JSON at the endpoint and skip forwarder.mjs entirely. The residential proxies dashboard generates sticky credentials per country, one set per forwarder. Residential traffic is billed by volume, and puppeteer_navigate waits for the full load event, so run your page count through the data usage calculator before you size a plan. Chrome for Testing also sent its own background requests through the forwarder in our tests, to clients2.google.com and accounts.google.com, so expect a little traffic per launch that the agent never asked for.

Troubleshooting Puppeteer MCP proxy errors

Errors from page.goto() come back as JSON-RPC errors with code -32603, not as tool results, so some clients show them as a generic tool failure. Expand the error text before you retry.

Symptom Likely cause Fix
net::ERR_NO_SUPPORTED_PROXIES at <url> Credentials inside the --proxy-server URL Remove them, then use IP allowlisting or a forwarder
net::ERR_INVALID_AUTH_CREDENTIALS at <url> The proxy returned 407 and Chrome had no credentials Start the forwarder or register the host's IP
Navigated to <url>, then an empty page On chrome-headless-shell, the same 407 Check responseStatus, then fix the forwarder or allowlist
net::ERR_PROXY_CONNECTION_FAILED at <url> Forwarder not running, or wrong host or port Start the forwarder and test it with curl -x. See ERR_PROXY_CONNECTION_FAILED
Navigation timeout of 30000 ms exceeded The proxy accepted the connection but never answered Test the upstream with curl. The tool has no timeout argument
Agent reports your own IP Malformed env JSON, a model-supplied direct:// or --no-proxy-server, or a stale server process Check stderr for Failed to parse PUPPETEER_LAUNCH_OPTIONS, review recent tool calls, restart the client
Could not find Chrome (ver. 131.0.6778.204) The first-run Chrome download was skipped or used a different cache path Run npx puppeteer browsers install chrome@131.0.6778.204 or set executablePath
Dangerous browser arguments detected An arg such as --no-sandbox without ALLOW_DANGEROUS=true Use the Docker image, or set the env var deliberately
403, 429, or a CAPTCHA with responseStatus from the site The request reached the target, which refused it Reduce the agent's request rate before blaming the route

For Chrome-level proxy errors that aren't specific to MCP, such as ERR_TUNNEL_CONNECTION_FAILED, the Puppeteer proxy error guide goes deeper.

None of this setup grants permission to automate a site. Point the agent only at accounts and data you're entitled to use, respect published rate limits and terms, and keep the reference server's own caution in mind: the browser it starts can open local files and internal addresses on your network.

FAQ

How do I add a proxy to Puppeteer MCP?

Set PUPPETEER_LAUNCH_OPTIONS in the server's env block to a JSON string whose args array includes --proxy-server=http://host:port. Restart the MCP server after changing it, because Chrome takes its proxy at launch.

Can Puppeteer MCP use a proxy with a username and password?

Not directly. Chrome rejects credentials in --proxy-server with ERR_NO_SUPPORTED_PROXIES, and the server never calls page.authenticate(). Register the host's IP with your provider so no login is needed, or point --proxy-server at a loopback forwarder (proxy-chain or GOST) that holds the login and attaches it upstream.

Can the AI agent change the proxy?

Yes. puppeteer_navigate accepts a launchOptions argument. A new --proxy-server value there replaces yours after a browser restart, including direct://, and adding --no-proxy-server sends full Chrome direct even with your flag still present. Deny tool calls that include launchOptions, or run the server on a network that can only reach your forwarder.

Is the Puppeteer MCP server still maintained?

The reference server lives in the archived modelcontextprotocol/servers-archived repo, and npm marks @modelcontextprotocol/server-puppeteer as deprecated. The @hisma/server-puppeteer fork updates it to Puppeteer 24 with the same configuration. Playwright MCP is the actively developed alternative.

Puppeteer MCP or Playwright MCP for proxied agents?

Playwright MCP accepts a proxy username and password in its config file, and its browsing tools can't relaunch the browser with new options. Puppeteer MCP needs a forwarder for credentials and lets the model change launch options. For a new setup that needs authenticated proxies, Playwright MCP needs fewer workarounds. The browser MCP server guide puts both next to Chrome DevTools MCP and the attach-mode servers, with the proxy setting for each.

Final thoughts

A Puppeteer MCP proxy setup is one --proxy-server flag inside PUPPETEER_LAUNCH_OPTIONS, plus the parts that flag doesn't cover. Add credentials with a local forwarder or IP allowlisting. Add the WebRTC flag. Remember that the model can pass its own launchOptions, and lock egress at the network if the route has to hold. Then have the agent load an IP echo page and read responseStatus before you hand it real work.

Technical references: Puppeteer MCP reference server, Puppeteer launch options, Chromium proxy documentation, Docker internal networks, Claude Code MCP documentation, and RFC 9110 on 407 Proxy Authentication Required.

About the Author

Unknown Proxies

Proxy Infrastructure Team

Stay Unknown

High-performance dedicated proxies optimized for speed and reliability. Get uncompromising quality, 99.9% uptime, and unmatched support. Stay Unknown.

Explore Plans