diff --git a/package-lock.json b/package-lock.json index 3f1fc41..3e28e18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.10.1", "commander": "^13.1.0", - "playwright": "1.53.0-alpha-1745357020000", + "playwright": "1.53.0-alpha-2025-04-25", "yaml": "^2.7.1", "zod-to-json-schema": "^3.24.4" }, @@ -21,7 +21,7 @@ "devDependencies": { "@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.19.0", - "@playwright/test": "1.53.0-alpha-1745357020000", + "@playwright/test": "1.53.0-alpha-2025-04-25", "@stylistic/eslint-plugin": "^3.0.1", "@types/node": "^22.13.10", "@typescript-eslint/eslint-plugin": "^8.26.1", @@ -287,13 +287,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.53.0-alpha-1745357020000", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.0-alpha-1745357020000.tgz", - "integrity": "sha512-7xQRHhsS//elVJVt2WybJPXAy++WiE8yJzMtVFcnzdQNg9VNSbpqo4b61io5IIG1nEfB22N4BhjQ/8jPrUyu9A==", + "version": "1.53.0-alpha-2025-04-25", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.0-alpha-2025-04-25.tgz", + "integrity": "sha512-3y4C2ZjAc2oUpwavC2yG2JzH53TOKgcMZvWb5GmpxnOa6fhuSVXK0kIsiIaImKmdffIVM1agsqNHp8yldeBTHQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.53.0-alpha-1745357020000" + "playwright": "1.53.0-alpha-2025-04-25" }, "bin": { "playwright": "cli.js" @@ -3299,12 +3299,12 @@ } }, "node_modules/playwright": { - "version": "1.53.0-alpha-1745357020000", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.0-alpha-1745357020000.tgz", - "integrity": "sha512-evnZJIB1CRSA1HfwCkLhyqyGZybSWdNwfyyUWhBoez9ISbYMuYrTtidx75oiGVXtbKr5s8iC0+opuvagu4L1vA==", + "version": "1.53.0-alpha-2025-04-25", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.0-alpha-2025-04-25.tgz", + "integrity": "sha512-b5VT4lWgyhhy99zHeCoUBt/FQckPxeQVA5ksvxBv0HeqcEvzZzhuyqrrcZewJyflE+5U+bmvqI+yoU0ks8mE3Q==", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.53.0-alpha-1745357020000" + "playwright-core": "1.53.0-alpha-2025-04-25" }, "bin": { "playwright": "cli.js" @@ -3317,9 +3317,9 @@ } }, "node_modules/playwright-core": { - "version": "1.53.0-alpha-1745357020000", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.0-alpha-1745357020000.tgz", - "integrity": "sha512-3oPzOUwJ/yhNWUs3fh5UbmI1Mf18sHUDo3gxzuPwqxN3QCSFKx9Ncg7cSB+FyJCkgz7ZD8fUlzJ75YsDE+PMfA==", + "version": "1.53.0-alpha-2025-04-25", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.0-alpha-2025-04-25.tgz", + "integrity": "sha512-gjV01l6A4q/zg+/pwEX50k9lhYWaE9NcDVypSDD331jB3EYrdk0LeDQxqz5XFDOzq/tC/8QTouDs9a/s/p95hA==", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" diff --git a/package.json b/package.json index fa8144e..010a32d 100644 --- a/package.json +++ b/package.json @@ -36,14 +36,14 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.10.1", "commander": "^13.1.0", - "playwright": "1.53.0-alpha-1745357020000", + "playwright": "1.53.0-alpha-2025-04-25", "yaml": "^2.7.1", "zod-to-json-schema": "^3.24.4" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.19.0", - "@playwright/test": "1.53.0-alpha-1745357020000", + "@playwright/test": "1.53.0-alpha-2025-04-25", "@stylistic/eslint-plugin": "^3.0.1", "@types/node": "^22.13.10", "@typescript-eslint/eslint-plugin": "^8.26.1", diff --git a/src/context.ts b/src/context.ts index 7bbc69a..fe5d3bf 100644 --- a/src/context.ts +++ b/src/context.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import net from 'net'; + import * as playwright from 'playwright'; import yaml from 'yaml'; @@ -26,7 +28,7 @@ import type { ModalState, Tool, ToolActionResult } from './tools/tool'; export type ContextOptions = { browserName?: 'chromium' | 'firefox' | 'webkit'; userDataDir: string; - launchOptions?: playwright.LaunchOptions; + launchOptions?: playwright.LaunchOptions & playwright.BrowserContextOptions; cdpEndpoint?: string; remoteEndpoint?: string; }; @@ -42,6 +44,7 @@ export class Context { readonly options: ContextOptions; private _browser: playwright.Browser | undefined; private _browserContext: playwright.BrowserContext | undefined; + private _createBrowserContextPromise: Promise<{ browser?: playwright.Browser, browserContext: playwright.BrowserContext }> | undefined; private _tabs: Tab[] = []; private _currentTab: Tab | undefined; private _modalStates: (ModalState & { tab: Tab })[] = []; @@ -259,6 +262,7 @@ ${code.join('\n')} return; const browserContext = this._browserContext; const browser = this._browser; + this._createBrowserContextPromise = undefined; this._browserContext = undefined; this._browser = undefined; @@ -280,6 +284,15 @@ ${code.join('\n')} } private async _createBrowserContext(): Promise<{ browser?: playwright.Browser, browserContext: playwright.BrowserContext }> { + if (!this._createBrowserContextPromise) + this._createBrowserContextPromise = this._innerCreateBrowserContext(); + return this._createBrowserContextPromise; + } + + private async _innerCreateBrowserContext(): Promise<{ browser?: playwright.Browser, browserContext: playwright.BrowserContext }> { + if (this.options.browserName === 'chromium') + (this.options.launchOptions as any).webSocketPort = await findFreePort(); + if (this.options.remoteEndpoint) { const url = new URL(this.options.remoteEndpoint); if (this.options.browserName) @@ -468,3 +481,14 @@ class PageSnapshot { export async function generateLocator(locator: playwright.Locator): Promise { return (locator as any)._generateLocatorString(); } + +async function findFreePort() { + return new Promise((resolve, reject) => { + const server = net.createServer(); + server.listen(0, () => { + const { port } = server.address() as net.AddressInfo; + server.close(() => resolve(port)); + }); + server.on('error', reject); + }); +} diff --git a/src/index.ts b/src/index.ts index eade841..88c947e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,7 +34,7 @@ import screen from './tools/screen'; import type { Tool, ToolCapability } from './tools/tool'; import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; -import type { LaunchOptions } from 'playwright'; +import type { LaunchOptions, BrowserContextOptions } from 'playwright'; const snapshotTools: Tool[] = [ ...common(true), @@ -84,6 +84,7 @@ export async function createServer(options?: Options): Promise { case 'chrome-beta': case 'chrome-canary': case 'chrome-dev': + case 'chromium': case 'msedge': case 'msedge-beta': case 'msedge-canary': @@ -91,9 +92,6 @@ export async function createServer(options?: Options): Promise { browserName = 'chromium'; channel = options.browser; break; - case 'chromium': - browserName = 'chromium'; - break; case 'firefox': browserName = 'firefox'; break; @@ -106,10 +104,12 @@ export async function createServer(options?: Options): Promise { } const userDataDir = options?.userDataDir ?? await createUserDataDir(browserName); - const launchOptions: LaunchOptions = { + const launchOptions: LaunchOptions & BrowserContextOptions = { headless: !!(options?.headless ?? (os.platform() === 'linux' && !process.env.DISPLAY)), channel, executablePath: options?.executablePath, + viewport: null, + ...{ assistantMode: true }, }; const allTools = options?.vision ? screenshotTools : snapshotTools; diff --git a/tests/webdriver.spec.ts b/tests/webdriver.spec.ts new file mode 100644 index 0000000..fca04a9 --- /dev/null +++ b/tests/webdriver.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './fixtures'; + +test('do not falsely advertise user agent as a test driver', async ({ client, server, mcpBrowser }) => { + test.skip(mcpBrowser === 'firefox'); + test.skip(mcpBrowser === 'webkit'); + server.route('/', (req, res) => { + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(` + + + `); + }); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { + url: server.PREFIX, + }, + })).toContainTextContent('webdriver: false'); +});