Back to Knowledge Hub
Deployment

Playwright MCP Setup for a Custom Chromium Build

Configure a Playwright MCP server to launch a specific Chromium build with a profile, then prove from inside the session which executable actually started.

BotBrowser Team

Documentation

Want the structured docs for Deployment?

This article lives in the editorial library. For step-by-step setup, reference material, and ongoing updates, jump into the docs section.

A Playwright MCP server sits between an agent client and a browser, and the browser it starts is decided by configuration that is easy to leave uninspected. When the configuration is right, the agent drives the build and profile the operator selected. When it is wrong, the server starts its own bundled Chromium and reports success, so an agent can complete an entire task against a browser nobody intended to use. The configuration keys that carry a custom executable, the readback that proves which binary started and the failure mode where the profile argument is silently dropped determine whether that server runs the intended build. Playwright MCP is a Microsoft project with its own release cycle, and the configuration below is documented against its published schema. BotBrowser supports executablePath and a profile argument from any Playwright-compatible launcher, and it does not ship an MCP server of its own, so a server that never forwards the option cannot be corrected from the browser side.

TL;DR

  • Put the executable in browser.launchOptions.executablePath, not in a top-level key. The MCP configuration schema nests Playwright's LaunchOptions under browser, and an unknown top-level key is ignored rather than rejected.
  • Two other routes exist and are easier to get wrong: the --executable-path CLI flag and the PLAYWRIGHT_MCP_EXECUTABLE_PATH environment variable. Pick one route per deployment so a stale value cannot win.
  • The profile flag belongs in launchOptions.args as a separate array element, because Playwright passes each element through as its own argument.
  • Prove the binary from inside the session: read navigator.userAgent, the process path the server reports, and a page-level marker the custom build exposes. A server that fell back to its bundled Chromium answers all three differently.
  • Keep one MCP server instance per profile. A persistent profile is locked to a single browser instance, so two clients sharing a workspace collide instead of isolating.

A configuration flow from the agent client through the MCP server to the resolved executable path, with a separate branch showing the silent fallback where the server starts its own bundled Chromium when executablePath is not forwarded.

Contents

Where the executable path belongs

The MCP configuration file is a JSON document with a browser object. Inside that object, launchOptions is typed as Playwright's LaunchOptions, which is the same type a direct Playwright script passes to browserType.launch. That is where executablePath lives, and it is the only nesting the server reads.

{
  "browser": {
    "browserName": "chromium",
    "launchOptions": {
      "executablePath": "/opt/botbrowser/chrome",
      "headless": true,
      "args": ["--bot-profile=/etc/botbrowser/profile.enc"]
    },
    "contextOptions": {
      "viewport": { "width": 1280, "height": 800 }
    }
  }
}

Three properties of that block matter more than the rest. browserName stays chromium, because a custom Chromium build still speaks the Chromium protocol and the other values (firefox, webkit) would route to a different driver. headless belongs in launchOptions rather than beside browserName, since the server forwards the whole object to the launch call. args is an array of separate strings, and each element becomes one argument; a single string containing a space does not split.

RouteWhere the value goesWins overTypical mistake
Configuration filebrowser.launchOptions.executablePathNothing else, unless a flag or env var is also setWriting executablePath at the top level of the file
CLI flag--executable-path /path/to/chromeThe configuration fileAssuming the flag also sets browserName
Environment variablePLAYWRIGHT_MCP_EXECUTABLE_PATH=/path/to/chromeThe configuration fileA stale value inherited from a shell profile

The three routes are alternatives, not layers. A deployment that sets the config file and also exports the environment variable has two sources of truth, and whichever the server reads last determines the browser. Pick one and assert the others are unset in the launch environment.

Why an unknown key fails silently

The server validates the shape it knows and ignores what it does not recognise. A configuration that puts executablePath at the top level, or spells it executable_path, or nests it under contextOptions instead of launchOptions, produces a server that starts normally and a browser that is not the intended one. Nothing in the startup log necessarily says so, because from the server's perspective the configuration was valid and the launch succeeded.

That behaviour is why the readback in the next section is not optional. Treating a successful start as evidence of the intended binary is the failure that is easiest to miss, and it fails in the direction that matters: the agent works, the task completes, and the browser was the wrong one.

The context options are separate

contextOptions is a different object and carries BrowserContextOptions, not LaunchOptions. Viewport size, locale, timezone, colour scheme and permissions belong there. Moving executablePath into contextOptions produces an invalid context option that is ignored, and the browser falls back to the bundled build.

The split is worth remembering because the two objects look interchangeable in a flat JSON file. A useful habit is to read the file back before launching and assert that browser.launchOptions.executablePath is a string that exists on disk and is executable, and that browser.contextOptions contains no path-shaped value at all.

Precedence between the three routes

When more than one route sets a path, the result depends on which the server reads last, and that order is not something to guess. The safe pattern is to make the other two impossible: unset PLAYWRIGHT_MCP_EXECUTABLE_PATH in the environment that starts the server, and never pass --executable-path on the command line when the config file already carries the value.

A concrete check costs one command in the launching environment: print the variable and confirm it is empty, then start the server. A shell profile that exports the variable for an unrelated project is a common cause of a server that starts the bundled browser despite a correct config file, because the environment overrides are merged after the configuration file is read, so the inherited value wins.

The same reasoning applies to a wrapper script that assembles the command line. If the wrapper appends --executable-path from a variable that is empty in one environment, the flag can be dropped entirely, leaving the config file as the only source. That is the desirable outcome, and it is worth writing the wrapper so the flag is omitted rather than passed empty.

Pass the profile without dropping it

A profile is not a launch option Playwright knows about. It is a browser argument, so it travels in launchOptions.args as its own array element. A direct Playwright script writes it the same way, which is why the profile is the piece most often lost when a working script is converted into an MCP configuration.

{
  "browser": {
    "browserName": "chromium",
    "launchOptions": {
      "executablePath": "/opt/botbrowser/chrome",
      "args": ["--bot-profile=/etc/botbrowser/profile.enc", "--bot-locale=en-US"]
    }
  }
}

Each argument is one array element with no surrounding shell quoting. "--bot-profile=/etc/botbrowser/profile.enc" and "--bot-profile=/etc/botbrowser/profile.enc --bot-locale=en-US" are not equivalent: the second is a single argument containing a space, and the browser receives it as one malformed flag rather than two.

Where the profile is read from

The profile file has to be readable by the process the server spawns, which is not always the process the operator started. A server launched by a desktop client inherits that client's environment, and a server launched inside a container sees only what was mounted. A path that works in an interactive shell can therefore fail in the MCP launch, and the failure can surface as a browser that starts with defaults rather than as an error.

Resolve the absolute path once, in the same environment that starts the server, and record it beside the configuration. Relative paths are resolved against the server's working directory, which the client chooses and the operator rarely sets deliberately.

One server instance, one profile

A persistent profile can be used by one browser instance at a time. Two MCP clients pointed at the same profile, or a client and a manual browser session sharing the same user data directory, will conflict: one of them either fails to start or attaches to the other's state. The isolation switch changes this by keeping the profile in memory, but it also discards the profile on close, so it is a different mode rather than a fix.

IntentSettingConsequence
One client, one fixed profilePersistent profile plus one server instanceThe profile is locked to that instance
Several clients in parallelIsolated mode or a distinct user data directory per clientNo shared state; the profile does not survive a restart
Reuse an already running browserConnect to an existing endpointThe server does not launch the configured executable at all

The third row is the trap in a custom-build deployment. Connecting to an existing endpoint skips the launch path entirely, so the executablePath in the configuration is never consulted. A team that adds a CDP endpoint to speed up startup can silently lose the custom build without changing any launch option.

Profile state and what a restart loses

The difference between a persistent profile and an isolated one is not only storage. A persistent profile carries cookies, local storage and the cached state a site uses to recognise a returning visitor. An isolated profile starts clean every time the browser closes, so a task that depends on a signed-in session has to inject that state explicitly rather than assume it survives.

For a monitoring or testing workload that is the right tradeoff, because a clean start makes two runs comparable. For a workload that must stay signed in, the persistent profile is required, and the single-instance restriction becomes an operational constraint rather than a nuisance. Recording which mode a run used, beside the profile path, prevents a later comparison between an isolated run and a persistent one from being treated as a behavioural difference.

Prove which binary started

The only reliable check runs inside the session the agent is driving. Three readings together distinguish the intended build from the bundled fallback, and each is cheap.

The first is the user agent string. A custom build may report a different Chromium version than the bundled one, or may report a configured user agent that matches no build at all, so this reading corroborates rather than decides. A bundled Playwright Chromium reports the version that shipped with the installed Playwright package, and the comparison is visible from a single navigator.userAgent read.

The second is the executable path the process reports. Where the build exposes it, read it from the page; where it does not, read it from the server's process list on the host that spawned it. Either way the value must equal the path in the configuration, character for character.

The third is a marker the custom build sets that a stock build does not. That can be an extra property on a browser API object, a version string in a product-specific namespace, or a documented capability flag. The marker is the strongest of the three because it cannot be produced by a coincidentally similar version number.

ReadingIntended build answersBundled fallback answersWhere to read it
navigator.userAgentThe custom build's Chromium versionThe Playwright-bundled versionAny page in the session
Process executable pathThe configured pathA path under the Playwright cacheServer process list, or a page read where exposed
Custom build markerPresentAbsent or defaultA page script that reads the specific property

A readback that checks only the first reading is weak, because a custom build can share a version string with the bundled one. A readback that checks only the third is also weak, because it proves a capability rather than the identity of the process serving the page. Check the path and the marker, and use the user agent as corroboration.

A readback the agent can run itself

The check does not need a separate harness. Ask the client to evaluate a short expression in the session and return the result; the agent can compare it with the configured path without leaving the conversation. What matters is that the comparison is explicit and recorded, not that it is automated.

Record three values per session: the configured path, the observed path, and the marker result. A session with a matching path and a present marker is the intended build. A session with a mismatched path is the fallback, whatever the user agent says.

Reading the path when the build does not expose it

Not every build exposes its own executable path to page script, and a build that does not expose it is not a defect. Where the page cannot answer, read the process list on the host that spawned the server and match the child process of the server. The parent-child relationship is the part worth checking: a machine may run several Chromium processes, and the one that matters is the descendant of the MCP server, not any browser the operator opened by hand.

A page-level marker remains the cheapest positive signal, so it is worth asking the build vendor which property is stable rather than scraping an incidental one. A marker that changes between minor releases is worse than no marker, because it produces a false negative that looks like a fallback.

Failure modes and the fallback branch

The failures cluster into a small set, and each has a distinct signature. Naming them makes the diagnosis a lookup rather than an experiment.

SymptomCauseConfirmationFix
Browser starts, wrong version in the user agentexecutablePath not readThe file has the key at the top level or under contextOptionsMove it into browser.launchOptions
Browser starts, profile settings absentProfile flag dropped or malformedargs holds one string with a space instead of separate elementsSplit into one array element per flag
Server exits with a launch errorConfigured path missing or not executableThe path does not resolve in the server's environmentUse an absolute path readable by the spawning process
Second client cannot start a browserPersistent profile already lockedBoth clients point at the same user data directoryIsolate, or give each client its own directory
Custom build ignored after a startup speed-upAn existing endpoint was configuredThe configuration still lists an endpointRemove the endpoint so the launch path runs

The first two rows are the ones that produce a working agent against the wrong browser, and they are also the ones a startup log will not report. The remaining three fail loudly enough to be noticed, which makes them the cheaper half of the problem.

What the client can and cannot see

An MCP client sees the tools the server exposes: navigation, snapshot, click, type, and whatever else the server publishes. It does not see the launch configuration, the process it spawned, or the arguments that were passed. That asymmetry is the reason the readback has to be requested through the session rather than assumed from the client side.

It also explains why a client cannot detect the fallback on its own. From the client's perspective the server is working correctly, because it is: the server launched a browser and is serving tools against it. The only party that knows which browser was intended is the operator who wrote the configuration. The wider division of responsibility between an agent loop and the browser process is the subject of headless browser for AI agents, the direct script this configuration has to reproduce is in the Playwright getting-started guide, and the flat executablePath / args / headless launch object a Puppeteer-based server expects is in Puppeteer MCP server configuration.

Recording the result of a verification

A verification that is not written down is a verification that will be repeated. Store the configured path, the observed path, the marker result and the timestamp together, so a later session can compare without re-deriving what "correct" meant at the time. When the configuration changes, the stored record makes the change visible rather than silent.

This is the same discipline a direct Playwright launch needs, and the reason it matters more under an MCP server is that the launch sits one layer further from the person who wrote the options. The client cannot see that layer, so the record is the only durable evidence that the layer was configured as intended.

FAQ

Does Playwright MCP read the executablePath at the top level of the config file?

No. The schema nests Playwright's LaunchOptions under browser.launchOptions, and a top-level key is not part of that shape. The server ignores what it does not recognise, so a misplaced key produces a normal start with the bundled browser rather than a validation error.

Can one MCP server serve several profiles at once?

Not through a single persistent profile. A persistent profile is locked to one browser instance, so parallel clients need isolated mode or separate user data directories. Isolated mode keeps the profile in memory and discards it on close, which is a different tradeoff rather than a way to share one profile.

Is a matching user agent enough to prove the right build started?

No. A custom build can report the same Chromium version as the bundled one, so a version match is corroboration rather than proof. Compare the process executable path with the configured path, and read a marker that only the custom build exposes.

Practical conclusion

Write the executable into browser.launchOptions.executablePath, put each profile and locale flag in its own args element, and choose exactly one of the three routes that can set the path. Then have the agent read back the process path and a build-specific marker before doing any real work.

The next action is one session long: start the server with the configuration, ask the client to return navigator.userAgent and the observed executable path, and compare both with the file. If the path matches, the setup is proven. If it does not, the server is running its bundled Chromium and every result from that session describes a browser you did not choose.

Sources

#Playwright MCP#MCP Server#executablePath#Browser Automation#AI Agent

This article is part of the guide: Browser Deployment and Release: Operations Map

Run the same check in BotBrowser

Load a profile, keep the signals you just read about consistent inside the browser, and confirm the result on your own machine before a rollout.