feat(agent): replace the runtime npx for browser agent chrome devtool mcp with pre-built bundle (#22213)

Co-authored-by: Gaurav Ghosh <gaghosh@google.com>
Co-authored-by: Gaurav <39389231+gsquared94@users.noreply.github.com>
This commit is contained in:
cynthialong0-0
2026-03-16 01:05:38 -07:00
committed by GitHub
parent 17b37144a9
commit 366aa84395
12 changed files with 763 additions and 252 deletions
+1 -1
View File
@@ -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,
+503 -6
View File
@@ -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"
+3
View File
@@ -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"
@@ -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();
@@ -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."
}
]
}
@@ -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);
@@ -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<typeof import('node:fs')>();
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']),
}),
);
@@ -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)`,
);
}
@@ -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({});
@@ -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<string, unknown>,
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<ToolCallConfirmationDetails | false> {
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<ToolResult> {
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<void> {
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<string, unknown>,
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<string, unknown>,
): ToolInvocation<Record<string, unknown>, 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<Array<McpDeclarativeTool | TypeTextDeclarativeTool>> {
): Promise<McpDeclarativeTool[]> {
// 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<McpDeclarativeTool | TypeTextDeclarativeTool> =
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;
+9
View File
@@ -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' });
+8
View File
@@ -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/');