Repository navigation
Description
Description
A --init-page hook that patches/wraps methods on the page object (e.g. page.screenshot) races the first tool call of a fresh MCP process: the tool can execute before the hook has been applied. From the second tool call on, the hook works as expected.
Root cause (from the bundled source, playwright-core 1.62.0-alpha-2026-06-29 shipped with @playwright/mcp 0.0.77):
- The Tab constructor starts initialization fire-and-forget:
this._initializedPromise = this._initialize();
_initialize()performs two roundtrips first (collectConsoleMessages,page.requests()) and only then applies the init-page hooks:for (const initPage of this.context.config.browser?.initPage || []) { const { default: func } = require(initPage); await func({ page: this.page }); }
- Many Tab methods do
await this._initializedPromise;(navigate,captureSnapshot,targetLocator,consoleMessages, ...), but thebrowser_take_screenshothandler does not -- for a plain viewport/fullPage screenshot it callstab.page.screenshot(options)directly. (The element-screenshot path happens to be safe only because it goes throughtargetLocator, which awaits the promise.)
So on the very first tools/call after server start, handle() can reach tab.page.screenshot() while _initialize() is still in its first awaits -- the hook's patch is not installed yet.
Steps to reproduce
hook.js:
module.exports.default = async ({ page }) => {
console.error('[hook] applied');
const orig = page.screenshot.bind(page);
page.screenshot = (o) => { console.error('[hook] wrapped call'); return orig(o); };
};- Attach to a running browser that already has a page open:
npx -y @playwright/mcp@latest --cdp-endpoint http://localhost:9222 --init-page hook.js - Send
initialize/notifications/initialized, then a singletools/call->browser_take_screenshot(no prior tool call).
Expected behavior
stderr shows [hook] applied and then [hook] wrapped call for the first screenshot.
Actual behavior
stderr shows [hook] applied, but the first screenshot runs the original page.screenshot (no [hook] wrapped call). From the second browser_take_screenshot on, the wrapper is invoked.
Why this matters (how we hit it)
We use an init-page hook to work around a CDP-attach screenshot hang: when the attached browser window is minimized, Chromium's compositor produces no frames and Page.captureScreenshot never resolves -- Playwright issues no bringToFront before taking a page screenshot, so browser_take_screenshot reliably times out (page.screenshot: Timeout 5000ms exceeded, call log ends after taking page screenshot). Our hook un-minimizes/brings the tab to front around page.screenshot and restores the window state afterwards. Because of this race, the first screenshot of every fresh MCP process still bypasses the hook and times out; every later one works.
(That underlying "page screenshot hangs while the attached browser window is minimized" behavior is arguably its own bug -- happy to file it separately if useful.)
Suggested fix
Await tab._initializedPromise before tool handlers touch tab.page -- either centrally at dispatch time for the resolved target tab, or at least in the screenshot/PDF handlers that use tab.page directly. That would guarantee init-page hooks are applied before any tool uses the page.
Environment
- OS: Windows Server 2025
@playwright/mcp: 0.0.77 (playwright-core1.62.0-alpha-2026-06-29)- Browser: Microsoft Edge 150.0.4078.48, attached via
--cdp-endpoint http://localhost:9222 - Node: 24.x
Activity
harshmathurx commentedon Jul 6, 2026
I can take this. I reproduced the source path locally:
browser_take_screenshottakes the no-target path straight totab.page.screenshot(...), while the target path goes throughtab.targetLocator(...)and waits for tab initialization.I'll open a focused PR in
microsoft/playwrightsince the implementation lives inplaywright-core, and link it back here.harshmathurx commentedon Jul 6, 2026
PR is open in
microsoft/playwright: #41646