From 366aa84395305a4d5e0f50b945892579b2ffb8b3 Mon Sep 17 00:00:00 2001 From: cynthialong0-0 <82900738+cynthialong0-0@users.noreply.github.com> Date: Mon, 16 Mar 2026 01:05:38 -0700 Subject: [PATCH] feat(agent): replace the runtime npx for browser agent chrome devtool mcp with pre-built bundle (#22213) Co-authored-by: Gaurav Ghosh Co-authored-by: Gaurav <39389231+gsquared94@users.noreply.github.com> --- eslint.config.js | 2 +- package-lock.json | 509 +++++++++++++++++- packages/core/package.json | 3 + packages/core/scripts/bundle-browser-mcp.mjs | 104 ++++ .../browser/browser-tools-manifest.json | 22 + .../browser/browserAgentFactory.test.ts | 8 +- .../src/agents/browser/browserManager.test.ts | 59 +- .../core/src/agents/browser/browserManager.ts | 41 +- .../src/agents/browser/mcpToolWrapper.test.ts | 12 +- .../core/src/agents/browser/mcpToolWrapper.ts | 238 +------- scripts/build_package.js | 9 + scripts/copy_bundle_assets.js | 8 + 12 files changed, 763 insertions(+), 252 deletions(-) create mode 100644 packages/core/scripts/bundle-browser-mcp.mjs create mode 100644 packages/core/src/agents/browser/browser-tools-manifest.json diff --git a/eslint.config.js b/eslint.config.js index d3a267f30a..150a50d2b7 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -303,7 +303,7 @@ export default tseslint.config( }, }, { - files: ['./scripts/**/*.js', 'esbuild.config.js'], + files: ['./scripts/**/*.js', 'esbuild.config.js', 'packages/core/scripts/**/*.{js,mjs}'], languageOptions: { globals: { ...globals.node, diff --git a/package-lock.json b/package-lock.json index ad4c9971db..92ce7568b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3044,6 +3044,27 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "license": "BSD-3-Clause" }, + "node_modules/@puppeteer/browsers": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz", + "integrity": "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", @@ -3768,6 +3789,12 @@ "node": ">= 10" } }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, "node_modules/@ts-morph/common": { "version": "0.12.3", "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.12.3.tgz", @@ -5593,6 +5620,18 @@ "node": ">=12" } }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/ast-v8-to-istanbul": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.8.tgz", @@ -5685,6 +5724,20 @@ "typed-rest-client": "^1.8.4" } }, + "node_modules/b4a": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -5694,6 +5747,93 @@ "node": "18 || 20 || >=22" } }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.5.tgz", + "integrity": "sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.7.1.tgz", + "integrity": "sha512-ebvMaS5BgZKmJlvuWh14dg9rbUI84QeV3WlWn6Ph6lFI8jJoh7ADtVTyD2c93euwbe+zgi0DVrl4YmqXeM9aIA==", + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.8.1.tgz", + "integrity": "sha512-bSeR8RfvbRwDpD7HWZvn8M3uYNDrk7m9DQjYOFkENZlXW8Ju/MPaqUPQq5LqJ3kyjEm07siTaAQ7wBKCU59oHg==", + "license": "Apache-2.0", + "dependencies": { + "streamx": "^2.21.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz", + "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -5714,6 +5854,15 @@ ], "license": "MIT" }, + "node_modules/basic-ftp": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz", + "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/before-after-hook": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", @@ -6112,6 +6261,32 @@ "node": ">=18" } }, + "node_modules/chrome-devtools-mcp": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/chrome-devtools-mcp/-/chrome-devtools-mcp-0.19.0.tgz", + "integrity": "sha512-LfqjOxdUjWvCQrfeI5V3ZBJCUIDKGNmexSbSAgsrjVggN4X1OSObLxleSlX2zwcXRZYxqy209cww0MXcXuN1zw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "chrome-devtools-mcp": "build/src/index.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/chromium-bidi": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", + "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, "node_modules/cjs-module-lexer": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", @@ -6954,6 +7129,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -7213,6 +7402,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/devtools-protocol": { + "version": "0.0.1581282", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz", + "integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==", + "license": "BSD-3-Clause" + }, "node_modules/dezalgo": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", @@ -7768,6 +7963,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, "node_modules/eslint": { "version": "9.29.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.29.0.tgz", @@ -8128,7 +8344,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -8147,7 +8362,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -8199,6 +8413,15 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/eventsource": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", @@ -8406,6 +8629,12 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -9048,6 +9277,29 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/get-uri/node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/glob": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/glob/-/glob-12.0.0.tgz", @@ -9675,7 +9927,6 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.0", @@ -11772,6 +12023,12 @@ "node": ">= 18" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -11972,6 +12229,15 @@ "node": ">= 0.6" } }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/node-addon-api": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", @@ -12675,6 +12941,38 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/package-json": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/package-json/-/package-json-10.0.1.tgz", @@ -13145,6 +13443,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -13250,6 +13557,40 @@ "node": ">= 0.10" } }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", @@ -13303,6 +13644,45 @@ "node": ">=6" } }, + "node_modules/puppeteer-core": { + "version": "24.39.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.39.0.tgz", + "integrity": "sha512-SzIxz76Kgu17HUIi57HOejPiN0JKa9VCd2GcPY1sAh6RA4BzGZarFQdOYIYrBdUVbtyH7CrDb9uhGEwVXK/YNA==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.0", + "chromium-bidi": "14.0.0", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1581282", + "typed-query-selector": "^2.12.1", + "webdriver-bidi-protocol": "0.4.1", + "ws": "^8.19.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core/node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/qs": { "version": "6.14.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", @@ -14265,9 +14645,9 @@ } }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -14598,6 +14978,54 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -14726,6 +15154,17 @@ "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", "license": "MIT" }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, "node_modules/strict-event-emitter": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", @@ -15323,6 +15762,32 @@ "node": ">=8" } }, + "node_modules/tar-fs": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", + "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", + "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/teeny-request": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", @@ -15378,6 +15843,15 @@ "node": ">= 6" } }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, "node_modules/terminal-link": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-4.0.0.tgz", @@ -15410,6 +15884,15 @@ "node": ">=18" } }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -15887,6 +16370,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typed-query-selector": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.1.tgz", + "integrity": "sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==", + "license": "MIT" + }, "node_modules/typed-rest-client": { "version": "1.8.11", "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", @@ -16358,6 +16847,12 @@ } } }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", + "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -17255,6 +17750,7 @@ "open": "^10.1.2", "picomatch": "^4.0.1", "proper-lockfile": "^4.1.2", + "puppeteer-core": "^24.0.0", "read-package-up": "^11.0.0", "shell-quote": "^1.8.3", "simple-git": "^3.28.0", @@ -17273,6 +17769,7 @@ "@types/fast-levenshtein": "^0.0.4", "@types/js-yaml": "^4.0.9", "@types/picomatch": "^4.0.1", + "chrome-devtools-mcp": "^0.19.0", "msw": "^2.3.4", "typescript": "^5.3.3", "vitest": "^3.1.1" diff --git a/packages/core/package.json b/packages/core/package.json index f5f821fb6d..4a560072d7 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -10,6 +10,7 @@ "type": "module", "main": "dist/index.js", "scripts": { + "bundle:browser-mcp": "node scripts/bundle-browser-mcp.mjs", "build": "node ../../scripts/build_package.js", "lint": "eslint . --ext .ts,.tsx", "format": "prettier --write .", @@ -73,6 +74,7 @@ "open": "^10.1.2", "picomatch": "^4.0.1", "proper-lockfile": "^4.1.2", + "puppeteer-core": "^24.0.0", "read-package-up": "^11.0.0", "shell-quote": "^1.8.3", "simple-git": "^3.28.0", @@ -101,6 +103,7 @@ "@types/fast-levenshtein": "^0.0.4", "@types/js-yaml": "^4.0.9", "@types/picomatch": "^4.0.1", + "chrome-devtools-mcp": "^0.19.0", "msw": "^2.3.4", "typescript": "^5.3.3", "vitest": "^3.1.1" diff --git a/packages/core/scripts/bundle-browser-mcp.mjs b/packages/core/scripts/bundle-browser-mcp.mjs new file mode 100644 index 0000000000..efbdd5714c --- /dev/null +++ b/packages/core/scripts/bundle-browser-mcp.mjs @@ -0,0 +1,104 @@ +import esbuild from 'esbuild'; +import fs from 'node:fs'; // Import the full fs module +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const manifestPath = path.resolve( + __dirname, + '../src/agents/browser/browser-tools-manifest.json', +); +const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')); + +// Only exclude tools explicitly mentioned in the manifest's exclude list +const excludedToolsFiles = (manifest.exclude || []).map((t) => t.name); + +// Basic esbuild plugin to empty out excluded modules +const emptyModulePlugin = { + name: 'empty-modules', + setup(build) { + if (excludedToolsFiles.length === 0) return; + + // Create a filter that matches any of the excluded tools + const excludeFilter = new RegExp(`(${excludedToolsFiles.join('|')})\\.js$`); + + build.onResolve({ filter: excludeFilter }, (args) => { + // Check if we are inside a tools directory to avoid accidental matches + if ( + args.importer.includes('chrome-devtools-mcp') && + /[\\/]tools[\\/]/.test(args.importer) + ) { + return { path: args.path, namespace: 'empty' }; + } + return null; + }); + + build.onLoad({ filter: /.*/, namespace: 'empty' }, (_args) => ({ + contents: 'export {};', // Empty module (ESM) + loader: 'js', + })); + }, +}; + +async function bundle() { + try { + const entryPoint = path.resolve( + __dirname, + '../../../node_modules/chrome-devtools-mcp/build/src/index.js', + ); + await esbuild.build({ + entryPoints: [entryPoint], + bundle: true, + outfile: path.resolve( + __dirname, + '../dist/bundled/chrome-devtools-mcp.mjs', + ), + format: 'esm', + platform: 'node', + plugins: [emptyModulePlugin], + external: [ + 'puppeteer-core', + '/bundled/*', + '../../../node_modules/puppeteer-core/*', + ], + banner: { + js: 'import { createRequire as __createRequire } from "module"; const require = __createRequire(import.meta.url);', + }, + }); + + // Copy third_party assets + const srcThirdParty = path.resolve( + __dirname, + '../../../node_modules/chrome-devtools-mcp/build/src/third_party', + ); + const destThirdParty = path.resolve( + __dirname, + '../dist/bundled/third_party', + ); + + if (fs.existsSync(srcThirdParty)) { + if (fs.existsSync(destThirdParty)) { + fs.rmSync(destThirdParty, { recursive: true, force: true }); + } + fs.cpSync(srcThirdParty, destThirdParty, { + recursive: true, + filter: (src) => { + // Skip large/unnecessary bundles that are either explicitly excluded + // or not required for the browser agent functionality. + return ( + !src.includes('lighthouse-devtools-mcp-bundle.js') && + !src.includes('devtools-formatter-worker.js') + ); + }, + }); + } else { + console.warn(`Warning: third_party assets not found at ${srcThirdParty}`); + } + } catch (error) { + console.error('Error bundling chrome-devtools-mcp:', error); + process.exit(1); + } +} + +bundle(); diff --git a/packages/core/src/agents/browser/browser-tools-manifest.json b/packages/core/src/agents/browser/browser-tools-manifest.json new file mode 100644 index 0000000000..26b7575890 --- /dev/null +++ b/packages/core/src/agents/browser/browser-tools-manifest.json @@ -0,0 +1,22 @@ +{ + "description": "Explicitly promoted tools from chrome-devtools-mcp for the gemini-cli browser agent.", + "targetVersion": "0.19.0", + "exclude": [ + { + "name": "lighthouse", + "reason": "3.5 MB pre-built bundle — not needed for gemini-cli browser agent's core tasks." + }, + { + "name": "performance", + "reason": "Depends on chrome-devtools-frontend TraceEngine (~800 KB) — not needed for core tasks." + }, + { + "name": "screencast", + "reason": "Requires ffmpeg at runtime — not a common browser agent use case and adds external dependency." + }, + { + "name": "extensions", + "reason": "Extension management not relevant for the gemini-cli browser agent's current scope." + } + ] +} diff --git a/packages/core/src/agents/browser/browserAgentFactory.test.ts b/packages/core/src/agents/browser/browserAgentFactory.test.ts index bbc317a282..94ee0bf0a1 100644 --- a/packages/core/src/agents/browser/browserAgentFactory.test.ts +++ b/packages/core/src/agents/browser/browserAgentFactory.test.ts @@ -24,6 +24,7 @@ const mockBrowserManager = { { name: 'click', description: 'Click element' }, { name: 'fill', description: 'Fill form field' }, { name: 'navigate_page', description: 'Navigate to URL' }, + { name: 'type_text', description: 'Type text into an element' }, // Visual tools (from --experimental-vision) { name: 'click_at', description: 'Click at coordinates' }, ]), @@ -70,6 +71,7 @@ describe('browserAgentFactory', () => { { name: 'click', description: 'Click element' }, { name: 'fill', description: 'Fill form field' }, { name: 'navigate_page', description: 'Navigate to URL' }, + { name: 'type_text', description: 'Type text into an element' }, // Visual tools (from --experimental-vision) { name: 'click_at', description: 'Click at coordinates' }, ]); @@ -135,7 +137,7 @@ describe('browserAgentFactory', () => { ); expect(definition.name).toBe(BROWSER_AGENT_NAME); - // 5 MCP tools + 1 type_text composite tool (no analyze_screenshot without visualModel) + // 6 MCP tools (no analyze_screenshot without visualModel) expect(definition.toolConfig?.tools).toHaveLength(6); }); @@ -228,7 +230,7 @@ describe('browserAgentFactory', () => { mockMessageBus, ); - // 5 MCP tools + 1 type_text + 1 analyze_screenshot + // 6 MCP tools + 1 analyze_screenshot expect(definition.toolConfig?.tools).toHaveLength(7); const toolNames = definition.toolConfig?.tools @@ -268,6 +270,7 @@ describe('browserAgentFactory', () => { { name: 'close_page', description: 'Close page' }, { name: 'select_page', description: 'Select page' }, { name: 'press_key', description: 'Press key' }, + { name: 'type_text', description: 'Type text into an element' }, { name: 'hover', description: 'Hover element' }, ]); @@ -291,7 +294,6 @@ describe('browserAgentFactory', () => { expect(toolNames).toContain('click'); expect(toolNames).toContain('take_snapshot'); expect(toolNames).toContain('press_key'); - // Custom composite tool must also be present expect(toolNames).toContain('type_text'); // Total: 9 MCP + 1 type_text (no analyze_screenshot without visualModel) expect(definition.toolConfig?.tools).toHaveLength(10); diff --git a/packages/core/src/agents/browser/browserManager.test.ts b/packages/core/src/agents/browser/browserManager.test.ts index f053e231e2..18ea162df9 100644 --- a/packages/core/src/agents/browser/browserManager.test.ts +++ b/packages/core/src/agents/browser/browserManager.test.ts @@ -39,6 +39,7 @@ vi.mock('@modelcontextprotocol/sdk/client/stdio.js', () => ({ vi.mock('../../utils/debugLogger.js', () => ({ debugLogger: { log: vi.fn(), + warn: vi.fn(), error: vi.fn(), }, })); @@ -47,6 +48,20 @@ vi.mock('./automationOverlay.js', () => ({ injectAutomationOverlay: vi.fn().mockResolvedValue(undefined), })); +vi.mock('node:fs', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + existsSync: vi.fn((p: string) => { + if (p.endsWith('bundled/chrome-devtools-mcp.mjs')) { + return false; // Default + } + return actual.existsSync(p); + }), + }; +}); + +import * as fs from 'node:fs'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; @@ -96,6 +111,40 @@ describe('BrowserManager', () => { vi.restoreAllMocks(); }); + describe('MCP bundled path resolution', () => { + it('should use bundled path if it exists (handles bundled CLI)', async () => { + vi.mocked(fs.existsSync).mockReturnValue(true); + const manager = new BrowserManager(mockConfig); + await manager.ensureConnection(); + + expect(StdioClientTransport).toHaveBeenCalledWith( + expect.objectContaining({ + command: 'node', + args: expect.arrayContaining([ + expect.stringMatching(/bundled\/chrome-devtools-mcp\.mjs$/), + ]), + }), + ); + }); + + it('should fall back to development path if bundled path does not exist', async () => { + vi.mocked(fs.existsSync).mockReturnValue(false); + const manager = new BrowserManager(mockConfig); + await manager.ensureConnection(); + + expect(StdioClientTransport).toHaveBeenCalledWith( + expect.objectContaining({ + command: 'node', + args: expect.arrayContaining([ + expect.stringMatching( + /(dist\/)?bundled\/chrome-devtools-mcp\.mjs$/, + ), + ]), + }), + ); + }); + }); + describe('getRawMcpClient', () => { it('should ensure connection and return raw MCP client', async () => { const manager = new BrowserManager(mockConfig); @@ -222,10 +271,9 @@ describe('BrowserManager', () => { // Verify StdioClientTransport was created with correct args expect(StdioClientTransport).toHaveBeenCalledWith( expect.objectContaining({ - command: process.platform === 'win32' ? 'npx.cmd' : 'npx', + command: 'node', args: expect.arrayContaining([ - '-y', - expect.stringMatching(/chrome-devtools-mcp@/), + expect.stringMatching(/chrome-devtools-mcp\.mjs$/), '--experimental-vision', ]), }), @@ -235,6 +283,7 @@ describe('BrowserManager', () => { ?.args as string[]; expect(args).not.toContain('--isolated'); expect(args).not.toContain('--autoConnect'); + expect(args).not.toContain('-y'); // Persistent mode should set the default --userDataDir under ~/.gemini expect(args).toContain('--userDataDir'); const userDataDirIndex = args.indexOf('--userDataDir'); @@ -294,7 +343,7 @@ describe('BrowserManager', () => { expect(StdioClientTransport).toHaveBeenCalledWith( expect.objectContaining({ - command: process.platform === 'win32' ? 'npx.cmd' : 'npx', + command: 'node', args: expect.arrayContaining(['--headless']), }), ); @@ -319,7 +368,7 @@ describe('BrowserManager', () => { expect(StdioClientTransport).toHaveBeenCalledWith( expect.objectContaining({ - command: process.platform === 'win32' ? 'npx.cmd' : 'npx', + command: 'node', args: expect.arrayContaining(['--userDataDir', '/path/to/profile']), }), ); diff --git a/packages/core/src/agents/browser/browserManager.ts b/packages/core/src/agents/browser/browserManager.ts index 63b5cff89a..08e9597755 100644 --- a/packages/core/src/agents/browser/browserManager.ts +++ b/packages/core/src/agents/browser/browserManager.ts @@ -25,10 +25,12 @@ import type { Config } from '../../config/config.js'; import { Storage } from '../../config/storage.js'; import { injectInputBlocker } from './inputBlocker.js'; import * as path from 'node:path'; +import * as fs from 'node:fs'; +import { fileURLToPath } from 'node:url'; import { injectAutomationOverlay } from './automationOverlay.js'; -// Pin chrome-devtools-mcp version for reproducibility. -const CHROME_DEVTOOLS_MCP_VERSION = '0.17.1'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); // Default browser profile directory name within ~/.gemini/ const BROWSER_PROFILE_DIR = 'cli-browser-profile'; @@ -279,7 +281,7 @@ export class BrowserManager { this.rawMcpClient = undefined; } - // Close transport (this terminates the npx process and browser) + // Close transport (this terminates the browser) if (this.mcpTransport) { try { await this.mcpTransport.close(); @@ -297,8 +299,7 @@ export class BrowserManager { /** * Connects to chrome-devtools-mcp which manages the browser process. * - * Spawns npx chrome-devtools-mcp with: - * - --isolated: Manages its own browser instance + * Spawns node with the bundled chrome-devtools-mcp.mjs. * - --experimental-vision: Enables visual tools (click_at, etc.) * * IMPORTANT: This does NOT use McpClientManager and does NOT register @@ -323,11 +324,7 @@ export class BrowserManager { const browserConfig = this.config.getBrowserAgentConfig(); const sessionMode = browserConfig.customConfig.sessionMode ?? 'persistent'; - const mcpArgs = [ - '-y', - `chrome-devtools-mcp@${CHROME_DEVTOOLS_MCP_VERSION}`, - '--experimental-vision', - ]; + const mcpArgs = ['--experimental-vision']; // Session mode determines how the browser is managed: // - "isolated": Temp profile, cleaned up after session (--isolated) @@ -373,15 +370,28 @@ export class BrowserManager { } debugLogger.log( - `Launching chrome-devtools-mcp (${sessionMode} mode) with args: ${mcpArgs.join(' ')}`, + `Launching bundled chrome-devtools-mcp (${sessionMode} mode) with args: ${mcpArgs.join(' ')}`, ); - // Create stdio transport to npx chrome-devtools-mcp. + // Create stdio transport to the bundled chrome-devtools-mcp. // stderr is piped (not inherited) to prevent MCP server banners and // warnings from corrupting the UI in alternate buffer mode. + let bundleMcpPath = path.resolve( + __dirname, + 'bundled/chrome-devtools-mcp.mjs', + ); + if (!fs.existsSync(bundleMcpPath)) { + bundleMcpPath = path.resolve( + __dirname, + __dirname.includes(`${path.sep}dist${path.sep}`) + ? '../../../bundled/chrome-devtools-mcp.mjs' + : '../../../dist/bundled/chrome-devtools-mcp.mjs', + ); + } + this.mcpTransport = new StdioClientTransport({ - command: process.platform === 'win32' ? 'npx.cmd' : 'npx', - args: mcpArgs, + command: 'node', + args: [bundleMcpPath, ...mcpArgs], stderr: 'pipe', }); @@ -492,8 +502,7 @@ export class BrowserManager { `Timed out connecting to Chrome: ${message}\n\n` + `Possible causes:\n` + ` 1. Chrome is not installed or not in PATH\n` + - ` 2. npx cannot download chrome-devtools-mcp (check network/proxy)\n` + - ` 3. Chrome failed to start (try setting headless: true in settings.json)`, + ` 2. Chrome failed to start (try setting headless: true in settings.json)`, ); } diff --git a/packages/core/src/agents/browser/mcpToolWrapper.test.ts b/packages/core/src/agents/browser/mcpToolWrapper.test.ts index c74f273b27..9dc2f77b1f 100644 --- a/packages/core/src/agents/browser/mcpToolWrapper.test.ts +++ b/packages/core/src/agents/browser/mcpToolWrapper.test.ts @@ -68,18 +68,19 @@ describe('mcpToolWrapper', () => { const tools = await createMcpDeclarativeTools( mockBrowserManager, mockMessageBus, + false, ); - expect(tools).toHaveLength(3); + expect(tools).toHaveLength(2); expect(tools[0].name).toBe('take_snapshot'); expect(tools[1].name).toBe('click'); - expect(tools[2].name).toBe('type_text'); }); it('should return tools with correct description', async () => { const tools = await createMcpDeclarativeTools( mockBrowserManager, mockMessageBus, + false, ); // Descriptions include augmented hints, so we check they contain the original @@ -93,6 +94,7 @@ describe('mcpToolWrapper', () => { const tools = await createMcpDeclarativeTools( mockBrowserManager, mockMessageBus, + false, ); const schema = tools[0].schema; @@ -106,6 +108,7 @@ describe('mcpToolWrapper', () => { const tools = await createMcpDeclarativeTools( mockBrowserManager, mockMessageBus, + false, ); const invocation = tools[0].build({ verbose: true }); @@ -118,6 +121,7 @@ describe('mcpToolWrapper', () => { const tools = await createMcpDeclarativeTools( mockBrowserManager, mockMessageBus, + false, ); const invocation = tools[0].build({}); @@ -131,6 +135,7 @@ describe('mcpToolWrapper', () => { const tools = await createMcpDeclarativeTools( mockBrowserManager, mockMessageBus, + false, ); const invocation = tools[1].build({ uid: 'elem-123' }); @@ -149,6 +154,7 @@ describe('mcpToolWrapper', () => { const tools = await createMcpDeclarativeTools( mockBrowserManager, mockMessageBus, + false, ); const invocation = tools[0].build({ verbose: true }); @@ -167,6 +173,7 @@ describe('mcpToolWrapper', () => { const tools = await createMcpDeclarativeTools( mockBrowserManager, mockMessageBus, + false, ); const invocation = tools[1].build({ uid: 'invalid' }); @@ -184,6 +191,7 @@ describe('mcpToolWrapper', () => { const tools = await createMcpDeclarativeTools( mockBrowserManager, mockMessageBus, + false, ); const invocation = tools[0].build({}); diff --git a/packages/core/src/agents/browser/mcpToolWrapper.ts b/packages/core/src/agents/browser/mcpToolWrapper.ts index edbff503ca..3af3f307da 100644 --- a/packages/core/src/agents/browser/mcpToolWrapper.ts +++ b/packages/core/src/agents/browser/mcpToolWrapper.ts @@ -175,144 +175,6 @@ class McpToolInvocation extends BaseToolInvocation< } } -/** - * Composite tool invocation that types a full string by calling press_key - * for each character internally, avoiding N model round-trips. - */ -class TypeTextInvocation extends BaseToolInvocation< - Record, - ToolResult -> { - constructor( - private readonly browserManager: BrowserManager, - private readonly text: string, - private readonly submitKey: string | undefined, - messageBus: MessageBus, - ) { - super({ text, submitKey }, messageBus, 'type_text', 'type_text'); - } - - getDescription(): string { - const preview = `"${this.text.substring(0, 50)}${this.text.length > 50 ? '...' : ''}"`; - return this.submitKey - ? `type_text: ${preview} + ${this.submitKey}` - : `type_text: ${preview}`; - } - - protected override async getConfirmationDetails( - _abortSignal: AbortSignal, - ): Promise { - if (!this.messageBus) { - return false; - } - - return { - type: 'mcp', - title: `Confirm Tool: type_text`, - serverName: 'browser-agent', - toolName: 'type_text', - toolDisplayName: 'type_text', - onConfirm: async (outcome: ToolConfirmationOutcome) => { - await this.publishPolicyUpdate(outcome); - }, - }; - } - - override getPolicyUpdateOptions( - _outcome: ToolConfirmationOutcome, - ): PolicyUpdateOptions | undefined { - return { - mcpName: 'browser-agent', - }; - } - - override async execute(signal: AbortSignal): Promise { - try { - if (signal.aborted) { - return { - llmContent: 'Error: Operation cancelled before typing started.', - returnDisplay: 'Operation cancelled before typing started.', - error: { message: 'Operation cancelled' }, - }; - } - - await this.typeCharByChar(signal); - - // Optionally press a submit key (Enter, Tab, etc.) after typing - if (this.submitKey && !signal.aborted) { - const keyResult = await this.browserManager.callTool( - 'press_key', - { key: this.submitKey }, - signal, - ); - if (keyResult.isError) { - const errText = this.extractErrorText(keyResult); - debugLogger.warn( - `type_text: submitKey("${this.submitKey}") failed: ${errText}`, - ); - } - } - - const summary = this.submitKey - ? `Successfully typed "${this.text}" and pressed ${this.submitKey}` - : `Successfully typed "${this.text}"`; - - return { - llmContent: summary, - returnDisplay: summary, - }; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - - // Chrome connection errors are fatal - if (errorMsg.includes('Could not connect to Chrome')) { - throw error; - } - - debugLogger.error(`type_text failed: ${errorMsg}`); - return { - llmContent: `Error: ${errorMsg}`, - returnDisplay: `Error: ${errorMsg}`, - error: { message: errorMsg }, - }; - } - } - - /** Types each character via individual press_key MCP calls. */ - private async typeCharByChar(signal: AbortSignal): Promise { - const chars = [...this.text]; // Handle Unicode correctly - for (const char of chars) { - if (signal.aborted) return; - - // Map special characters to key names - const key = char === ' ' ? 'Space' : char; - const result = await this.browserManager.callTool( - 'press_key', - { key }, - signal, - ); - - if (result.isError) { - debugLogger.warn( - `type_text: press_key("${key}") failed: ${this.extractErrorText(result)}`, - ); - } - } - } - - /** Extract error text from an MCP tool result. */ - private extractErrorText(result: McpToolCallResult): string { - return ( - result.content - ?.filter( - (c: { type: string; text?: string }) => c.type === 'text' && c.text, - ) - .map((c: { type: string; text?: string }) => c.text) - .join('\n') || 'Unknown error' - ); - } -} - /** * DeclarativeTool wrapper for an MCP tool. */ @@ -353,65 +215,6 @@ class McpDeclarativeTool extends DeclarativeTool< } } -/** - * DeclarativeTool for the custom type_text composite tool. - */ -class TypeTextDeclarativeTool extends DeclarativeTool< - Record, - ToolResult -> { - constructor( - private readonly browserManager: BrowserManager, - messageBus: MessageBus, - ) { - super( - 'type_text', - 'type_text', - 'Types a full text string into the currently focused element. ' + - 'Much faster than calling press_key for each character individually. ' + - 'Use this to enter text into form fields, search boxes, spreadsheet cells, or any focused input. ' + - 'The element must already be focused (e.g., after a click). ' + - 'Use submitKey to press a key after typing (e.g., submitKey="Enter" to submit a form or confirm a value, submitKey="Tab" to move to the next field).', - Kind.Other, - { - type: 'object', - properties: { - text: { - type: 'string', - description: 'The text to type into the focused element.', - }, - submitKey: { - type: 'string', - description: - 'Optional key to press after typing (e.g., "Enter", "Tab", "Escape"). ' + - 'Useful for submitting form fields or moving to the next cell in a spreadsheet.', - }, - }, - required: ['text'], - }, - messageBus, - /* isOutputMarkdown */ true, - /* canUpdateOutput */ false, - ); - } - - build( - params: Record, - ): ToolInvocation, ToolResult> { - const submitKey = - // eslint-disable-next-line no-restricted-syntax - typeof params['submitKey'] === 'string' && params['submitKey'] - ? params['submitKey'] - : undefined; - return new TypeTextInvocation( - this.browserManager, - String(params['text'] ?? ''), - submitKey, - this.messageBus, - ); - } -} - /** * Creates DeclarativeTool instances from dynamically discovered MCP tools, * plus custom composite tools (like type_text). @@ -423,13 +226,14 @@ class TypeTextDeclarativeTool extends DeclarativeTool< * * @param browserManager The browser manager with isolated MCP client * @param messageBus Message bus for tool invocations + * @param shouldDisableInput Whether input should be disabled for this agent * @returns Array of DeclarativeTools that dispatch to the isolated MCP client */ export async function createMcpDeclarativeTools( browserManager: BrowserManager, messageBus: MessageBus, shouldDisableInput: boolean = false, -): Promise> { +): Promise { // Get dynamically discovered tools from the MCP server const mcpTools = await browserManager.getDiscoveredTools(); @@ -438,29 +242,25 @@ export async function createMcpDeclarativeTools( (shouldDisableInput ? ' (input blocker enabled)' : ''), ); - const tools: Array = - mcpTools.map((mcpTool) => { - const schema = convertMcpToolToFunctionDeclaration(mcpTool); - // Augment description with uid-context hints - const augmentedDescription = augmentToolDescription( - mcpTool.name, - mcpTool.description ?? '', - ); - return new McpDeclarativeTool( - browserManager, - mcpTool.name, - augmentedDescription, - schema.parametersJsonSchema, - messageBus, - shouldDisableInput, - ); - }); - - // Add custom composite tools - tools.push(new TypeTextDeclarativeTool(browserManager, messageBus)); + const tools: McpDeclarativeTool[] = mcpTools.map((mcpTool) => { + const schema = convertMcpToolToFunctionDeclaration(mcpTool); + // Augment description with uid-context hints + const augmentedDescription = augmentToolDescription( + mcpTool.name, + mcpTool.description ?? '', + ); + return new McpDeclarativeTool( + browserManager, + mcpTool.name, + augmentedDescription, + schema.parametersJsonSchema, + messageBus, + shouldDisableInput, + ); + }); debugLogger.log( - `Total tools registered: ${tools.length} (${mcpTools.length} MCP + 1 custom)`, + `Total tools registered: ${tools.length} (${mcpTools.length} MCP)`, ); return tools; diff --git a/scripts/build_package.js b/scripts/build_package.js index c201333d2c..279e46fa94 100644 --- a/scripts/build_package.js +++ b/scripts/build_package.js @@ -31,6 +31,15 @@ const packageName = basename(process.cwd()); // build typescript files execSync('tsc --build', { stdio: 'inherit' }); +// Run package-specific bundling if the script exists +const bundleScript = join(process.cwd(), 'scripts', 'bundle-browser-mcp.mjs'); +if (packageName === 'core' && existsSync(bundleScript)) { + console.log('Running chrome devtools MCP bundling...'); + execSync('npm run bundle:browser-mcp', { + stdio: 'inherit', + }); +} + // copy .{md,json} files execSync('node ../../scripts/copy_files.js', { stdio: 'inherit' }); diff --git a/scripts/copy_bundle_assets.js b/scripts/copy_bundle_assets.js index 7884bf428b..dea50101ef 100644 --- a/scripts/copy_bundle_assets.js +++ b/scripts/copy_bundle_assets.js @@ -95,4 +95,12 @@ if (existsSync(devtoolsDistSrc)) { console.log('Copied devtools package to bundle/node_modules/'); } +// 6. Copy bundled chrome-devtools-mcp +const bundleMcpSrc = join(root, 'packages/core/dist/bundled'); +const bundleMcpDest = join(bundleDir, 'bundled'); +if (existsSync(bundleMcpSrc)) { + cpSync(bundleMcpSrc, bundleMcpDest, { recursive: true, dereference: true }); + console.log('Copied bundled chrome-devtools-mcp to bundle/bundled/'); +} + console.log('Assets copied to bundle/');