项目文件夹

文件
Alex Rudenko d50255778a feat: support experimental allowlist for navigate tool calls (#1935)
This PR adds an experimental allowlist for the page navigate tool call
(requires `--experimentalNavigationAllowlist`, off by default). The
feature uses a list of URLPatterns to decline navigations that land on
disallowed URLs. If that happens, the client can update the allowlist
and re-try. The purpose of the this feature is to offer additional
guardrails on top of the MCP server. It does not restrict subresources
or JS/iframe navigations in any way. The performance impact is minimized
by turning off interception as soon as the navigation request is done.
2026-04-22 12:16:10 +00:00

103 行
2.9 KiB
TypeScript

/**
* @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',
'<html><body><h1>Final Destination</h1></body></html>',
);
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',
'<html><body><h1>Final Destination</h1></body></html>',
);
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'),
),
);
});
});
});