Back to Knowledge Hub
Deployment

Puppeteer MCP Server Configuration for a Custom Build

Configure a Puppeteer-based MCP server to launch a specific Chromium build with a profile, and see why a Playwright MCP config cannot be copied across.

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 Puppeteer MCP server owns the browser launch, so the configuration you hand it decides which Chromium binary and which profile every agent task runs against. Puppeteer's LaunchOptions is a different shape from Playwright's, and a configuration copied between the two silently loses the parts that do not exist on the other side. The failure is quiet: the server starts, the tools respond, and the browser is whatever the server would have used anyway. Puppeteer documents executablePath as a path used instead of the bundled browser, together with a warning that Puppeteer only guarantees support for its own bundled build, so the setting is deliberate rather than transparent. BotBrowser supports launching from Puppeteer with executablePath plus a profile argument in args, and it does not ship an MCP server of its own, so a server that never forwards those values cannot be corrected from the browser side.

TL;DR

  • Puppeteer's launch object puts executablePath, args, headless, userDataDir, ignoreDefaultArgs, pipe and browser at one level. Playwright nests the same ideas under launchOptions, so the two shapes are not interchangeable.
  • headless in Puppeteer accepts true, false or the string 'shell', and 'shell' selects the old headless implementation. A Playwright-style boolean written here is valid; a Playwright config copied wholesale is not.
  • executablePath replaces the bundled browser, and Puppeteer's own documentation warns that only the bundled build is guaranteed. Treat a custom path as a supported-but-unverified configuration and prove it by reading the process back.
  • The profile travels in args as its own element, exactly as it does under Playwright, because it is a browser argument rather than a Puppeteer option.
  • ignoreDefaultArgs and pipe change what the server hands the process. A server that sets ignoreDefaultArgs: true drops the flags Puppeteer would add, so the effective argv is neither yours nor Puppeteer's defaults.

A configuration flow shows a Puppeteer MCP server reading executablePath, args and headless from one flat launch object, then launching the configured binary, with a comparison column showing that the Playwright MCP shape nests the same values under browser.launchOptions.

Contents

Where the Puppeteer launch object lives

Puppeteer's LaunchOptions is one flat object passed to puppeteer.launch(). executablePath is a top-level member of it, alongside args, headless, userDataDir, ignoreDefaultArgs, pipe, browser, channel and debuggingPort. There is no launchOptions wrapper, because the whole object is the launch options.

{
  "mcpServers": {
    "puppeteer": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-puppeteer"],
      "env": {
        "PUPPETEER_EXECUTABLE_PATH": "/opt/botbrowser/chrome",
        "PUPPETEER_LAUNCH_OPTIONS": "{\"headless\":true,\"args\":[\"--bot-profile=/etc/botbrowser/profile.enc\"]}"
      }
    }
  }
}

The example above shows the two ways a server can receive the values: a dedicated environment variable for the executable, and a JSON blob for the rest. Neither name is standardised across Puppeteer-based servers, which is the first thing to check: read the server's own documentation for the exact variable or flag it reads, then confirm the value arrives.

Puppeteer memberTypeEffect on a custom build
executablePathstringReplaces the bundled browser; Puppeteer guarantees support only for its own build
argsstring[]Extra command-line arguments, one array element per argument
headlessboolean or 'shell'true uses the new headless mode, 'shell' the old one
userDataDirstringPersistent profile directory, separate from a BotBrowser profile argument
ignoreDefaultArgsboolean or string[]Drops Puppeteer's default flags, or filters named ones out
pipebooleanCarries the protocol over stdio instead of a WebSocket
browserSupportedBrowserSelects the browser family Puppeteer should expect
channelChromeReleaseChannelLooks for an installed Chrome at a known location instead of the bundled binary

Two of those rows deserve attention in a custom-build deployment. channel and executablePath can both point at a browser, and Puppeteer's documentation describes channel as looking for a regular Chrome installation at a known system location. Setting both leaves two answers to the same question, and the one that wins is an implementation detail worth eliminating rather than testing.

Why the flat shape is easy to misread

A flat object has no structural signal about which keys are launch-level and which belong to a context. Under Playwright, a misplaced key lands in a different object and is ignored; under Puppeteer, a key that does not exist in LaunchOptions is simply not part of the options, and puppeteer.launch() has no reason to complain. The result is the same silent outcome by a different route: the server starts, and one of your settings was never applied.

The practical check is to write the object down and compare its keys with the documented LaunchOptions members. Anything not on that list is not a launch option, whatever it looks like.

What Puppeteer does with a path it cannot use

executablePath points at a file, and Puppeteer starts that file. If the path does not exist, does not resolve in the server's working directory, or exists but is not executable by the account that runs the server, the launcher raises an error instead of substituting another binary; that follows from Puppeteer handing the given path to the process it spawns, and it is an inference from the launcher behaviour rather than a sentence the LaunchOptions page states. That behaviour is worth knowing because it is the opposite of the silent case: a wrong path that does exist produces a working browser that is not the intended one, while a path that does not exist produces an error the operator sees.

The failure that hides is therefore the one where the path is valid and points somewhere else. A server that substitutes a default path when its configuration is absent produces exactly that: a valid executable, a working browser, and no error. The check has to compare the observed path with the intended one rather than check that a path exists.

What the server adds on top

A Puppeteer-based MCP server is a wrapper: it reads its own configuration, builds a LaunchOptions object, and calls puppeteer.launch(). Everything the server adds between its configuration and that call is invisible from the outside. That is why the readback matters more here than in a direct script, where the options object is the one the operator wrote.

The Playwright variant of this wrapper, including its nested shape and the silent fallback to a bundled browser, is covered in Playwright MCP setup for a custom Chromium build, and the division of responsibility between an agent loop and the browser process is in headless browser for AI agents. A server may add a default args array, a headless default, or an ignoreDefaultArgs value. A server that appends its own arguments produces an argv that is the union of yours and its defaults, in an order you did not choose. Reading the running process is the only way to see the union.

Why the effective argv matters more than the config

Two runs can share a configuration file and still start different browsers if the server version changed its defaults between them. A server that begins adding a new default flag, or stops adding one, changes the effective argv without touching the operator's file. Because the browser reads argv rather than the server's configuration, the observable behaviour follows the argv.

That is the argument for recording the effective argv beside the observation rather than only the configuration that produced it. A configuration plus a server version is reproducible; a configuration alone is not, once the server can change what it appends.

How it differs from the Playwright MCP shape

The two ecosystems answer the same question with different structures, and the differences are the ones that break a copied configuration.

ConcernPlaywright MCPPuppeteer launch object
Where executablePath goesbrowser.launchOptions.executablePathTop-level executablePath
Where args goesbrowser.launchOptions.argsTop-level args
Headless valueBoolean headless inside launchOptionsBoolean or the string 'shell'
Context-level settingsSeparate browser.contextOptions objectNot part of LaunchOptions; the server or the code sets viewport elsewhere
Profile persistenceuserDataDir under browseruserDataDir at the top level
Connecting to a running browsercdpEndpoint or remoteEndpointbrowserWSEndpoint or connect(), outside LaunchOptions

The first two rows are why a Playwright MCP JSON block pasted into a Puppeteer server does nothing: the keys exist in neither place the Puppeteer server looks. The third row is subtler, because headless: true is valid in both, so a copy can appear to work while the surrounding structure is wrong.

The context split does not exist here

Playwright separates launch options from context options, which is why viewport, locale and permissions live in a different object. Puppeteer's LaunchOptions has no such split: viewport and page-level settings are applied after launch, through page.setViewport() or a device descriptor. A configuration that expects a contextOptions block to carry a locale will not find one.

That difference matters when a task depends on a locale or a timezone. Under Playwright MCP, those belong to the context and are configured before the page exists. Under Puppeteer, they are either browser arguments in args or script calls after launch, and the server has to expose a way to do one of them.

Emulation calls are a third route

page.emulate() and page.setViewport() change what the page reports after the browser has started, which makes them a third place a locale, a timezone or a viewport can come from. A workload that sets a locale through args, another through an emulation call, and a third through the server's own defaults has three sources and no single answer.

The rule that keeps this tractable is one source per property. Choose args for anything the browser must know before the first request, and an emulation call only for what the page needs after load. Record which route each property used, because a later comparison between a run that set a locale in args and a run that set it through emulation is not comparing the same thing.

Two different notions of a profile

Puppeteer's userDataDir and a BotBrowser profile argument both describe persistent state, and they are not the same thing. userDataDir is a Chromium user data directory that Puppeteer passes through. A BotBrowser profile is an encrypted configuration file the binary loads through its own flag. Setting one does not set the other, and a server that exposes only userDataDir has no slot for the profile flag unless it forwards arbitrary args.

Pass the profile and the headless mode

The profile flag is a browser argument, so it goes in args as one element. The headless mode is a Puppeteer option, so it goes in headless. Mixing the two up is the next configuration error to check after the shape confusion.

{
  "executablePath": "/opt/botbrowser/chrome",
  "headless": true,
  "args": ["--bot-profile=/etc/botbrowser/profile.enc", "--bot-locale=en-US", "--no-first-run"]
}

Each element is one argument with no shell quoting. A single string containing spaces is one malformed argument. The third element in the example is a plain Chromium flag rather than a BotBrowser one, which is worth noting because args mixes both kinds freely: Puppeteer does not distinguish them.

Quoting, escaping, and the JSON layer

When the configuration is JSON, the arguments pass through two layers: JSON string escaping and then the argument vector. A Windows path such as C:\botbrowser\chrome.exe needs its backslashes escaped in JSON, and a value containing a quote needs the JSON escape rather than a shell escape. Getting one layer right and the other wrong produces a path that looks correct in the file and wrong at launch.

Keep the values free of characters that need escaping where possible. A profile path with no spaces and no quotes avoids the whole class of problem, and an absolute path under a short directory is easier to verify character for character than a long one.

Headless, 'shell', and what changes

Puppeteer documents headless as accepting true or the string 'shell', where true selects the new headless mode and 'shell' selects the old one. The distinction is not cosmetic: the two modes differ in how the browser composites and in which APIs report what. A workload that compares rendering or reads a media capability should record which mode a run used, because two runs in different modes are not the same measurement.

When ignoreDefaultArgs changes the answer

ignoreDefaultArgs accepts true or an array of flag names. With true, Puppeteer stops adding its default arguments, so the browser starts with only what args contains. With an array, the named defaults are filtered out and the rest remain. Both change the effective argv, and both are the kind of setting a server may apply on its own.

The operational consequence is that a documented BotBrowser flag can be neutralised by a default Puppeteer adds, and a default Puppeteer expects can be removed by ignoreDefaultArgs. Neither is visible in the configuration file, which is why the verification step reads the process rather than the config.

pipe and how the server talks to the browser

pipe: true carries the automation protocol over standard input and output instead of opening a WebSocket. For a server that runs the browser as a child process, that removes a listening port from the equation, which changes both the failure modes and the security surface. A configuration that assumes a debugging port will exist does not hold when pipe is set, and a tool that connects over a port cannot reach a piped browser.

Record which transport a run used. A result gathered over a pipe and a result gathered over a port come from the same browser only if the rest of the configuration matches, and the transport is part of that configuration rather than an implementation detail.

Verify the binary that actually started

Verification has three readings, and they answer different questions. The process path proves which file was executed. The user agent describes what the page reports, which a custom build may configure independently of the binary. A build marker proves a capability the stock build lacks.

Read the process first. On the host that spawned the server, find the child process and compare its executable with the configured path. A server that failed to forward executablePath shows a path under Puppeteer's cache directory instead, and that single comparison settles the question without any page-level probing.

ReadingWhat it provesWhat it does not prove
Child process executableWhich file the server executedThat the profile was loaded
navigator.userAgentWhat the page reportsWhich binary produced it
Build markerThat a specific capability is presentThat the configuration reached it by the intended route
Profile load evidenceThat the encrypted profile was acceptedThat the page content is correct

When the process tree is not visible

A server running inside a container, a remote host or a sandbox may not expose its child processes to the operator's shell. In that case the path has to come from inside the environment that owns the process, through a log line the server writes, a /proc entry, or a diagnostic tool the platform provides. The requirement is unchanged: compare the observed executable with the configured one, from wherever the observation is available.

Where none of those is available, a build marker inside the page is the fallback evidence, and it should be described as weaker. A marker proves a capability is present; it does not prove the process that serves the page is the one the operator configured.

Recording a verification

Store the configured path, the observed path, the headless mode, the effective args, and the marker result together with a timestamp. A later run can then compare without re-deriving what the baseline was. When a server update changes its default args, that record is what makes the change visible.

What a client cannot see

An MCP client sees the tools the server publishes. It does not see the launch object, the process tree, or the arguments that were passed. From the client side a misconfigured server is indistinguishable from a correct one, which is why the verification has to be requested through the session and recorded outside it.

A symptom table for the Puppeteer path

SymptomCauseConfirmationFix
Browser starts, no profile settingsProfile flag missing from argsargs has no element beginning with the profile flagAdd one array element per flag
Browser starts from the cache directoryexecutablePath never reached the launch objectChild process path resolves under the Puppeteer cacheMove the key to the top level of the launch object
Server exits with a launch errorPath missing or not executableThe path does not resolve for the server's accountUse an absolute path the account can execute
Flags behave differently after a server updateThe server changed its default argsThe effective argv differs from the recorded oneRecord argv and pin the server version
A tool cannot reach the browserTransport mismatch between pipe and portThe configuration sets pipe while the tool expects a portChoose one transport per deployment

The second row is the one that survives a working session. A server that fell back to its bundled browser answers every tool call correctly, and the only evidence is the process path.

FAQ

Can a Playwright MCP configuration be reused for a Puppeteer server?

No. Playwright MCP nests executablePath and args under browser.launchOptions, and Puppeteer expects both at the top level of its launch object. A pasted block leaves the Puppeteer server with no executable path, so it starts its bundled browser and reports success.

Is headless: 'shell' the same as headless: false?

No. 'shell' selects the old headless implementation, which still runs without a visible window but composites and reports differently from the new headless mode. false runs a headed browser. Recording which of the three a run used keeps two measurements comparable.

Does userDataDir replace a BotBrowser profile?

No. userDataDir is a Chromium user data directory that Puppeteer passes to the browser. A BotBrowser profile is an encrypted configuration file the binary loads through its own flag in args. A server that exposes only userDataDir has no slot for the profile unless it forwards arbitrary arguments.

Practical conclusion

Write the launch object flat, with executablePath, args and headless as siblings, and put the profile flag inside args as its own element. Then read the child process on the host and compare its executable with the configured path before trusting any result from the session.

The next action is one launch: start the server, ask the client to run a single navigation, and read the child process path on the host. If it matches the configured path, the binary is proven. If it points into Puppeteer's cache, the server ignored the option and every result describes a browser you did not choose.

Sources

#Puppeteer 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.