From b0f786a1660044467873bd72e4587104e9841f48 Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Fri, 19 Dec 2025 19:08:24 +0100 Subject: [PATCH] less root crashes in mcp. more error logs. --- extension/src/background.ts | 2 +- playwriter/package.json | 1 + playwriter/src/cdp-session.ts | 70 +++++++++++++++++----------- playwriter/src/mcp.test.ts | 64 ++++++++++++++++++++++++- playwriter/src/mcp.ts | 62 +++++++++++++++--------- playwriter/src/utils.ts | 4 +- playwriter/src/wait-for-page-load.ts | 25 +++++++--- pnpm-lock.yaml | 10 ++++ 8 files changed, 178 insertions(+), 60 deletions(-) diff --git a/extension/src/background.ts b/extension/src/background.ts index 711cd47..4d9de55 100644 --- a/extension/src/background.ts +++ b/extension/src/background.ts @@ -732,7 +732,7 @@ const icons = { }, title: 'Error', badgeText: '!', - badgeColor: [0, 0, 0, 0] as [number, number, number, number], + badgeColor: [220, 38, 38, 255] as [number, number, number, number], }, } as const diff --git a/playwriter/package.json b/playwriter/package.json index a09725c..4ece0a1 100644 --- a/playwriter/package.json +++ b/playwriter/package.json @@ -30,6 +30,7 @@ "@mizchi/selector-generator": "1.50.0-next", "@types/chrome": "^0.0.315", "@types/node": "^24.10.1", + "@types/ws": "^8.18.1", "@vitest/ui": "^4.0.8", "image-size": "^2.0.2", "mcp-extension": "workspace:*", diff --git a/playwriter/src/cdp-session.ts b/playwriter/src/cdp-session.ts index 0a16de2..2c71fa9 100644 --- a/playwriter/src/cdp-session.ts +++ b/playwriter/src/cdp-session.ts @@ -18,29 +18,33 @@ export class CDPSession { constructor(ws: WebSocket) { this.ws = ws this.ws.on('message', (data) => { - const message = JSON.parse(data.toString()) as CDPResponseBase | CDPEventBase + try { + const message = JSON.parse(data.toString()) as CDPResponseBase | CDPEventBase - if ('id' in message) { - const response = message as CDPResponseBase - const pending = this.pendingRequests.get(response.id) - if (pending) { - this.pendingRequests.delete(response.id) - if (response.error) { - pending.reject(new Error(response.error.message)) - } else { - pending.resolve(response.result) + if ('id' in message) { + const response = message as CDPResponseBase + const pending = this.pendingRequests.get(response.id) + if (pending) { + this.pendingRequests.delete(response.id) + if (response.error) { + pending.reject(new Error(response.error.message)) + } else { + pending.resolve(response.result) + } } - } - } else if ('method' in message) { - const event = message as CDPEventBase - if (event.sessionId === this.sessionId || !event.sessionId) { - const listeners = this.eventListeners.get(event.method) - if (listeners) { - for (const listener of listeners) { - listener(event.params) + } else if ('method' in message) { + const event = message as CDPEventBase + if (event.sessionId === this.sessionId || !event.sessionId) { + const listeners = this.eventListeners.get(event.method) + if (listeners) { + for (const listener of listeners) { + listener(event.params) + } } } } + } catch (e) { + console.error('[CDPSession] Message handling error:', e) } }) } @@ -51,7 +55,7 @@ export class CDPSession { send( method: K, - params?: ProtocolMapping.Commands[K]['paramsType'][0] + params?: ProtocolMapping.Commands[K]['paramsType'][0], ): Promise { const id = ++this.messageId const message: Record = { id, method, params } @@ -73,10 +77,16 @@ export class CDPSession { reject: (error) => { clearTimeout(timeout) reject(error) - } + }, }) - this.ws.send(JSON.stringify(message)) + try { + this.ws.send(JSON.stringify(message)) + } catch (error) { + clearTimeout(timeout) + this.pendingRequests.delete(id) + reject(error instanceof Error ? error : new Error(String(error))) + } }) } @@ -92,12 +102,16 @@ export class CDPSession { } detach() { - for (const pending of this.pendingRequests.values()) { - pending.reject(new Error('CDPSession detached')) + try { + for (const pending of this.pendingRequests.values()) { + pending.reject(new Error('CDPSession detached')) + } + this.pendingRequests.clear() + this.eventListeners.clear() + this.ws.close() + } catch (e) { + console.error('[CDPSession] WebSocket close error:', e) } - this.pendingRequests.clear() - this.eventListeners.clear() - this.ws.close() } } @@ -119,7 +133,7 @@ export async function getCDPSessionForPage({ page, wsUrl }: { page: Page; wsUrl: } const { targetInfos } = await cdp.send('Target.getTargets') - const pageTargets = targetInfos.filter(t => t.type === 'page') + const pageTargets = targetInfos.filter((t) => t.type === 'page') if (pageIndex >= pageTargets.length) { cdp.detach() @@ -134,7 +148,7 @@ export async function getCDPSessionForPage({ page, wsUrl }: { page: Page; wsUrl: const { sessionId } = await cdp.send('Target.attachToTarget', { targetId: target.targetId, - flatten: true + flatten: true, }) cdp.setSessionId(sessionId) diff --git a/playwriter/src/mcp.test.ts b/playwriter/src/mcp.test.ts index ec377f2..e497ed0 100644 --- a/playwriter/src/mcp.test.ts +++ b/playwriter/src/mcp.test.ts @@ -86,6 +86,7 @@ async function setupTestContext({ tempDirPrefix }: { tempDirPrefix: string }): P const browserContext = await chromium.launchPersistentContext(userDataDir, { channel: 'chromium', headless: !process.env.HEADFUL, + colorScheme: 'dark', args: [ `--disable-extensions-except=${extensionPath}`, `--load-extension=${extensionPath}`, @@ -1698,6 +1699,59 @@ describe('MCP Server Tests', () => { await stagehand.close() }, 60000) + it('should preserve system color scheme instead of forcing light mode', async () => { + const browserContext = getBrowserContext() + const serviceWorker = await getExtensionServiceWorker(browserContext) + + const page = await browserContext.newPage() + await page.goto('https://example.com') + await page.bringToFront() + + const colorSchemeBefore = await page.evaluate(() => { + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' + }) + console.log('Color scheme before MCP connection:', colorSchemeBefore) + + await serviceWorker.evaluate(async () => { + await globalThis.toggleExtensionForActiveTab() + }) + await new Promise(r => setTimeout(r, 500)) + + const result = await client.callTool({ + name: 'execute', + arguments: { + code: js` + const pages = context.pages(); + const urls = pages.map(p => p.url()); + const targetPage = pages.find(p => p.url().includes('example.com')); + if (!targetPage) { + return { error: 'Page not found', urls }; + } + const isDark = await targetPage.evaluate(() => window.matchMedia('(prefers-color-scheme: dark)').matches); + const isLight = await targetPage.evaluate(() => window.matchMedia('(prefers-color-scheme: light)').matches); + return { matchesDark: isDark, matchesLight: isLight }; + `, + }, + }) + + console.log('Color scheme after MCP connection:', result.content) + + expect(result.content).toMatchInlineSnapshot(` + [ + { + "text": "Return value: + { + "error": "Page not found", + "urls": [] + }", + "type": "text", + }, + ] + `) + + await page.close() + }, 60000) + }) @@ -1914,13 +1968,19 @@ describe('CDP Session Tests', () => { sampleFunctionNames: functionNames, }).toMatchInlineSnapshot(` { - "durationMicroseconds": 11057, + "durationMicroseconds": 7536, "hasNodes": true, - "nodeCount": 4, + "nodeCount": 20, "sampleFunctionNames": [ "(root)", "(program)", "(idle)", + "evaluate", + "fibonacci", + "fibonacci", + "fibonacci", + "fibonacci", + "fibonacci", "fibonacci", ], } diff --git a/playwriter/src/mcp.ts b/playwriter/src/mcp.ts index 8a19685..fb57432 100644 --- a/playwriter/src/mcp.ts +++ b/playwriter/src/mcp.ts @@ -94,8 +94,6 @@ const cdpSessionCache: WeakMap = new WeakMap() const RELAY_PORT = 19988 const NO_TABS_ERROR = `No browser tabs are connected. Please install and enable the Playwriter extension on at least one tab: https://chromewebstore.google.com/detail/playwriter-mcp/jfeammnjpkecdekppnclgkkffahnhfhe` - - async function setDeviceScaleFactorForMacOS(context: BrowserContext): Promise { if (os.platform() !== 'darwin') { return @@ -113,8 +111,25 @@ async function setDeviceScaleFactorForMacOS(context: BrowserContext): Promise { + const options = (context as any)._options + if (!options) { + return + } + options.colorScheme = 'no-override' + options.reducedMotion = 'no-override' + options.forcedColors = 'no-override' + await Promise.all( + context.pages().map((page) => { + return page.emulateMedia({ colorScheme: null, reducedMotion: null, forcedColors: null }).catch(() => {}) + }), + ) +} + function isRegExp(value: any): value is RegExp { - return typeof value === 'object' && value !== null && typeof value.test === 'function' && typeof value.exec === 'function' + return ( + typeof value === 'object' && value !== null && typeof value.test === 'function' && typeof value.exec === 'function' + ) } function clearUserState() { @@ -209,6 +224,7 @@ async function ensureConnection(): Promise<{ browser: Browser; page: Page }> { // Set up console listener for all existing pages context.pages().forEach((p) => setupPageConsoleListener(p)) + await preserveSystemColorScheme(context) await setDeviceScaleFactorForMacOS(context) state.browser = browser @@ -246,31 +262,32 @@ function setupPageConsoleListener(page: Page) { browserLogs.set(targetId, []) } - // Clear logs on navigation/reload page.on('framenavigated', (frame) => { - // Only clear if it's the main frame navigating (page reload/navigation) if (frame === page.mainFrame()) { browserLogs.set(targetId, []) } }) - // Delete logs when page is closed page.on('close', () => { browserLogs.delete(targetId) }) page.on('console', (msg) => { - const logEntry = `[${msg.type()}] ${msg.text()}` + try { + let logEntry = `[${msg.type()}] ${msg.text()}` - // Get or create logs array for this page targetId - if (!browserLogs.has(targetId)) { - browserLogs.set(targetId, []) - } - const pageLogs = browserLogs.get(targetId)! + if (!browserLogs.has(targetId)) { + browserLogs.set(targetId, []) + } + const pageLogs = browserLogs.get(targetId)! - pageLogs.push(logEntry) - if (pageLogs.length > MAX_LOGS_PER_PAGE) { - pageLogs.shift() + pageLogs.push(logEntry) + if (pageLogs.length > MAX_LOGS_PER_PAGE) { + pageLogs.shift() + } + } catch (e) { + console.error('[MCP] Failed to get console message text:', e) + return } }) } @@ -287,8 +304,7 @@ async function getCurrentPage(timeout = 5000) { if (pages.length > 0) { const page = pages[0] - page.waitForEvent('load', { timeout }) - await page.emulateMedia({ colorScheme: null }) + await page.waitForEvent('load', { timeout }).catch(() => {}) return page } } @@ -334,6 +350,7 @@ async function resetConnection(): Promise<{ browser: Browser; page: Page; contex // Set up console listener for all existing pages context.pages().forEach((p) => setupPageConsoleListener(p)) + await preserveSystemColorScheme(context) await setDeviceScaleFactorForMacOS(context) state.browser = browser @@ -350,7 +367,9 @@ const server = new McpServer({ version: '1.0.0', }) -const promptContent = fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), 'prompt.md'), 'utf-8') + `\n\nfor debugging errors, check relay server logs at: ${LOG_FILE_PATH}` +const promptContent = + fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), 'prompt.md'), 'utf-8') + + `\n\nfor debugging errors, check relay server logs at: ${LOG_FILE_PATH}` server.tool( 'execute', @@ -694,12 +713,13 @@ server.tool( }, ) -// Start the server async function main() { await ensureRelayServer() const transport = new StdioServerTransport() await server.connect(transport) - // console.error('Playwright MCP server running on stdio') } -main().catch(console.error) +main().catch((error) => { + console.error('Fatal error starting MCP server:', error) + process.exit(1) +}) diff --git a/playwriter/src/utils.ts b/playwriter/src/utils.ts index 6e5879a..b76f48b 100644 --- a/playwriter/src/utils.ts +++ b/playwriter/src/utils.ts @@ -1,4 +1,5 @@ import fs from 'node:fs' +import os from 'node:os' import path from 'node:path' import { xdgData } from 'xdg-basedir' @@ -8,7 +9,8 @@ export function getCdpUrl({ port = 19988, host = '127.0.0.1' }: { port?: number; } export function getDataDir(): string { - return path.join(xdgData!, 'playwriter') + const dataDir = xdgData || path.join(os.homedir(), '.local', 'share') + return path.join(dataDir, 'playwriter') } export function ensureDataDir(): string { diff --git a/playwriter/src/wait-for-page-load.ts b/playwriter/src/wait-for-page-load.ts index 55eb292..5fe080c 100644 --- a/playwriter/src/wait-for-page-load.ts +++ b/playwriter/src/wait-for-page-load.ts @@ -133,15 +133,26 @@ export async function waitForPageLoad(options: WaitForPageLoadOptions): Promise< await new Promise((resolve) => setTimeout(resolve, minWait)) while (Date.now() - startTime < timeout) { - const { ready, readyState, pendingRequests } = await checkPageReady() - lastReadyState = readyState - lastPendingRequests = pendingRequests + try { + const { ready, readyState, pendingRequests } = await checkPageReady() + lastReadyState = readyState + lastPendingRequests = pendingRequests - if (ready) { + if (ready) { + return { + success: true, + readyState, + pendingRequests: [], + waitTimeMs: Date.now() - startTime, + timedOut: false, + } + } + } catch (e) { + console.error('[waitForPageLoad] page.evaluate failed:', e) return { - success: true, - readyState, - pendingRequests: [], + success: false, + readyState: 'error', + pendingRequests: ['page.evaluate failed - page may have closed or navigated'], waitTimeMs: Date.now() - startTime, timedOut: false, } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 26ad6a9..df40782 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -112,6 +112,9 @@ importers: '@types/node': specifier: ^24.10.1 version: 24.10.1 + '@types/ws': + specifier: ^8.18.1 + version: 8.18.1 '@vitest/ui': specifier: ^4.0.8 version: 4.0.8(vitest@4.0.8) @@ -884,6 +887,9 @@ packages: '@types/uuid@10.0.0': resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} @@ -3555,6 +3561,10 @@ snapshots: '@types/uuid@10.0.0': {} + '@types/ws@8.18.1': + dependencies: + '@types/node': 24.10.1 + '@types/yauzl@2.10.3': dependencies: '@types/node': 24.10.1