diff --git a/package-lock.json b/package-lock.json index 362032d42..3cc500e41 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,7 +46,6 @@ "sinon": "^22.0.0", "typescript": "^6.0.2", "typescript-eslint": "^8.43.0", - "urlpattern-polyfill": "^10.1.0", "yargs": "18.0.0" }, "engines": { @@ -1237,21 +1236,20 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" + "@protobufjs/aspromise": "^1.1.1" } }, "node_modules/@protobufjs/float": { @@ -1261,13 +1259,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.1.tgz", - "integrity": "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", @@ -6128,9 +6119,9 @@ } }, "node_modules/lighthouse/node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", + "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", "dev": true, "license": "MIT", "engines": { @@ -6961,9 +6952,9 @@ } }, "node_modules/protobufjs": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.8.tgz", - "integrity": "sha512-dvpCIeLPbXZS/Ete7yLaO7RenOdken2NHKykBXbsaGxZT0UTltcarBciw+A78SRQs9iMAAVpsYA+l8b1hTePIA==", + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz", + "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==", "dev": true, "hasInstallScript": true, "license": "BSD-3-Clause", @@ -6971,15 +6962,14 @@ "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.1", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", - "long": "^5.0.0" + "long": "^5.3.2" }, "engines": { "node": ">=12.0.0" @@ -8402,13 +8392,6 @@ "punycode": "^2.1.0" } }, - "node_modules/urlpattern-polyfill": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz", - "integrity": "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==", - "dev": true, - "license": "MIT" - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index eb3af969c..48113fb53 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,6 @@ "sinon": "^22.0.0", "typescript": "^6.0.2", "typescript-eslint": "^8.43.0", - "urlpattern-polyfill": "^10.1.0", "yargs": "18.0.0" }, "engines": { diff --git a/src/bin/chrome-devtools-mcp-cli-options.ts b/src/bin/chrome-devtools-mcp-cli-options.ts index 6c446873c..919a4b8b8 100644 --- a/src/bin/chrome-devtools-mcp-cli-options.ts +++ b/src/bin/chrome-devtools-mcp-cli-options.ts @@ -181,11 +181,6 @@ export const cliOptions = { describe: 'Whether to include all kinds of pages such as webviews or background pages as pages.', }, - experimentalNavigationAllowlist: { - type: 'boolean', - describe: 'Whether to enable navigation allowlist tool parameter.', - hidden: true, - }, experimentalInteropTools: { type: 'boolean', describe: 'Whether to enable interoperability tools', diff --git a/src/telemetry/flag_usage_metrics.json b/src/telemetry/flag_usage_metrics.json index 635009be8..ad3243e94 100644 --- a/src/telemetry/flag_usage_metrics.json +++ b/src/telemetry/flag_usage_metrics.json @@ -232,11 +232,13 @@ }, { "name": "experimental_navigation_allowlist", - "flagType": "boolean" + "flagType": "boolean", + "isDeprecated": true }, { "name": "experimental_navigation_allowlist_present", - "flagType": "boolean" + "flagType": "boolean", + "isDeprecated": true }, { "name": "experimental_page_id_routing", diff --git a/src/third_party/index.ts b/src/third_party/index.ts index da1907765..2642a089a 100644 --- a/src/third_party/index.ts +++ b/src/third_party/index.ts @@ -4,7 +4,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import 'urlpattern-polyfill'; import 'core-js/modules/es.promise.with-resolvers.js'; import 'core-js/modules/es.set.union.v2.js'; import 'core-js/proposals/iterator-helpers.js'; diff --git a/src/tools/pages.ts b/src/tools/pages.ts index 0a74aa228..eed2967db 100644 --- a/src/tools/pages.ts +++ b/src/tools/pages.ts @@ -5,11 +5,10 @@ */ import {logger} from '../logger.js'; -import type {CdpPage, Dialog, HTTPRequest} from '../third_party/index.js'; +import type {CdpPage, Dialog} from '../third_party/index.js'; import {zod} from '../third_party/index.js'; import {ToolCategory} from './categories.js'; -import type {ContextPage} from './ToolDefinition.js'; import { CLOSE_PAGE_ERROR, definePageTool, @@ -17,64 +16,6 @@ import { timeoutSchema, } from './ToolDefinition.js'; -async function navigateWithInterception( - page: ContextPage, - action: () => Promise, - allowListString?: string, - timeout?: number, -): Promise { - const allowList = allowListString - ? allowListString.split(',').map((p: string) => new URLPattern(p.trim())) - : undefined; - - const requestHandler = (interceptedRequest: HTTPRequest) => { - if (!interceptedRequest.isNavigationRequest()) { - void interceptedRequest.continue(); - return; - } - const requestUrl = interceptedRequest.url(); - const isAllowed = allowList!.some((pattern: URLPattern) => - pattern.test(requestUrl), - ); - - if (isAllowed) { - void interceptedRequest.continue(); - } else { - logger?.(`Blocking request to: ${requestUrl}`); - void interceptedRequest.abort('blockedbyclient'); - } - }; - - const cleanupInterception = async () => { - if (allowList) { - page.pptrPage.off('request', requestHandler); - await page.pptrPage.setRequestInterception(false).catch(error => { - logger?.(`Failed to disable request interception`, error); - }); - } - }; - - if (allowList) { - await page.pptrPage.setRequestInterception(true); - page.pptrPage.on('request', requestHandler); - } - - try { - await page.waitForEventsAfterAction( - async () => { - try { - await action(); - } finally { - await cleanupInterception(); - } - }, - {timeout}, - ); - } finally { - await cleanupInterception(); - } -} - export const listPages = defineTool(args => { return { name: 'list_pages', @@ -155,7 +96,7 @@ export const closePage = defineTool({ }, }); -export const newPage = defineTool(args => { +export const newPage = defineTool(() => { return { name: 'new_page', description: `Open a new tab and load a URL. Use project URL if not specified otherwise.`, @@ -179,16 +120,6 @@ export const newPage = defineTool(args => { 'Pages in the same browser context share cookies and storage. ' + 'Pages in different browser contexts are fully isolated.', ), - ...(args?.experimentalNavigationAllowlist - ? { - allowList: zod - .string() - .optional() - .describe( - 'Optional comma-separated list of URL patterns to allow. If provided, all other navigations will be blocked.', - ), - } - : {}), ...timeoutSchema, }, blockedByDialog: false, @@ -199,14 +130,13 @@ export const newPage = defineTool(args => { request.params.isolatedContext, ); - await navigateWithInterception( - page, - () => - page.pptrPage.goto(request.params.url, { + await page.waitForEventsAfterAction( + async () => { + await page.pptrPage.goto(request.params.url, { timeout: request.params.timeout, - }), - request.params.allowList, - request.params.timeout, + }); + }, + {timeout: request.params.timeout}, ); response.setIncludePages(true); @@ -215,7 +145,7 @@ export const newPage = defineTool(args => { }; }); -export const navigatePage = definePageTool(args => { +export const navigatePage = definePageTool(() => { return { name: 'navigate_page', description: `Go to a URL, or back, forward, or reload. Use project URL if not specified otherwise.`, @@ -247,16 +177,6 @@ export const navigatePage = definePageTool(args => { .describe( 'A JavaScript script to be executed on each new document before any other scripts for the next navigation.', ), - ...(args?.experimentalNavigationAllowlist - ? { - allowList: zod - .string() - .optional() - .describe( - 'Optional comma-separated list of URL patterns to allow. If provided, all other navigations will be blocked.', - ), - } - : {}), ...timeoutSchema, }, blockedByDialog: false, @@ -298,11 +218,8 @@ export const navigatePage = definePageTool(args => { initScriptId = identifier; } - page.pptrPage.on('dialog', dialogHandler); - try { - await navigateWithInterception( - page, + await page.waitForEventsAfterAction( async () => { switch (request.params.type) { case 'url': @@ -363,8 +280,7 @@ export const navigatePage = definePageTool(args => { break; } }, - request.params.allowList, - request.params.timeout, + {timeout: request.params.timeout}, ); } finally { page.pptrPage.off('dialog', dialogHandler); diff --git a/tests/tools/pagesNavigateAllowlist.test.ts b/tests/tools/pagesNavigateAllowlist.test.ts deleted file mode 100644 index 98da02d79..000000000 --- a/tests/tools/pagesNavigateAllowlist.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'node:assert'; -import {describe, it} from 'node:test'; - -import type {ParsedArguments} from '../../src/bin/chrome-devtools-mcp-cli-options.js'; -import {navigatePage} from '../../src/tools/pages.js'; -import {serverHooks} from '../server.js'; -import {withMcpContext} from '../utils.js'; - -describe('pages allowList', () => { - const server = serverHooks(); - const args = {experimentalNavigationAllowlist: true} as ParsedArguments; - - it('navigates through redirects when all URLs are allowed', async () => { - server.addRoute('/a.html', (_req, res) => { - res.writeHead(302, {Location: '/b.html'}); - res.end(); - }); - server.addRoute('/b.html', (_req, res) => { - res.writeHead(302, {Location: '/c.html'}); - res.end(); - }); - server.addHtmlRoute( - '/c.html', - '

Final Destination

', - ); - - await withMcpContext(async (response, context) => { - const page = context.getSelectedMcpPage(); - const baseUrl = server.baseUrl; - const allowList = `${baseUrl}/a.html,${baseUrl}/b.html,${baseUrl}/c.html`; - - await navigatePage(args).handler( - { - params: { - url: `${baseUrl}/a.html`, - allowList, - }, - page, - }, - response, - context, - ); - - assert.strictEqual(page.pptrPage.url(), `${baseUrl}/c.html`); - const content = await page.pptrPage.evaluate( - () => document.querySelector('h1')?.textContent, - ); - assert.strictEqual(content, 'Final Destination'); - }); - }); - - it('blocks navigation when a redirect target is not allowed', async () => { - server.addRoute('/a.html', (_req, res) => { - res.writeHead(302, {Location: '/b.html'}); - res.end(); - }); - server.addRoute('/b.html', (_req, res) => { - res.writeHead(302, {Location: '/c.html'}); - res.end(); - }); - server.addHtmlRoute( - '/c.html', - '

Final Destination

', - ); - - await withMcpContext(async (response, context) => { - const page = context.getSelectedMcpPage(); - const baseUrl = server.baseUrl; - // b.html is missing from allowList - const allowList = `${baseUrl}/a.html,${baseUrl}/c.html`; - - await navigatePage(args).handler( - { - params: { - url: `${baseUrl}/a.html`, - allowList, - timeout: 2000, // Short timeout for failure - }, - page, - }, - response, - context, - ); - - // The navigation to b.html should be blocked. - // Puppeteer's goto will likely throw a timeout or net::ERR_ABORTED error. - const url = page.pptrPage.url(); - assert.notStrictEqual(url, `${baseUrl}/c.html`); - assert.ok( - response.responseLines.some(line => - line.includes('Unable to navigate'), - ), - ); - }); - }); -});