mirror of
https://github.com/ChromeDevTools/chrome-devtools-mcp.git
synced 2026-11-04 05:02:31 +00:00
feat(config): share ReadonlyServerConfig with McpContext and McpPage (#3204)
Shares `ReadonlyServerConfig` across `McpServer`, `McpContext`, and `McpPage` so page and context settings read from a single configuration source. - Passes `config: ReadonlyServerConfig` to `McpContext` and `McpPage` instead of copying individual flags (`sourceMaps`, `allowlist`, `blocklist`, `performanceCrux`, `experimentalDevToolsDebugging`, `experimentalIncludeAllPages`, `allowUnrestrictedPaths`, `categoryExtensions`, `hasNetworkBlockOrAllowlist`). - Adds `McpPage.syncSourceMaps()` to update JS and CSS source map settings on an initialized DevTools universe from `config.resolved.sourceMaps`. - Adds `McpPage.disposeCommentBridge()` and gates `ensureDevToolsCommentBridge()` in `openDevTools()` and `getDevToolsData()` on `config.resolved.devtoolsComments` so disabling `--devtools-comments` tears down active bridges without `openDevTools()` or `getDevToolsData()` re-attaching them. - `McpPage.syncSourceMaps()` and `McpPage.disposeCommentBridge()` have no production caller yet in this PR; they are called by `McpContext.onConfigChanged()` in the follow-up config reload PR. - Wires `configParser.parseConfig()` into `McpServer.from()` across the CLI (`chrome-devtools-mcp-main.ts`, which no longer exports `args`) and daemon (`daemon.ts`) entry points (while keeping `createMcpServer(serverArgs, options)` accepting `ParsedArguments`). <sub>Stack created with <a href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
This commit is contained in:
1 parent
37f1fda81b
commit
0813bf3c51
11 files changed
+367
-140
No files matched your search
+21
-37
@@ -40,6 +40,7 @@ import {
|
||||
type Root,
|
||||
type DevTools,
|
||||
} from './third_party/index.js';
|
||||
import type {ReadonlyServerConfig} from './config/ServerConfig.js';
|
||||
import {CLOSE_PAGE_ERROR} from './tools/ToolDefinition.js';
|
||||
import type {
|
||||
BrowserlessContext,
|
||||
@@ -53,28 +54,11 @@ import {McpWorker} from './McpWorker.js';
|
||||
import {getTempFilePath, resolveCanonicalPath} from './utils/files.js';
|
||||
import {escapeForLog} from './utils/logger.js';
|
||||
import {isAllowedUrl} from './utils/url.js';
|
||||
|
||||
export interface McpContextOptions {
|
||||
// Whether the DevTools windows are exposed as pages for debugging of DevTools.
|
||||
experimentalDevToolsDebugging: boolean;
|
||||
// Whether all page-like targets are exposed as pages.
|
||||
experimentalIncludeAllPages?: boolean;
|
||||
// Whether CrUX data should be fetched.
|
||||
performanceCrux: boolean;
|
||||
// Whether source maps are enabled in DevTools.
|
||||
sourceMaps?: boolean;
|
||||
// The allow list of URL patterns to allow loading resources.
|
||||
allowlist?: string[];
|
||||
// The block list of URL patterns to block loading resources.
|
||||
blocklist?: string[];
|
||||
// Whether to skip path validation when the client did not negotiate the roots
|
||||
// capability. When false (default), tools that read or write local files are
|
||||
// restricted to the OS temp directory. When true, the previous permissive
|
||||
// behavior is restored.
|
||||
allowUnrestrictedPaths?: boolean;
|
||||
config: ReadonlyServerConfig;
|
||||
// Custom navigation timeout in milliseconds to override default.
|
||||
navigationTimeout?: number;
|
||||
// Whether extension tools and targets are enabled.
|
||||
categoryExtensions?: boolean;
|
||||
// Shared heap snapshot manager to preserve loaded snapshots across browser reconnects.
|
||||
heapSnapshotManager: HeapSnapshotManager;
|
||||
// Returns the browser to use. Called by ensureBrowser(), so the browser
|
||||
@@ -122,10 +106,10 @@ export class McpContext implements Context, BrowserlessContext {
|
||||
#traceResults: TraceResult[] = [];
|
||||
|
||||
#locatorClass: typeof Locator;
|
||||
#config: ReadonlyServerConfig;
|
||||
#options: McpContextOptions;
|
||||
#heapSnapshotManager: HeapSnapshotManager;
|
||||
#roots: Root[] | undefined = undefined;
|
||||
#allowUnrestrictedPaths: boolean;
|
||||
|
||||
private constructor(
|
||||
logger: Logger,
|
||||
@@ -140,9 +124,9 @@ export class McpContext implements Context, BrowserlessContext {
|
||||
|
||||
this.logger = logger;
|
||||
this.#locatorClass = locatorClass;
|
||||
this.#config = options.config;
|
||||
this.#options = options;
|
||||
this.#heapSnapshotManager = options.heapSnapshotManager;
|
||||
this.#allowUnrestrictedPaths = options.allowUnrestrictedPaths ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -333,7 +317,10 @@ export class McpContext implements Context, BrowserlessContext {
|
||||
// If the client never negotiated roots and the operator has explicitly
|
||||
// opted into unrestricted access via --allow-unrestricted-paths, restore
|
||||
// the previous permissive behavior and skip validation.
|
||||
if (this.#roots === undefined && this.#allowUnrestrictedPaths) {
|
||||
if (
|
||||
this.#roots === undefined &&
|
||||
this.#config.resolved.allowUnrestrictedPaths
|
||||
) {
|
||||
// Canonical path might not exist yet so we fallback to
|
||||
// path.resolve(filePath). Consumers should not follow symlinks.
|
||||
return canonicalPath || path.resolve(filePath);
|
||||
@@ -429,10 +416,6 @@ export class McpContext implements Context, BrowserlessContext {
|
||||
await page.close();
|
||||
}
|
||||
|
||||
get #hasNetworkBlockOrAllowlist(): boolean {
|
||||
return !!(this.#options.allowlist || this.#options.blocklist);
|
||||
}
|
||||
|
||||
async installPWA(options: InstallPWAOptions): Promise<string> {
|
||||
try {
|
||||
return await this.browser.installPWA(options);
|
||||
@@ -478,7 +461,7 @@ export class McpContext implements Context, BrowserlessContext {
|
||||
}
|
||||
|
||||
isCruxEnabled(): boolean {
|
||||
return this.#options.performanceCrux;
|
||||
return this.#config.resolved.performanceCrux;
|
||||
}
|
||||
|
||||
getPages(): McpPage[] {
|
||||
@@ -672,13 +655,12 @@ export class McpContext implements Context, BrowserlessContext {
|
||||
(this.#selectedPage?.target === target ? this.#selectedPage : undefined);
|
||||
if (!mcpPage) {
|
||||
mcpPage = new McpPage(target, nextPageId++, {
|
||||
config: this.#config,
|
||||
locatorClass: this.#locatorClass,
|
||||
hasNetworkBlockOrAllowlist: this.#hasNetworkBlockOrAllowlist,
|
||||
isolatedContextName: this.#getBrowserContextToNameMap().get(
|
||||
target.browserContext(),
|
||||
),
|
||||
navigationTimeout: this.#options.navigationTimeout,
|
||||
sourceMaps: this.#options.sourceMaps,
|
||||
});
|
||||
}
|
||||
this.#mcpPages.set(target, mcpPage);
|
||||
@@ -736,14 +718,14 @@ export class McpContext implements Context, BrowserlessContext {
|
||||
(this.#selectedPage?.target === target ? this.#selectedPage : undefined);
|
||||
const url = existingMcpPage ? existingMcpPage.url() : target.url();
|
||||
if (
|
||||
!this.#options.experimentalDevToolsDebugging &&
|
||||
!this.#config.resolved.experimentalDevtools &&
|
||||
url.startsWith('devtools://')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!isAllowedUrl(url, {
|
||||
categoryExtensions: this.#options.categoryExtensions,
|
||||
categoryExtensions: this.#config.resolved.categoryExtensions,
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
@@ -753,13 +735,13 @@ export class McpContext implements Context, BrowserlessContext {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
this.#options.experimentalIncludeAllPages &&
|
||||
this.#config.resolved.experimentalIncludeAllPages &&
|
||||
(type === 'background_page' || type === 'webview')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
this.#options.experimentalDevToolsDebugging &&
|
||||
this.#config.resolved.experimentalDevtools &&
|
||||
type === 'other' &&
|
||||
url.startsWith('devtools://')
|
||||
) {
|
||||
@@ -995,10 +977,11 @@ export class McpContext implements Context, BrowserlessContext {
|
||||
}
|
||||
|
||||
#validateUrlNotBlocked(url: URL): void {
|
||||
if (!this.#options.blocklist) {
|
||||
const blocklist = this.#config.resolved.blockedUrlPattern;
|
||||
if (!blocklist) {
|
||||
return;
|
||||
}
|
||||
for (const block of this.#options.blocklist) {
|
||||
for (const block of blocklist) {
|
||||
const pattern = new URLPattern(block);
|
||||
if (pattern.test(url)) {
|
||||
throw new Error(`Blocked by blocklist: ${url}`);
|
||||
@@ -1007,10 +990,11 @@ export class McpContext implements Context, BrowserlessContext {
|
||||
}
|
||||
|
||||
#validateUrlAllowed(url: URL): void {
|
||||
if (!this.#options.allowlist) {
|
||||
const allowlist = this.#config.resolved.allowedUrlPattern;
|
||||
if (!allowlist) {
|
||||
return;
|
||||
}
|
||||
for (const allow of this.#options.allowlist) {
|
||||
for (const allow of allowlist) {
|
||||
const pattern = new URLPattern(allow);
|
||||
if (pattern.test(url)) {
|
||||
return;
|
||||
|
||||
+37
-11
@@ -4,6 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {ReadonlyServerConfig} from './config/ServerConfig.js';
|
||||
import {DevToolsCommentBridge} from './devtools/DevToolsCommentBridge.js';
|
||||
import {
|
||||
createTargetUniverse,
|
||||
@@ -177,27 +178,24 @@ export class McpPage implements ContextPage {
|
||||
#networkCollector?: NetworkCollector;
|
||||
#consoleCollector?: ConsoleCollector;
|
||||
|
||||
#hasNetworkBlockOrAllowlist: boolean;
|
||||
#locatorClass: typeof Locator;
|
||||
#navigationTimeout: number;
|
||||
#sourceMaps: boolean;
|
||||
#config: ReadonlyServerConfig;
|
||||
#commentBridge?: DevToolsCommentBridge;
|
||||
|
||||
constructor(
|
||||
target: Target,
|
||||
id: number,
|
||||
options: {
|
||||
hasNetworkBlockOrAllowlist: boolean;
|
||||
config: ReadonlyServerConfig;
|
||||
locatorClass: typeof Locator;
|
||||
isolatedContextName?: string;
|
||||
navigationTimeout?: number;
|
||||
sourceMaps?: boolean;
|
||||
},
|
||||
) {
|
||||
this.#hasNetworkBlockOrAllowlist = options.hasNetworkBlockOrAllowlist;
|
||||
this.#config = options.config;
|
||||
this.#locatorClass = options.locatorClass;
|
||||
this.#navigationTimeout = options.navigationTimeout ?? NAVIGATION_TIMEOUT;
|
||||
this.#sourceMaps = options.sourceMaps ?? true;
|
||||
this.target = target;
|
||||
this.id = id;
|
||||
this.isolatedContextName = options.isolatedContextName;
|
||||
@@ -206,6 +204,13 @@ export class McpPage implements ContextPage {
|
||||
};
|
||||
}
|
||||
|
||||
get #hasNetworkBlockOrAllowlist(): boolean {
|
||||
return Boolean(
|
||||
this.#config.resolved.allowedUrlPattern ||
|
||||
this.#config.resolved.blockedUrlPattern,
|
||||
);
|
||||
}
|
||||
|
||||
get pptrPage(): Page {
|
||||
if (!this.#pptrPage) {
|
||||
throw new Error(
|
||||
@@ -351,13 +356,26 @@ export class McpPage implements ContextPage {
|
||||
try {
|
||||
const session = await this.pptrPage.createCDPSession();
|
||||
this.#devtoolsUniverse = await createTargetUniverse(session, {
|
||||
sourceMaps: this.#sourceMaps,
|
||||
sourceMaps: this.#config.resolved.sourceMaps,
|
||||
});
|
||||
} catch (e) {
|
||||
logger?.('Failed to initialize DevTools universe', e);
|
||||
}
|
||||
}
|
||||
|
||||
syncSourceMaps(): void {
|
||||
if (!this.#devtoolsUniverse) {
|
||||
return;
|
||||
}
|
||||
const sourceMaps = this.#config.resolved.sourceMaps;
|
||||
this.#devtoolsUniverse.universe.settings
|
||||
.resolve(DevTools.SDKSettings.jsSourceMapsEnabledSettingDescriptor)
|
||||
.set(sourceMaps);
|
||||
this.#devtoolsUniverse.universe.settings
|
||||
.resolve(DevTools.SDKSettings.cssSourceMapsEnabledSettingDescriptor)
|
||||
.set(sourceMaps);
|
||||
}
|
||||
|
||||
get devtoolsUniverse(): TargetUniverse | undefined {
|
||||
return this.#devtoolsUniverse;
|
||||
}
|
||||
@@ -548,6 +566,11 @@ export class McpPage implements ContextPage {
|
||||
return this.#commentBridge;
|
||||
}
|
||||
|
||||
disposeCommentBridge(): void {
|
||||
this.#commentBridge?.dispose();
|
||||
this.#commentBridge = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the comment bridge attached to the DevTools page, or undefined if
|
||||
* the DevTools page does not support DevTools comments.
|
||||
@@ -578,7 +601,9 @@ export class McpPage implements ContextPage {
|
||||
|
||||
async openDevTools(): Promise<Page | undefined> {
|
||||
const devtoolsPage = await this.pptrPage.openDevTools();
|
||||
await this.ensureDevToolsCommentBridge(devtoolsPage);
|
||||
if (this.#config.resolved.devtoolsComments) {
|
||||
await this.ensureDevToolsCommentBridge(devtoolsPage);
|
||||
}
|
||||
return devtoolsPage;
|
||||
}
|
||||
|
||||
@@ -661,8 +686,7 @@ export class McpPage implements ContextPage {
|
||||
|
||||
dispose(): void {
|
||||
this.#disposed = true;
|
||||
this.#commentBridge?.dispose();
|
||||
this.#commentBridge = undefined;
|
||||
this.disposeCommentBridge();
|
||||
if (this.#screenRecorder) {
|
||||
const {recorder} = this.#screenRecorder;
|
||||
this.#screenRecorder = null;
|
||||
@@ -1074,7 +1098,9 @@ export class McpPage implements ContextPage {
|
||||
logger?.('No DevTools page detected');
|
||||
return {};
|
||||
}
|
||||
await this.ensureDevToolsCommentBridge(devtoolsPage);
|
||||
if (this.#config.resolved.devtoolsComments) {
|
||||
await this.ensureDevToolsCommentBridge(devtoolsPage);
|
||||
}
|
||||
const {cdpRequestId, cdpBackendNodeId} = await devtoolsPage.evaluate(
|
||||
async () => {
|
||||
// @ts-expect-error no types
|
||||
|
||||
@@ -27,7 +27,8 @@ await checkForUpdates(
|
||||
);
|
||||
|
||||
const configParser = new ConfigParser(VERSION);
|
||||
export const args = configParser.parse();
|
||||
const serverConfig = configParser.parseConfig();
|
||||
const args = serverConfig.resolved;
|
||||
|
||||
const logFile = args.logFile ? saveLogsToFile(args.logFile) : undefined;
|
||||
|
||||
@@ -36,10 +37,11 @@ setupUnhandledRejectionHandler(() => {
|
||||
});
|
||||
|
||||
logger?.(`Starting Chrome DevTools MCP Server v${VERSION}`);
|
||||
// BrowserManager only reads non-reloadable launch and connection options.
|
||||
const browserManager = new BrowserManager(args, {
|
||||
logFile,
|
||||
});
|
||||
const serverPromise = McpServer.from(args, {
|
||||
const serverPromise = McpServer.from(serverConfig, {
|
||||
browserManager,
|
||||
});
|
||||
|
||||
|
||||
@@ -141,12 +141,13 @@ let server: Server | null = null;
|
||||
async function setupMCPServer(): Promise<void> {
|
||||
logger?.(`Starting Chrome DevTools MCP Server v${VERSION}`);
|
||||
const configParser = new ConfigParser(VERSION);
|
||||
const args = configParser.parse();
|
||||
const serverConfig = configParser.parseConfig();
|
||||
const args = serverConfig.resolved;
|
||||
const logFile = args.logFile ? saveLogsToFile(args.logFile) : undefined;
|
||||
const browserManager = new BrowserManager(args, {
|
||||
logFile,
|
||||
});
|
||||
mcpServer = await McpServer.from(args, {
|
||||
mcpServer = await McpServer.from(serverConfig, {
|
||||
browserManager,
|
||||
logFile,
|
||||
});
|
||||
|
||||
+41
-34
@@ -11,6 +11,7 @@ import {pathToFileURL} from 'node:url';
|
||||
import {BrowserManager} from './BrowserManager.js';
|
||||
import {type ParsedArguments} from './config/ConfigParser.js';
|
||||
import {DEFAULT_FILESYSTEM_ROOT} from './config/mcp-options.js';
|
||||
import {ServerConfig} from './config/ServerConfig.js';
|
||||
import {loadIssueDescriptions} from './devtools/issueDescriptions.js';
|
||||
import {McpContext} from './McpContext.js';
|
||||
import {HeapSnapshotManager} from './processors/HeapSnapshotManager.js';
|
||||
@@ -55,7 +56,7 @@ interface ToolEntry {
|
||||
|
||||
export class McpServer {
|
||||
readonly server: SdkMcpServer;
|
||||
#serverArgs: ParsedArguments;
|
||||
#config: ServerConfig;
|
||||
#browserManager: BrowserManager;
|
||||
#context: McpContext;
|
||||
#heapSnapshotManager = new HeapSnapshotManager();
|
||||
@@ -63,7 +64,7 @@ export class McpServer {
|
||||
/**
|
||||
* Client roots stay valid across browser reconnects and only the client can
|
||||
* invalidate them through a `roots/list_changed` notification. CLI-configured
|
||||
* roots are read from `#serverArgs` when combining roots.
|
||||
* roots are read from `#config` when combining roots.
|
||||
*/
|
||||
#lastClientRoots?: Root[];
|
||||
/**
|
||||
@@ -74,19 +75,19 @@ export class McpServer {
|
||||
#toolMutex = new Mutex();
|
||||
#tools = new Map<string, ToolEntry>();
|
||||
|
||||
private constructor(serverArgs: ParsedArguments, options: McpServerOptions) {
|
||||
this.#serverArgs = serverArgs;
|
||||
private constructor(config: ServerConfig, options: McpServerOptions) {
|
||||
this.#config = config;
|
||||
this.#browserManager = options.browserManager;
|
||||
const serverArgs = this.#config.resolved;
|
||||
|
||||
if (this.#serverArgs.usageStatistics) {
|
||||
if (serverArgs.usageStatistics) {
|
||||
ClearcutLogger.initialize({
|
||||
persistence: new FilePersistence(),
|
||||
logFile: this.#serverArgs.logFile,
|
||||
logFile: serverArgs.logFile,
|
||||
appVersion: VERSION,
|
||||
clearcutEndpoint: this.#serverArgs.clearcutEndpoint,
|
||||
clearcutForceFlushIntervalMs:
|
||||
this.#serverArgs.clearcutForceFlushIntervalMs,
|
||||
clearcutIncludePidHeader: this.#serverArgs.clearcutIncludePidHeader,
|
||||
clearcutEndpoint: serverArgs.clearcutEndpoint,
|
||||
clearcutForceFlushIntervalMs: serverArgs.clearcutForceFlushIntervalMs,
|
||||
clearcutIncludePidHeader: serverArgs.clearcutIncludePidHeader,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -104,15 +105,7 @@ export class McpServer {
|
||||
);
|
||||
|
||||
this.#context = McpContext.create(logger, {
|
||||
experimentalDevToolsDebugging:
|
||||
this.#serverArgs.experimentalDevtools ?? false,
|
||||
experimentalIncludeAllPages: this.#serverArgs.experimentalIncludeAllPages,
|
||||
performanceCrux: this.#serverArgs.performanceCrux,
|
||||
sourceMaps: this.#serverArgs.sourceMaps,
|
||||
allowlist: this.#serverArgs.allowedUrlPattern,
|
||||
blocklist: this.#serverArgs.blockedUrlPattern,
|
||||
allowUnrestrictedPaths: this.#serverArgs.allowUnrestrictedPaths,
|
||||
categoryExtensions: this.#serverArgs.categoryExtensions,
|
||||
config: this.#config,
|
||||
heapSnapshotManager: this.#heapSnapshotManager,
|
||||
getBrowser: () => this.#browserManager.ensureBrowser(),
|
||||
});
|
||||
@@ -136,9 +129,9 @@ export class McpServer {
|
||||
},
|
||||
);
|
||||
} else if (
|
||||
!this.#serverArgs.allowUnrestrictedPaths &&
|
||||
((this.#serverArgs.filesystemRoot ?? []).length === 0 ||
|
||||
this.#serverArgs.filesystemRoot === DEFAULT_FILESYSTEM_ROOT)
|
||||
!this.#config.resolved.allowUnrestrictedPaths &&
|
||||
((this.#config.resolved.filesystemRoot ?? []).length === 0 ||
|
||||
this.#config.resolved.filesystemRoot === DEFAULT_FILESYSTEM_ROOT)
|
||||
) {
|
||||
console.warn(
|
||||
'[chrome-devtools-mcp] The connecting client did not negotiate the MCP roots ' +
|
||||
@@ -218,26 +211,26 @@ export class McpServer {
|
||||
}
|
||||
|
||||
static async from(
|
||||
serverArgs: ParsedArguments,
|
||||
config: ServerConfig,
|
||||
options: McpServerOptions,
|
||||
): Promise<McpServer> {
|
||||
const server = new McpServer(serverArgs, options);
|
||||
const server = new McpServer(config, options);
|
||||
await server.#init();
|
||||
return server;
|
||||
}
|
||||
|
||||
async #init(): Promise<void> {
|
||||
for (const tool of createTools(this.#serverArgs)) {
|
||||
// Tool definitions take a ParsedArguments snapshot; reload recreates them.
|
||||
for (const tool of createTools(this.#config.resolved)) {
|
||||
this.#registerTool(tool);
|
||||
}
|
||||
await loadIssueDescriptions();
|
||||
}
|
||||
|
||||
#combinedRoots(): Root[] | undefined {
|
||||
const serverArgs = this.#config.resolved;
|
||||
const configuredRoots = (
|
||||
this.#serverArgs.allowUnrestrictedPaths
|
||||
? []
|
||||
: (this.#serverArgs.filesystemRoot ?? [])
|
||||
serverArgs.allowUnrestrictedPaths ? [] : (serverArgs.filesystemRoot ?? [])
|
||||
).map(root => {
|
||||
const rootPath = path.resolve(root);
|
||||
return {
|
||||
@@ -284,9 +277,11 @@ export class McpServer {
|
||||
}
|
||||
|
||||
#createToolHandler(tool: AnyToolDefinition): ToolHandler {
|
||||
// No production code calls ServerConfig.commit() yet; a follow-up PR passes
|
||||
// ReadonlyServerConfig to ToolHandler.
|
||||
return new ToolHandler(
|
||||
tool,
|
||||
this.#serverArgs,
|
||||
this.#config.resolved,
|
||||
() => this.#getContext(),
|
||||
this.#toolMutex,
|
||||
() => this.#browserManager.abandonPendingAttempt(),
|
||||
@@ -319,7 +314,7 @@ export class McpServer {
|
||||
*
|
||||
* Maintained as a public API for backwards compatibility because external
|
||||
* consumers and integrations rely on `createMcpServer()`. For new code,
|
||||
* prefer using `McpServer.from(serverArgs, options)`.
|
||||
* prefer using `McpServer.from(config, options)`.
|
||||
*/
|
||||
export async function createMcpServer(
|
||||
serverArgs: ParsedArguments,
|
||||
@@ -327,12 +322,24 @@ export async function createMcpServer(
|
||||
logFile?: fs.WriteStream;
|
||||
},
|
||||
): Promise<{server: SdkMcpServer}> {
|
||||
// BrowserManager only reads non-reloadable browser launch and connection
|
||||
// options.
|
||||
const browserManager = new BrowserManager(serverArgs, {
|
||||
logFile: options.logFile,
|
||||
});
|
||||
const server = await McpServer.from(serverArgs, {
|
||||
browserManager,
|
||||
...options,
|
||||
});
|
||||
// createMcpServer takes a flat ParsedArguments snapshot and only returns
|
||||
// SdkMcpServer, so instances created here are never reloaded.
|
||||
const server = await McpServer.from(
|
||||
new ServerConfig({
|
||||
cliArgs: {},
|
||||
fileArgs: {},
|
||||
configPath: undefined,
|
||||
resolved: serverArgs,
|
||||
}),
|
||||
{
|
||||
browserManager,
|
||||
...options,
|
||||
},
|
||||
);
|
||||
return {server: server.server};
|
||||
}
|
||||
+19
-13
@@ -36,6 +36,7 @@ import {
|
||||
createMockPuppeteerBrowser,
|
||||
createMockPuppeteerPage,
|
||||
createMockPuppeteerTarget,
|
||||
createMockServerConfig,
|
||||
} from './mocks.js';
|
||||
import {serverHooks, TestServer} from './server.js';
|
||||
import {
|
||||
@@ -80,8 +81,7 @@ describe('McpContext', () => {
|
||||
return await createConnectedMcpContext(
|
||||
browser,
|
||||
{
|
||||
experimentalDevToolsDebugging: false,
|
||||
performanceCrux: true,
|
||||
config: createMockServerConfig(),
|
||||
heapSnapshotManager: new HeapSnapshotManager(),
|
||||
},
|
||||
Locator,
|
||||
@@ -95,8 +95,9 @@ describe('McpContext', () => {
|
||||
const context = await createConnectedMcpContext(
|
||||
browser,
|
||||
{
|
||||
experimentalDevToolsDebugging: false,
|
||||
performanceCrux: false,
|
||||
config: createMockServerConfig({
|
||||
performanceCrux: false,
|
||||
}),
|
||||
heapSnapshotManager: new HeapSnapshotManager(),
|
||||
},
|
||||
Locator,
|
||||
@@ -141,8 +142,9 @@ describe('McpContext', () => {
|
||||
const context = await createConnectedMcpContext(
|
||||
browser,
|
||||
{
|
||||
experimentalDevToolsDebugging: false,
|
||||
performanceCrux: false,
|
||||
config: createMockServerConfig({
|
||||
performanceCrux: false,
|
||||
}),
|
||||
heapSnapshotManager: new HeapSnapshotManager(),
|
||||
},
|
||||
Locator,
|
||||
@@ -603,8 +605,9 @@ describe('McpContext', () => {
|
||||
it('continues page ids across contexts so stale ids do not resolve', async () => {
|
||||
await withBrowser(async browser => {
|
||||
const options = {
|
||||
experimentalDevToolsDebugging: false,
|
||||
performanceCrux: false,
|
||||
config: createMockServerConfig({
|
||||
performanceCrux: false,
|
||||
}),
|
||||
heapSnapshotManager: new HeapSnapshotManager(),
|
||||
};
|
||||
const first = await createConnectedMcpContext(browser, options, Locator);
|
||||
@@ -648,8 +651,9 @@ describe('McpContext', () => {
|
||||
return McpContext.create(
|
||||
undefined,
|
||||
{
|
||||
experimentalDevToolsDebugging: false,
|
||||
performanceCrux: false,
|
||||
config: createMockServerConfig({
|
||||
performanceCrux: false,
|
||||
}),
|
||||
heapSnapshotManager: new HeapSnapshotManager(),
|
||||
getBrowser,
|
||||
},
|
||||
@@ -1169,10 +1173,12 @@ describe('McpContext', () => {
|
||||
return await createConnectedMcpContext(
|
||||
browser,
|
||||
{
|
||||
experimentalDevToolsDebugging: false,
|
||||
performanceCrux: false,
|
||||
config: createMockServerConfig({
|
||||
performanceCrux: false,
|
||||
allowedUrlPattern: options.allowlist,
|
||||
blockedUrlPattern: options.blocklist,
|
||||
}),
|
||||
heapSnapshotManager: new HeapSnapshotManager(),
|
||||
...options,
|
||||
},
|
||||
Locator,
|
||||
);
|
||||
|
||||
+194
-22
@@ -9,6 +9,7 @@ import {describe, it} from 'node:test';
|
||||
|
||||
import sinon from 'sinon';
|
||||
|
||||
import type {ReadonlyServerConfig} from '../src/config/ServerConfig.js';
|
||||
import type {TargetUniverse} from '../src/devtools/DevtoolsUtils.js';
|
||||
import {McpPage, replaceHtmlElementsWithUids} from '../src/McpPage.js';
|
||||
import {DevTools, Locator, ScreenRecorder} from '../src/third_party/index.js';
|
||||
@@ -16,10 +17,12 @@ import type {JSONSchema7Definition} from '../src/third_party/index.js';
|
||||
import {TextSnapshot} from '../src/TextSnapshot.js';
|
||||
import type {TextSnapshotNode} from '../src/types.js';
|
||||
import {
|
||||
createMockCDPSession,
|
||||
createMockDialog,
|
||||
createMockMcpResponse,
|
||||
createMockPuppeteerPage,
|
||||
createMockPuppeteerTarget,
|
||||
createMockServerConfig,
|
||||
createMockWebMCPTool,
|
||||
} from './mocks.js';
|
||||
import {serverHooks} from './server.js';
|
||||
@@ -274,14 +277,12 @@ describe('replaceHtmlElementsWithUids', () => {
|
||||
});
|
||||
|
||||
describe('McpPage', () => {
|
||||
async function createMcpPage(
|
||||
options: {hasNetworkBlockOrAllowlist?: boolean} = {},
|
||||
) {
|
||||
async function createMcpPage(options: {config?: ReadonlyServerConfig} = {}) {
|
||||
const pptrPage = createMockPuppeteerPage();
|
||||
pptrPage.emulateFocusedPage.resolves();
|
||||
const target = createMockPuppeteerTarget({page: pptrPage});
|
||||
const mcpPage = new McpPage(target, 1, {
|
||||
hasNetworkBlockOrAllowlist: options.hasNetworkBlockOrAllowlist ?? false,
|
||||
config: options.config ?? createMockServerConfig(),
|
||||
locatorClass: Locator,
|
||||
});
|
||||
const mockSession = {
|
||||
@@ -317,7 +318,7 @@ describe('McpPage', () => {
|
||||
title: 'Target Title',
|
||||
});
|
||||
const mcpPage = new McpPage(target, 1, {
|
||||
hasNetworkBlockOrAllowlist: false,
|
||||
config: createMockServerConfig(),
|
||||
locatorClass: Locator,
|
||||
});
|
||||
|
||||
@@ -347,7 +348,7 @@ describe('McpPage', () => {
|
||||
target.page.resolves(null);
|
||||
|
||||
const mcpPage = new McpPage(target, 1, {
|
||||
hasNetworkBlockOrAllowlist: false,
|
||||
config: createMockServerConfig(),
|
||||
locatorClass: Locator,
|
||||
});
|
||||
|
||||
@@ -361,7 +362,7 @@ describe('McpPage', () => {
|
||||
it('disposes safely before init() is called and rejects subsequent init()', async () => {
|
||||
const target = createMockPuppeteerTarget();
|
||||
const mcpPage = new McpPage(target, 1, {
|
||||
hasNetworkBlockOrAllowlist: false,
|
||||
config: createMockServerConfig(),
|
||||
locatorClass: Locator,
|
||||
});
|
||||
|
||||
@@ -379,7 +380,7 @@ describe('McpPage', () => {
|
||||
const pptrPage = createMockPuppeteerPage();
|
||||
const target = createMockPuppeteerTarget({page: pptrPage});
|
||||
const mcpPage = new McpPage(target, 1, {
|
||||
hasNetworkBlockOrAllowlist: false,
|
||||
config: createMockServerConfig(),
|
||||
locatorClass: Locator,
|
||||
});
|
||||
|
||||
@@ -395,7 +396,7 @@ describe('McpPage', () => {
|
||||
it('stops and clears an active screenRecorder on dispose()', async () => {
|
||||
const target = createMockPuppeteerTarget();
|
||||
const mcpPage = new McpPage(target, 1, {
|
||||
hasNetworkBlockOrAllowlist: false,
|
||||
config: createMockServerConfig(),
|
||||
locatorClass: Locator,
|
||||
});
|
||||
const recorder = sinon.createStubInstance(ScreenRecorder);
|
||||
@@ -413,7 +414,7 @@ describe('McpPage', () => {
|
||||
function createPage(url: string, title: string): McpPage {
|
||||
const target = createMockPuppeteerTarget({url, title});
|
||||
return new McpPage(target, 1, {
|
||||
hasNetworkBlockOrAllowlist: false,
|
||||
config: createMockServerConfig(),
|
||||
locatorClass: Locator,
|
||||
});
|
||||
}
|
||||
@@ -513,7 +514,9 @@ describe('McpPage', () => {
|
||||
|
||||
it('throws when networkConditions is set with network blocking enabled', async () => {
|
||||
const {mcpPage, pptrPage} = await createMcpPage({
|
||||
hasNetworkBlockOrAllowlist: true,
|
||||
config: createMockServerConfig({
|
||||
blockedUrlPattern: ['*://blocked.example.com/*'],
|
||||
}),
|
||||
});
|
||||
await assert.rejects(
|
||||
() => mcpPage.emulate({networkConditions: 'Slow 3G'}),
|
||||
@@ -1010,7 +1013,9 @@ describe('McpPage', () => {
|
||||
|
||||
describe('DevToolsCommentBridge lifecycle', () => {
|
||||
it('does not create commentBridge on construction or getDevToolsPage', async () => {
|
||||
const {mcpPage, pptrPage} = await createMcpPage();
|
||||
const {mcpPage, pptrPage} = await createMcpPage({
|
||||
config: createMockServerConfig({devtoolsComments: true}),
|
||||
});
|
||||
pptrPage.hasDevTools.resolves(true);
|
||||
const devtoolsPage = createMockPuppeteerPage();
|
||||
pptrPage.openDevTools.resolves(devtoolsPage);
|
||||
@@ -1023,7 +1028,9 @@ describe('McpPage', () => {
|
||||
});
|
||||
|
||||
it('creates and attaches commentBridge when openDevTools is called', async () => {
|
||||
const {mcpPage, pptrPage} = await createMcpPage();
|
||||
const {mcpPage, pptrPage} = await createMcpPage({
|
||||
config: createMockServerConfig({devtoolsComments: true}),
|
||||
});
|
||||
const devtoolsPage = createMockPuppeteerPage();
|
||||
devtoolsPage.evaluate.resolves(true);
|
||||
pptrPage.openDevTools.resolves(devtoolsPage);
|
||||
@@ -1036,6 +1043,26 @@ describe('McpPage', () => {
|
||||
sinon.assert.calledOnce(devtoolsPage.exposeFunction);
|
||||
});
|
||||
|
||||
it('does not attach commentBridge in openDevTools() when devtoolsComments is disabled', async () => {
|
||||
const config = createMockServerConfig({devtoolsComments: true});
|
||||
const {mcpPage, pptrPage} = await createMcpPage({config});
|
||||
const devtoolsPage = createMockPuppeteerPage();
|
||||
devtoolsPage.evaluate.resolves(true);
|
||||
pptrPage.openDevTools.resolves(devtoolsPage);
|
||||
|
||||
await mcpPage.openDevTools();
|
||||
assert.notStrictEqual(mcpPage.commentBridge, undefined);
|
||||
|
||||
config.commit(createMockServerConfig());
|
||||
mcpPage.disposeCommentBridge();
|
||||
devtoolsPage.exposeFunction.resetHistory();
|
||||
|
||||
const result = await mcpPage.openDevTools();
|
||||
assert.strictEqual(result, devtoolsPage);
|
||||
assert.strictEqual(mcpPage.commentBridge, undefined);
|
||||
sinon.assert.notCalled(devtoolsPage.exposeFunction);
|
||||
});
|
||||
|
||||
it('returns the commentBridge when the DevTools page has a CD4A bridge', async () => {
|
||||
const {mcpPage} = await createMcpPage();
|
||||
const devtoolsPage = createMockPuppeteerPage();
|
||||
@@ -1059,25 +1086,27 @@ describe('McpPage', () => {
|
||||
});
|
||||
|
||||
it('disposes commentBridge on mcpPage.dispose()', async () => {
|
||||
const {mcpPage, pptrPage} = await createMcpPage();
|
||||
const {mcpPage, pptrPage} = await createMcpPage({
|
||||
config: createMockServerConfig({devtoolsComments: true}),
|
||||
});
|
||||
const devtoolsPage = createMockPuppeteerPage();
|
||||
pptrPage.openDevTools.resolves(devtoolsPage);
|
||||
|
||||
await mcpPage.openDevTools();
|
||||
const bridge = mcpPage.commentBridge;
|
||||
assert.notStrictEqual(bridge, undefined);
|
||||
assert.ok(bridge);
|
||||
|
||||
if (bridge) {
|
||||
const disposeSpy = sinon.spy(bridge, 'dispose');
|
||||
mcpPage.dispose();
|
||||
const disposeSpy = sinon.spy(bridge, 'dispose');
|
||||
mcpPage.dispose();
|
||||
|
||||
sinon.assert.calledOnce(disposeSpy);
|
||||
assert.strictEqual(mcpPage.commentBridge, undefined);
|
||||
}
|
||||
sinon.assert.calledOnce(disposeSpy);
|
||||
assert.strictEqual(mcpPage.commentBridge, undefined);
|
||||
});
|
||||
|
||||
it('creates and attaches commentBridge when getDevToolsData is called', async () => {
|
||||
const {mcpPage, pptrPage} = await createMcpPage();
|
||||
const {mcpPage, pptrPage} = await createMcpPage({
|
||||
config: createMockServerConfig({devtoolsComments: true}),
|
||||
});
|
||||
pptrPage.hasDevTools.resolves(true);
|
||||
const devtoolsPage = createMockPuppeteerPage();
|
||||
devtoolsPage.evaluate.resolves(true);
|
||||
@@ -1098,6 +1127,149 @@ describe('McpPage', () => {
|
||||
sinon.assert.calledOnce(devtoolsPage.exposeFunction);
|
||||
sinon.assert.calledThrice(devtoolsPage.evaluate);
|
||||
});
|
||||
|
||||
it('disposes and clears commentBridge on mcpPage.disposeCommentBridge()', async () => {
|
||||
const {mcpPage, pptrPage} = await createMcpPage({
|
||||
config: createMockServerConfig({devtoolsComments: true}),
|
||||
});
|
||||
const devtoolsPage = createMockPuppeteerPage();
|
||||
pptrPage.openDevTools.resolves(devtoolsPage);
|
||||
|
||||
await mcpPage.openDevTools();
|
||||
const bridge = mcpPage.commentBridge;
|
||||
assert.ok(bridge);
|
||||
|
||||
const disposeSpy = sinon.spy(bridge, 'dispose');
|
||||
mcpPage.disposeCommentBridge();
|
||||
|
||||
sinon.assert.calledOnceWithExactly(disposeSpy);
|
||||
assert.strictEqual(mcpPage.commentBridge, undefined);
|
||||
});
|
||||
|
||||
it('does not re-attach commentBridge in getDevToolsData() when devtoolsComments is disabled', async () => {
|
||||
const config = createMockServerConfig({devtoolsComments: true});
|
||||
const {mcpPage, pptrPage} = await createMcpPage({config});
|
||||
pptrPage.hasDevTools.resolves(true);
|
||||
const devtoolsPage = createMockPuppeteerPage();
|
||||
devtoolsPage.evaluate.resolves(true);
|
||||
pptrPage.openDevTools.resolves(devtoolsPage);
|
||||
|
||||
await mcpPage.openDevTools();
|
||||
assert.notStrictEqual(mcpPage.commentBridge, undefined);
|
||||
|
||||
config.commit(createMockServerConfig());
|
||||
mcpPage.disposeCommentBridge();
|
||||
assert.strictEqual(mcpPage.commentBridge, undefined);
|
||||
devtoolsPage.exposeFunction.resetHistory();
|
||||
devtoolsPage.evaluate.reset();
|
||||
devtoolsPage.evaluate.resolves({
|
||||
cdpRequestId: 'req-2',
|
||||
cdpBackendNodeId: 20,
|
||||
});
|
||||
|
||||
const data = await mcpPage.getDevToolsData();
|
||||
|
||||
assert.deepStrictEqual(data, {
|
||||
cdpRequestId: 'req-2',
|
||||
cdpBackendNodeId: 20,
|
||||
});
|
||||
assert.strictEqual(mcpPage.commentBridge, undefined);
|
||||
sinon.assert.notCalled(devtoolsPage.exposeFunction);
|
||||
sinon.assert.calledOnce(devtoolsPage.evaluate);
|
||||
});
|
||||
|
||||
it('re-attaches commentBridge when ensureDevToolsCommentBridge() is called after disposeCommentBridge()', async () => {
|
||||
const {mcpPage, pptrPage} = await createMcpPage({
|
||||
config: createMockServerConfig({devtoolsComments: true}),
|
||||
});
|
||||
const devtoolsPage = createMockPuppeteerPage();
|
||||
devtoolsPage.evaluate.resolves(true);
|
||||
pptrPage.openDevTools.resolves(devtoolsPage);
|
||||
|
||||
await mcpPage.openDevTools();
|
||||
mcpPage.disposeCommentBridge();
|
||||
assert.strictEqual(mcpPage.commentBridge, undefined);
|
||||
|
||||
const bridge = await mcpPage.ensureDevToolsCommentBridge(devtoolsPage);
|
||||
|
||||
assert.notStrictEqual(bridge, undefined);
|
||||
assert.strictEqual(bridge, mcpPage.commentBridge);
|
||||
});
|
||||
});
|
||||
|
||||
describe('syncSourceMaps()', () => {
|
||||
it('does nothing before init() initializes the DevTools universe', () => {
|
||||
const config = createMockServerConfig({sourceMaps: false});
|
||||
const target = createMockPuppeteerTarget();
|
||||
const mcpPage = new McpPage(target, 1, {
|
||||
config,
|
||||
locatorClass: Locator,
|
||||
});
|
||||
|
||||
mcpPage.syncSourceMaps();
|
||||
|
||||
assert.strictEqual(mcpPage.devtoolsUniverse, undefined);
|
||||
sinon.assert.notCalled(target.page);
|
||||
});
|
||||
|
||||
it('updates JS and CSS source map settings on an initialized DevTools universe', async () => {
|
||||
const config = createMockServerConfig();
|
||||
const pptrPage = createMockPuppeteerPage();
|
||||
pptrPage.createCDPSession.resolves(createMockCDPSession());
|
||||
const target = createMockPuppeteerTarget({page: pptrPage});
|
||||
const mcpPage = new McpPage(target, 1, {
|
||||
config,
|
||||
locatorClass: Locator,
|
||||
});
|
||||
await mcpPage.init();
|
||||
|
||||
config.commit(createMockServerConfig({sourceMaps: false}));
|
||||
mcpPage.syncSourceMaps();
|
||||
|
||||
const settings = mcpPage.devtoolsUniverse?.universe.settings;
|
||||
assert.ok(settings);
|
||||
assert.strictEqual(
|
||||
settings
|
||||
.resolve(DevTools.SDKSettings.jsSourceMapsEnabledSettingDescriptor)
|
||||
.get(),
|
||||
false,
|
||||
);
|
||||
assert.strictEqual(
|
||||
settings
|
||||
.resolve(DevTools.SDKSettings.cssSourceMapsEnabledSettingDescriptor)
|
||||
.get(),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('reads updated sourceMaps from config when initialized later', async () => {
|
||||
const config = createMockServerConfig();
|
||||
const pptrPage = createMockPuppeteerPage();
|
||||
pptrPage.createCDPSession.resolves(createMockCDPSession());
|
||||
const target = createMockPuppeteerTarget({page: pptrPage});
|
||||
const mcpPage = new McpPage(target, 1, {
|
||||
config,
|
||||
locatorClass: Locator,
|
||||
});
|
||||
|
||||
config.commit(createMockServerConfig({sourceMaps: false}));
|
||||
await mcpPage.init();
|
||||
|
||||
const settings = mcpPage.devtoolsUniverse?.universe.settings;
|
||||
assert.ok(settings);
|
||||
assert.strictEqual(
|
||||
settings
|
||||
.resolve(DevTools.SDKSettings.jsSourceMapsEnabledSettingDescriptor)
|
||||
.get(),
|
||||
false,
|
||||
);
|
||||
assert.strictEqual(
|
||||
settings
|
||||
.resolve(DevTools.SDKSettings.cssSourceMapsEnabledSettingDescriptor)
|
||||
.get(),
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveBackendNodeId()', () => {
|
||||
|
||||
@@ -30,6 +30,12 @@ describe('McpServer', () => {
|
||||
ClearcutLogger.resetForTesting();
|
||||
});
|
||||
|
||||
function parseConfig(argv: string[] = []) {
|
||||
return new ConfigParser('1.0.0', ['node', 'script.js', ...argv], {
|
||||
CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS: 'true',
|
||||
}).parseConfig();
|
||||
}
|
||||
|
||||
async function createTestServer(
|
||||
extraArgs: string[] = [],
|
||||
{mockContext = true}: {mockContext?: boolean} = {},
|
||||
@@ -45,14 +51,8 @@ describe('McpServer', () => {
|
||||
sinon.stub(McpContext, 'create').returns(context);
|
||||
}
|
||||
|
||||
const serverArgs = new ConfigParser(
|
||||
'1.0.0',
|
||||
['node', 'script.js', ...extraArgs],
|
||||
{
|
||||
CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS: 'true',
|
||||
},
|
||||
).parse();
|
||||
const server = await McpServer.from(serverArgs, {browserManager});
|
||||
const config = parseConfig(extraArgs);
|
||||
const server = await McpServer.from(config, {browserManager});
|
||||
return {server, browserManager, context};
|
||||
}
|
||||
|
||||
|
||||
+23
-1
@@ -1304,6 +1304,16 @@ export function createMockContextAnalysisResult(): DevTools.HeapSnapshotModel.He
|
||||
};
|
||||
}
|
||||
|
||||
export function stripUndefined<T extends object>(options: T): T {
|
||||
const result = {...options};
|
||||
for (const [key, value] of Object.entries(result)) {
|
||||
if (value === undefined) {
|
||||
Reflect.deleteProperty(result, key);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function createMockParsedArguments(
|
||||
options: Partial<ParsedArguments> = {},
|
||||
): ParsedArguments {
|
||||
@@ -1311,7 +1321,19 @@ export function createMockParsedArguments(
|
||||
{},
|
||||
{suppressUsageStatisticsNotice: true},
|
||||
);
|
||||
return {...defaultArgs, ...options};
|
||||
return {...defaultArgs, ...stripUndefined(options)};
|
||||
}
|
||||
|
||||
export function createMockServerConfig(
|
||||
options: Partial<ParsedArguments> = {},
|
||||
): ServerConfig {
|
||||
const definedOptions = stripUndefined(options);
|
||||
return new ServerConfig({
|
||||
cliArgs: definedOptions,
|
||||
fileArgs: {},
|
||||
configPath: definedOptions.config,
|
||||
resolved: createMockParsedArguments(definedOptions),
|
||||
});
|
||||
}
|
||||
|
||||
export function createMockHeapSnapshotNode(
|
||||
|
||||
+4
-3
@@ -18,7 +18,7 @@ import {HeapSnapshotManager} from '../src/processors/HeapSnapshotManager.js';
|
||||
import {resolveCanonicalPath} from '../src/utils/files.js';
|
||||
import {escapeForLog} from '../src/utils/logger.js';
|
||||
|
||||
import {createMockPuppeteerBrowser} from './mocks.js';
|
||||
import {createMockPuppeteerBrowser, createMockServerConfig} from './mocks.js';
|
||||
import {
|
||||
createConnectedMcpContext,
|
||||
createTempDir,
|
||||
@@ -173,8 +173,9 @@ describe('McpContext path validation escaping', () => {
|
||||
const browser = createMockPuppeteerBrowser();
|
||||
browser.targets.returns([]);
|
||||
return await createConnectedMcpContext(browser, {
|
||||
experimentalDevToolsDebugging: false,
|
||||
performanceCrux: false,
|
||||
config: createMockServerConfig({
|
||||
performanceCrux: false,
|
||||
}),
|
||||
heapSnapshotManager: new HeapSnapshotManager(),
|
||||
});
|
||||
}
|
||||
|
||||
+13
-7
@@ -41,7 +41,9 @@ import {stableIdSymbol} from '../src/utils/id.js';
|
||||
import {
|
||||
createMockParsedArguments,
|
||||
createMockPuppeteerPage,
|
||||
createMockServerConfig,
|
||||
mockListener,
|
||||
stripUndefined,
|
||||
} from './mocks.js';
|
||||
|
||||
export function assertNoServiceWorkerReported(targets: Target[], id: string) {
|
||||
@@ -212,6 +214,16 @@ export async function withMcpContext(
|
||||
McpContext.resetPageIdsForTesting();
|
||||
McpContext.resetWorkerIdsForTesting();
|
||||
const parsedArgs = createMockParsedArguments(args);
|
||||
const serverConfig = createMockServerConfig({
|
||||
...args,
|
||||
...stripUndefined({
|
||||
performanceCrux: options.performanceCrux,
|
||||
sourceMaps: options.sourceMaps,
|
||||
allowedUrlPattern: options.allowedUrlPattern,
|
||||
blockedUrlPattern: options.blockedUrlPattern,
|
||||
allowUnrestrictedPaths: options.allowUnrestrictedPaths,
|
||||
}),
|
||||
});
|
||||
const response = new McpResponse(parsedArgs);
|
||||
const heapSnapshotManager = new HeapSnapshotManager();
|
||||
if (context) {
|
||||
@@ -220,16 +232,10 @@ export async function withMcpContext(
|
||||
context = await createConnectedMcpContext(
|
||||
browser,
|
||||
{
|
||||
experimentalDevToolsDebugging: false,
|
||||
performanceCrux: options.performanceCrux ?? true,
|
||||
sourceMaps: options.sourceMaps ?? true,
|
||||
allowlist: options.allowedUrlPattern,
|
||||
blocklist: options.blockedUrlPattern,
|
||||
allowUnrestrictedPaths: options.allowUnrestrictedPaths ?? false,
|
||||
config: serverConfig,
|
||||
navigationTimeout:
|
||||
options.navigationTimeout ??
|
||||
(process.platform === 'win32' ? 20000 : undefined),
|
||||
categoryExtensions: parsedArgs.categoryExtensions,
|
||||
heapSnapshotManager,
|
||||
},
|
||||
Locator,
|
||||
|
||||
Reference in new issue
Block a user