chromedevtools--chrome-devtools-mcp
4637ab9389
This allows the MCP server to call the in-page tools provided by the inspected page. Handling of (non-serializable) DOM elements as tool parameters or tool output will be added in follow-ups.
938 行
29 KiB
TypeScript
938 行
29 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import type {ParsedArguments} from './bin/chrome-devtools-mcp-cli-options.js';
|
|
import {ConsoleFormatter} from './formatters/ConsoleFormatter.js';
|
|
import {IssueFormatter} from './formatters/IssueFormatter.js';
|
|
import {NetworkFormatter} from './formatters/NetworkFormatter.js';
|
|
import {SnapshotFormatter} from './formatters/SnapshotFormatter.js';
|
|
import type {McpContext} from './McpContext.js';
|
|
import type {McpPage} from './McpPage.js';
|
|
import {UncaughtError} from './PageCollector.js';
|
|
import {DevTools, type Protocol} from './third_party/index.js';
|
|
import type {
|
|
ConsoleMessage,
|
|
ImageContent,
|
|
Page,
|
|
ResourceType,
|
|
TextContent,
|
|
} from './third_party/index.js';
|
|
import type {ToolGroup, ToolDefinition} from './tools/inPage.js';
|
|
import {handleDialog} from './tools/pages.js';
|
|
import type {
|
|
DevToolsData,
|
|
ImageContentData,
|
|
LighthouseData,
|
|
Response,
|
|
SnapshotParams,
|
|
} from './tools/ToolDefinition.js';
|
|
import type {InsightName, TraceResult} from './trace-processing/parse.js';
|
|
import {getInsightOutput, getTraceSummary} from './trace-processing/parse.js';
|
|
import type {InstalledExtension} from './utils/ExtensionRegistry.js';
|
|
import {paginate} from './utils/pagination.js';
|
|
import type {PaginationOptions} from './utils/types.js';
|
|
|
|
interface TraceInsightData {
|
|
trace: TraceResult;
|
|
insightSetId: string;
|
|
insightName: InsightName;
|
|
}
|
|
|
|
async function getToolGroup(
|
|
page: McpPage,
|
|
): Promise<ToolGroup<ToolDefinition> | undefined> {
|
|
// Check if there is a `devtoolstooldiscovery` event listener
|
|
const windowHandle = await page.pptrPage.evaluateHandle(() => window);
|
|
// @ts-expect-error internal API
|
|
const client = page.pptrPage._client();
|
|
const {listeners}: {listeners: Protocol.DOMDebugger.EventListener[]} =
|
|
await client.send('DOMDebugger.getEventListeners', {
|
|
objectId: windowHandle.remoteObject().objectId,
|
|
});
|
|
if (listeners.find(l => l.type === 'devtoolstooldiscovery') === undefined) {
|
|
return;
|
|
}
|
|
|
|
const toolGroup = await page.pptrPage.evaluate(() => {
|
|
return new Promise<ToolGroup<ToolDefinition> | undefined>(resolve => {
|
|
const event = new CustomEvent('devtoolstooldiscovery');
|
|
// @ts-expect-error Adding custom property
|
|
event.respondWith = (toolGroup: ToolGroup) => {
|
|
if (!window.__dtmcp) {
|
|
window.__dtmcp = {};
|
|
}
|
|
window.__dtmcp.toolGroup = toolGroup;
|
|
|
|
// When receiving a toolGroup for the first time, expose a simple execution helper
|
|
if (!window.__dtmcp.executeTool) {
|
|
window.__dtmcp.executeTool = async (toolName, args) => {
|
|
if (!window.__dtmcp?.toolGroup) {
|
|
throw new Error('No tools found on the page');
|
|
}
|
|
const tool = window.__dtmcp.toolGroup.tools.find(
|
|
t => t.name === toolName,
|
|
);
|
|
if (!tool) {
|
|
throw new Error(`Tool ${toolName} not found`);
|
|
}
|
|
return await tool.execute(args);
|
|
};
|
|
}
|
|
|
|
resolve(toolGroup);
|
|
};
|
|
window.dispatchEvent(event);
|
|
// If the page does not synchronously call `event.respondWith`, return instead of timing out
|
|
setTimeout(() => {
|
|
resolve(undefined);
|
|
}, 0);
|
|
});
|
|
});
|
|
return toolGroup;
|
|
}
|
|
|
|
export class McpResponse implements Response {
|
|
#includePages = false;
|
|
#includeExtensionServiceWorkers = false;
|
|
#includeExtensionPages = false;
|
|
#snapshotParams?: SnapshotParams;
|
|
#attachedNetworkRequestId?: number;
|
|
#attachedNetworkRequestOptions?: {
|
|
requestFilePath?: string;
|
|
responseFilePath?: string;
|
|
};
|
|
#attachedConsoleMessageId?: number;
|
|
#attachedTraceSummary?: TraceResult;
|
|
#attachedTraceInsight?: TraceInsightData;
|
|
#attachedLighthouseResult?: LighthouseData;
|
|
#textResponseLines: string[] = [];
|
|
#images: ImageContentData[] = [];
|
|
#networkRequestsOptions?: {
|
|
include: boolean;
|
|
pagination?: PaginationOptions;
|
|
resourceTypes?: ResourceType[];
|
|
includePreservedRequests?: boolean;
|
|
networkRequestIdInDevToolsUI?: number;
|
|
};
|
|
#consoleDataOptions?: {
|
|
include: boolean;
|
|
pagination?: PaginationOptions;
|
|
types?: string[];
|
|
includePreservedMessages?: boolean;
|
|
};
|
|
#listExtensions?: boolean;
|
|
#listInPageTools?: boolean;
|
|
#devToolsData?: DevToolsData;
|
|
#tabId?: string;
|
|
#args: ParsedArguments;
|
|
#page?: McpPage;
|
|
|
|
constructor(args: ParsedArguments) {
|
|
this.#args = args;
|
|
}
|
|
|
|
setPage(page: McpPage): void {
|
|
this.#page = page;
|
|
}
|
|
|
|
attachDevToolsData(data: DevToolsData): void {
|
|
this.#devToolsData = data;
|
|
}
|
|
|
|
setTabId(tabId: string): void {
|
|
this.#tabId = tabId;
|
|
}
|
|
|
|
setIncludePages(value: boolean): void {
|
|
this.#includePages = value;
|
|
|
|
if (this.#args.categoryExtensions) {
|
|
this.#includeExtensionServiceWorkers = value;
|
|
this.#includeExtensionPages = value;
|
|
}
|
|
}
|
|
|
|
includeSnapshot(params?: SnapshotParams): void {
|
|
this.#snapshotParams = params ?? {
|
|
verbose: false,
|
|
};
|
|
}
|
|
|
|
setListExtensions(): void {
|
|
this.#listExtensions = true;
|
|
}
|
|
|
|
setListInPageTools(): void {
|
|
if (this.#args.categoryInPageTools) {
|
|
this.#listInPageTools = true;
|
|
}
|
|
}
|
|
|
|
setIncludeNetworkRequests(
|
|
value: boolean,
|
|
options?: PaginationOptions & {
|
|
resourceTypes?: ResourceType[];
|
|
includePreservedRequests?: boolean;
|
|
networkRequestIdInDevToolsUI?: number;
|
|
},
|
|
): void {
|
|
if (!value) {
|
|
this.#networkRequestsOptions = undefined;
|
|
return;
|
|
}
|
|
|
|
this.#networkRequestsOptions = {
|
|
include: value,
|
|
pagination:
|
|
options?.pageSize || options?.pageIdx
|
|
? {
|
|
pageSize: options.pageSize,
|
|
pageIdx: options.pageIdx,
|
|
}
|
|
: undefined,
|
|
resourceTypes: options?.resourceTypes,
|
|
includePreservedRequests: options?.includePreservedRequests,
|
|
networkRequestIdInDevToolsUI: options?.networkRequestIdInDevToolsUI,
|
|
};
|
|
}
|
|
|
|
setIncludeConsoleData(
|
|
value: boolean,
|
|
options?: PaginationOptions & {
|
|
types?: string[];
|
|
includePreservedMessages?: boolean;
|
|
},
|
|
): void {
|
|
if (!value) {
|
|
this.#consoleDataOptions = undefined;
|
|
return;
|
|
}
|
|
|
|
this.#consoleDataOptions = {
|
|
include: value,
|
|
pagination:
|
|
options?.pageSize || options?.pageIdx
|
|
? {
|
|
pageSize: options.pageSize,
|
|
pageIdx: options.pageIdx,
|
|
}
|
|
: undefined,
|
|
types: options?.types,
|
|
includePreservedMessages: options?.includePreservedMessages,
|
|
};
|
|
}
|
|
|
|
attachNetworkRequest(
|
|
reqId: number,
|
|
options?: {requestFilePath?: string; responseFilePath?: string},
|
|
): void {
|
|
this.#attachedNetworkRequestId = reqId;
|
|
this.#attachedNetworkRequestOptions = options;
|
|
}
|
|
|
|
attachConsoleMessage(msgid: number): void {
|
|
this.#attachedConsoleMessageId = msgid;
|
|
}
|
|
|
|
attachTraceSummary(result: TraceResult): void {
|
|
this.#attachedTraceSummary = result;
|
|
}
|
|
|
|
attachTraceInsight(
|
|
trace: TraceResult,
|
|
insightSetId: string,
|
|
insightName: InsightName,
|
|
): void {
|
|
this.#attachedTraceInsight = {
|
|
trace,
|
|
insightSetId,
|
|
insightName,
|
|
};
|
|
}
|
|
|
|
attachLighthouseResult(result: LighthouseData): void {
|
|
this.#attachedLighthouseResult = result;
|
|
}
|
|
|
|
get includePages(): boolean {
|
|
return this.#includePages;
|
|
}
|
|
|
|
get attachedTraceSummary(): TraceResult | undefined {
|
|
return this.#attachedTraceSummary;
|
|
}
|
|
|
|
get attachedTracedInsight(): TraceInsightData | undefined {
|
|
return this.#attachedTraceInsight;
|
|
}
|
|
|
|
get attachedLighthouseResult(): LighthouseData | undefined {
|
|
return this.#attachedLighthouseResult;
|
|
}
|
|
|
|
get includeNetworkRequests(): boolean {
|
|
return this.#networkRequestsOptions?.include ?? false;
|
|
}
|
|
|
|
get includeConsoleData(): boolean {
|
|
return this.#consoleDataOptions?.include ?? false;
|
|
}
|
|
get attachedNetworkRequestId(): number | undefined {
|
|
return this.#attachedNetworkRequestId;
|
|
}
|
|
get networkRequestsPageIdx(): number | undefined {
|
|
return this.#networkRequestsOptions?.pagination?.pageIdx;
|
|
}
|
|
get consoleMessagesPageIdx(): number | undefined {
|
|
return this.#consoleDataOptions?.pagination?.pageIdx;
|
|
}
|
|
get consoleMessagesTypes(): string[] | undefined {
|
|
return this.#consoleDataOptions?.types;
|
|
}
|
|
|
|
appendResponseLine(value: string): void {
|
|
this.#textResponseLines.push(value);
|
|
}
|
|
|
|
attachImage(value: ImageContentData): void {
|
|
this.#images.push(value);
|
|
}
|
|
|
|
get responseLines(): readonly string[] {
|
|
return this.#textResponseLines;
|
|
}
|
|
|
|
get images(): ImageContentData[] {
|
|
return this.#images;
|
|
}
|
|
|
|
get snapshotParams(): SnapshotParams | undefined {
|
|
return this.#snapshotParams;
|
|
}
|
|
|
|
async handle(
|
|
toolName: string,
|
|
context: McpContext,
|
|
): Promise<{
|
|
content: Array<TextContent | ImageContent>;
|
|
structuredContent: object;
|
|
}> {
|
|
if (this.#includePages) {
|
|
await context.createPagesSnapshot();
|
|
}
|
|
|
|
if (this.#includeExtensionServiceWorkers) {
|
|
await context.createExtensionServiceWorkersSnapshot();
|
|
}
|
|
|
|
let snapshot: SnapshotFormatter | string | undefined;
|
|
if (this.#snapshotParams) {
|
|
if (!this.#page) {
|
|
throw new Error('Response must have a page');
|
|
}
|
|
await context.createTextSnapshot(
|
|
this.#page,
|
|
this.#snapshotParams.verbose,
|
|
this.#devToolsData,
|
|
);
|
|
const textSnapshot = this.#page.textSnapshot;
|
|
if (textSnapshot) {
|
|
const formatter = new SnapshotFormatter(textSnapshot);
|
|
if (this.#snapshotParams.filePath) {
|
|
await context.saveFile(
|
|
new TextEncoder().encode(formatter.toString()),
|
|
this.#snapshotParams.filePath,
|
|
);
|
|
snapshot = this.#snapshotParams.filePath;
|
|
} else {
|
|
snapshot = formatter;
|
|
}
|
|
}
|
|
}
|
|
|
|
let detailedNetworkRequest: NetworkFormatter | undefined;
|
|
if (this.#attachedNetworkRequestId) {
|
|
if (!this.#page) {
|
|
throw new Error(`Response must have an McpPage`);
|
|
}
|
|
const request = context.getNetworkRequestById(
|
|
this.#page,
|
|
this.#attachedNetworkRequestId,
|
|
);
|
|
const formatter = await NetworkFormatter.from(request, {
|
|
requestId: this.#attachedNetworkRequestId,
|
|
requestIdResolver: req => context.getNetworkRequestStableId(req),
|
|
fetchData: true,
|
|
requestFilePath: this.#attachedNetworkRequestOptions?.requestFilePath,
|
|
responseFilePath: this.#attachedNetworkRequestOptions?.responseFilePath,
|
|
saveFile: (data, filename) => context.saveFile(data, filename),
|
|
});
|
|
detailedNetworkRequest = formatter;
|
|
}
|
|
|
|
let detailedConsoleMessage: ConsoleFormatter | IssueFormatter | undefined;
|
|
|
|
if (this.#attachedConsoleMessageId) {
|
|
if (!this.#page) {
|
|
throw new Error(`Response must have an McpPage`);
|
|
}
|
|
|
|
const message = context.getConsoleMessageById(
|
|
this.#page,
|
|
this.#attachedConsoleMessageId,
|
|
);
|
|
const consoleMessageStableId = this.#attachedConsoleMessageId;
|
|
if ('args' in message || message instanceof UncaughtError) {
|
|
const consoleMessage = message as ConsoleMessage | UncaughtError;
|
|
const devTools = context.getDevToolsUniverse(this.#page);
|
|
detailedConsoleMessage = await ConsoleFormatter.from(consoleMessage, {
|
|
id: consoleMessageStableId,
|
|
fetchDetailedData: true,
|
|
devTools: devTools ?? undefined,
|
|
});
|
|
} else if (message instanceof DevTools.AggregatedIssue) {
|
|
const formatter = new IssueFormatter(message, {
|
|
id: consoleMessageStableId,
|
|
requestIdResolver: context.resolveCdpRequestId.bind(
|
|
context,
|
|
this.#page,
|
|
),
|
|
elementIdResolver: context.resolveCdpElementId.bind(
|
|
context,
|
|
this.#page,
|
|
),
|
|
});
|
|
if (!formatter.isValid()) {
|
|
throw new Error(
|
|
"Can't provide details for the msgid " + consoleMessageStableId,
|
|
);
|
|
}
|
|
detailedConsoleMessage = formatter;
|
|
}
|
|
}
|
|
|
|
let extensions: InstalledExtension[] | undefined;
|
|
if (this.#listExtensions) {
|
|
extensions = context.listExtensions();
|
|
}
|
|
|
|
let inPageTools: ToolGroup<ToolDefinition> | undefined;
|
|
if (this.#listInPageTools) {
|
|
inPageTools = await getToolGroup(context.getSelectedMcpPage());
|
|
context.setInPageTools(inPageTools);
|
|
}
|
|
|
|
let consoleMessages: Array<ConsoleFormatter | IssueFormatter> | undefined;
|
|
if (this.#consoleDataOptions?.include) {
|
|
if (!this.#page) {
|
|
throw new Error(`Response must have an McpPage`);
|
|
}
|
|
const page = this.#page;
|
|
let messages = context.getConsoleData(
|
|
this.#page,
|
|
this.#consoleDataOptions.includePreservedMessages,
|
|
);
|
|
|
|
if (this.#consoleDataOptions.types?.length) {
|
|
const normalizedTypes = new Set(this.#consoleDataOptions.types);
|
|
messages = messages.filter(message => {
|
|
if ('type' in message) {
|
|
return normalizedTypes.has(message.type());
|
|
}
|
|
if (message instanceof DevTools.AggregatedIssue) {
|
|
return normalizedTypes.has('issue');
|
|
}
|
|
return normalizedTypes.has('error');
|
|
});
|
|
}
|
|
|
|
consoleMessages = (
|
|
await Promise.all(
|
|
messages.map(
|
|
async (item): Promise<ConsoleFormatter | IssueFormatter | null> => {
|
|
const consoleMessageStableId =
|
|
context.getConsoleMessageStableId(item);
|
|
if ('args' in item || item instanceof UncaughtError) {
|
|
const consoleMessage = item as ConsoleMessage | UncaughtError;
|
|
const devTools = context.getDevToolsUniverse(page);
|
|
return await ConsoleFormatter.from(consoleMessage, {
|
|
id: consoleMessageStableId,
|
|
fetchDetailedData: false,
|
|
devTools: devTools ?? undefined,
|
|
});
|
|
}
|
|
if (item instanceof DevTools.AggregatedIssue) {
|
|
const formatter = new IssueFormatter(item, {
|
|
id: consoleMessageStableId,
|
|
});
|
|
if (!formatter.isValid()) {
|
|
return null;
|
|
}
|
|
return formatter;
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
)
|
|
).filter(item => item !== null);
|
|
}
|
|
|
|
let networkRequests: NetworkFormatter[] | undefined;
|
|
if (this.#networkRequestsOptions?.include) {
|
|
if (!this.#page) {
|
|
throw new Error(`Response must have an McpPage`);
|
|
}
|
|
let requests = context.getNetworkRequests(
|
|
this.#page,
|
|
this.#networkRequestsOptions?.includePreservedRequests,
|
|
);
|
|
|
|
// Apply resource type filtering if specified
|
|
if (this.#networkRequestsOptions.resourceTypes?.length) {
|
|
const normalizedTypes = new Set(
|
|
this.#networkRequestsOptions.resourceTypes,
|
|
);
|
|
requests = requests.filter(request => {
|
|
const type = request.resourceType();
|
|
return normalizedTypes.has(type);
|
|
});
|
|
}
|
|
|
|
if (requests.length) {
|
|
networkRequests = await Promise.all(
|
|
requests.map(request =>
|
|
NetworkFormatter.from(request, {
|
|
requestId: context.getNetworkRequestStableId(request),
|
|
selectedInDevToolsUI:
|
|
context.getNetworkRequestStableId(request) ===
|
|
this.#networkRequestsOptions?.networkRequestIdInDevToolsUI,
|
|
fetchData: false,
|
|
saveFile: (data, filename) => context.saveFile(data, filename),
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
return this.format(toolName, context, {
|
|
detailedConsoleMessage,
|
|
consoleMessages,
|
|
snapshot,
|
|
detailedNetworkRequest,
|
|
networkRequests,
|
|
traceInsight: this.#attachedTraceInsight,
|
|
traceSummary: this.#attachedTraceSummary,
|
|
extensions,
|
|
lighthouseResult: this.#attachedLighthouseResult,
|
|
inPageTools,
|
|
});
|
|
}
|
|
|
|
format(
|
|
toolName: string,
|
|
context: McpContext,
|
|
data: {
|
|
detailedConsoleMessage: ConsoleFormatter | IssueFormatter | undefined;
|
|
consoleMessages: Array<ConsoleFormatter | IssueFormatter> | undefined;
|
|
snapshot: SnapshotFormatter | string | undefined;
|
|
detailedNetworkRequest?: NetworkFormatter;
|
|
networkRequests?: NetworkFormatter[];
|
|
traceSummary?: TraceResult;
|
|
traceInsight?: TraceInsightData;
|
|
extensions?: InstalledExtension[];
|
|
lighthouseResult?: LighthouseData;
|
|
inPageTools?: ToolGroup<ToolDefinition>;
|
|
},
|
|
): {content: Array<TextContent | ImageContent>; structuredContent: object} {
|
|
const structuredContent: {
|
|
snapshot?: object;
|
|
snapshotFilePath?: string;
|
|
tabId?: string;
|
|
networkRequest?: object;
|
|
networkRequests?: object[];
|
|
consoleMessage?: object;
|
|
consoleMessages?: object[];
|
|
traceSummary?: string;
|
|
traceInsights?: Array<{insightName: string; insightKey: string}>;
|
|
lighthouseResult?: object;
|
|
extensions?: object[];
|
|
inPageTools?: object;
|
|
message?: string;
|
|
networkConditions?: string;
|
|
navigationTimeout?: number;
|
|
viewport?: object;
|
|
userAgent?: string;
|
|
cpuThrottlingRate?: number;
|
|
colorScheme?: string;
|
|
dialog?: {
|
|
type: string;
|
|
message: string;
|
|
defaultValue?: string;
|
|
};
|
|
pages?: object[];
|
|
pagination?: object;
|
|
extensionServiceWorkers?: object[];
|
|
extensionPages?: object[];
|
|
} = {};
|
|
|
|
const response = [];
|
|
if (this.#textResponseLines.length) {
|
|
structuredContent.message = this.#textResponseLines.join('\n');
|
|
response.push(...this.#textResponseLines);
|
|
}
|
|
|
|
const networkConditions = this.#page?.networkConditions;
|
|
if (networkConditions) {
|
|
const timeout = this.#page!.pptrPage.getDefaultNavigationTimeout();
|
|
response.push(`Emulating network conditions: ${networkConditions}`);
|
|
response.push(`Default navigation timeout set to ${timeout} ms`);
|
|
structuredContent.networkConditions = networkConditions;
|
|
structuredContent.navigationTimeout = timeout;
|
|
}
|
|
|
|
const viewport = this.#page?.viewport;
|
|
if (viewport) {
|
|
response.push(`Emulating viewport: ${JSON.stringify(viewport)}`);
|
|
structuredContent.viewport = viewport;
|
|
}
|
|
|
|
const userAgent = this.#page?.userAgent;
|
|
if (userAgent) {
|
|
response.push(`Emulating user agent: ${userAgent}`);
|
|
structuredContent.userAgent = userAgent;
|
|
}
|
|
|
|
const cpuThrottlingRate = this.#page?.cpuThrottlingRate ?? 1;
|
|
if (cpuThrottlingRate > 1) {
|
|
response.push(`Emulating CPU throttling: ${cpuThrottlingRate}x slowdown`);
|
|
structuredContent.cpuThrottlingRate = cpuThrottlingRate;
|
|
}
|
|
|
|
const colorScheme = this.#page?.colorScheme;
|
|
if (colorScheme) {
|
|
response.push(`Emulating color scheme: ${colorScheme}`);
|
|
structuredContent.colorScheme = colorScheme;
|
|
}
|
|
|
|
const dialog = this.#page?.getDialog();
|
|
if (dialog) {
|
|
const defaultValueIfNeeded =
|
|
dialog.type() === 'prompt'
|
|
? ` (default value: "${dialog.defaultValue()}")`
|
|
: '';
|
|
response.push(`# Open dialog
|
|
${dialog.type()}: ${dialog.message()}${defaultValueIfNeeded}.
|
|
Call ${handleDialog.name} to handle it before continuing.`);
|
|
structuredContent.dialog = {
|
|
type: dialog.type(),
|
|
message: dialog.message(),
|
|
defaultValue: dialog.defaultValue(),
|
|
};
|
|
}
|
|
|
|
if (this.#includePages) {
|
|
const allPages = context.getPages();
|
|
|
|
const {regularPages, extensionPages} = allPages.reduce(
|
|
(acc: {regularPages: Page[]; extensionPages: Page[]}, page: Page) => {
|
|
if (page.url().startsWith('chrome-extension://')) {
|
|
acc.extensionPages.push(page);
|
|
} else {
|
|
acc.regularPages.push(page);
|
|
}
|
|
return acc;
|
|
},
|
|
{regularPages: [], extensionPages: []},
|
|
);
|
|
|
|
if (regularPages.length) {
|
|
const parts = [`## Pages`];
|
|
const structuredPages = [];
|
|
for (const page of regularPages) {
|
|
const isolatedContextName = context.getIsolatedContextName(page);
|
|
const contextLabel = isolatedContextName
|
|
? ` isolatedContext=${isolatedContextName}`
|
|
: '';
|
|
parts.push(
|
|
`${context.getPageId(page)}: ${page.url()}${context.isPageSelected(page) ? ' [selected]' : ''}${contextLabel}`,
|
|
);
|
|
structuredPages.push(createStructuredPage(page, context));
|
|
}
|
|
response.push(...parts);
|
|
structuredContent.pages = structuredPages;
|
|
}
|
|
|
|
if (this.#includeExtensionPages) {
|
|
if (extensionPages.length) {
|
|
response.push(`## Extension Pages`);
|
|
const structuredExtensionPages = [];
|
|
for (const page of extensionPages) {
|
|
const isolatedContextName = context.getIsolatedContextName(page);
|
|
const contextLabel = isolatedContextName
|
|
? ` isolatedContext=${isolatedContextName}`
|
|
: '';
|
|
response.push(
|
|
`${context.getPageId(page)}: ${page.url()}${context.isPageSelected(page) ? ' [selected]' : ''}${contextLabel}`,
|
|
);
|
|
structuredExtensionPages.push(createStructuredPage(page, context));
|
|
}
|
|
structuredContent.extensionPages = structuredExtensionPages;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.#includeExtensionServiceWorkers) {
|
|
if (context.getExtensionServiceWorkers().length) {
|
|
response.push(`## Extension Service Workers`);
|
|
}
|
|
|
|
for (const extensionServiceWorker of context.getExtensionServiceWorkers()) {
|
|
response.push(
|
|
`${extensionServiceWorker.id}: ${extensionServiceWorker.url}`,
|
|
);
|
|
}
|
|
structuredContent.extensionServiceWorkers = context
|
|
.getExtensionServiceWorkers()
|
|
.map(extensionServiceWorker => {
|
|
return {
|
|
id: extensionServiceWorker.id,
|
|
url: extensionServiceWorker.url,
|
|
};
|
|
});
|
|
}
|
|
|
|
if (this.#tabId) {
|
|
structuredContent.tabId = this.#tabId;
|
|
}
|
|
|
|
if (data.traceSummary) {
|
|
const summary = getTraceSummary(data.traceSummary);
|
|
response.push(summary);
|
|
structuredContent.traceSummary = summary;
|
|
structuredContent.traceInsights = [];
|
|
for (const insightSet of data.traceSummary.insights?.values() ?? []) {
|
|
for (const [insightName, model] of Object.entries(insightSet.model)) {
|
|
structuredContent.traceInsights.push({
|
|
insightName,
|
|
insightKey: model.insightKey,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (data.traceInsight) {
|
|
const insightOutput = getInsightOutput(
|
|
data.traceInsight.trace,
|
|
data.traceInsight.insightSetId,
|
|
data.traceInsight.insightName,
|
|
);
|
|
if ('error' in insightOutput) {
|
|
response.push(insightOutput.error);
|
|
} else {
|
|
response.push(insightOutput.output);
|
|
}
|
|
}
|
|
|
|
if (data.lighthouseResult) {
|
|
structuredContent.lighthouseResult = data.lighthouseResult;
|
|
const {summary, reports} = data.lighthouseResult;
|
|
response.push('## Lighthouse Audit Results');
|
|
response.push(`Mode: ${summary.mode}`);
|
|
response.push(`Device: ${summary.device}`);
|
|
response.push(`URL: ${summary.url}`);
|
|
response.push('### Category Scores');
|
|
for (const score of summary.scores) {
|
|
response.push(
|
|
`- ${score.title}: ${(score.score ?? 0) * 100} (${score.id})`,
|
|
);
|
|
}
|
|
response.push('### Audit Summary');
|
|
response.push(`Passed: ${summary.audits.passed}`);
|
|
response.push(`Failed: ${summary.audits.failed}`);
|
|
response.push(`Total Timing: ${summary.timing.total}ms`);
|
|
response.push('### Reports');
|
|
for (const report of reports) {
|
|
response.push(`- ${report}`);
|
|
}
|
|
}
|
|
|
|
if (data.snapshot) {
|
|
if (typeof data.snapshot === 'string') {
|
|
response.push(`Saved snapshot to ${data.snapshot}.`);
|
|
structuredContent.snapshotFilePath = data.snapshot;
|
|
} else {
|
|
response.push('## Latest page snapshot');
|
|
response.push(data.snapshot.toString());
|
|
structuredContent.snapshot = data.snapshot.toJSON();
|
|
}
|
|
}
|
|
|
|
if (data.detailedNetworkRequest) {
|
|
response.push(data.detailedNetworkRequest.toStringDetailed());
|
|
structuredContent.networkRequest =
|
|
data.detailedNetworkRequest.toJSONDetailed();
|
|
}
|
|
|
|
if (data.detailedConsoleMessage) {
|
|
response.push(data.detailedConsoleMessage.toStringDetailed());
|
|
structuredContent.consoleMessage =
|
|
data.detailedConsoleMessage.toJSONDetailed();
|
|
}
|
|
|
|
if (data.extensions) {
|
|
structuredContent.extensions = data.extensions;
|
|
response.push('## Extensions');
|
|
if (data.extensions.length === 0) {
|
|
response.push('No extensions installed.');
|
|
} else {
|
|
const extensionsMessage = data.extensions
|
|
.map(extension => {
|
|
return `id=${extension.id} "${extension.name}" v${extension.version} ${extension.isEnabled ? 'Enabled' : 'Disabled'}`;
|
|
})
|
|
.join('\n');
|
|
response.push(extensionsMessage);
|
|
}
|
|
}
|
|
|
|
if (this.#listInPageTools) {
|
|
structuredContent.inPageTools = data.inPageTools ?? undefined;
|
|
response.push('## In-page tools');
|
|
if (!data.inPageTools || !data.inPageTools.tools) {
|
|
response.push('No in-page tools available.');
|
|
} else {
|
|
const toolGroup = data.inPageTools;
|
|
response.push(`${toolGroup.name}: ${toolGroup.description}`);
|
|
response.push('Available tools:');
|
|
const toolDefinitionsMessage = toolGroup.tools
|
|
.map(tool => {
|
|
return `name="${tool.name}", description="${tool.description}", inputSchema=${JSON.stringify(
|
|
tool.inputSchema,
|
|
)}`;
|
|
})
|
|
.join('\n');
|
|
response.push(toolDefinitionsMessage);
|
|
}
|
|
}
|
|
|
|
if (this.#networkRequestsOptions?.include && data.networkRequests) {
|
|
const requests = data.networkRequests;
|
|
|
|
response.push('## Network requests');
|
|
if (requests.length) {
|
|
const paginationData = this.#dataWithPagination(
|
|
requests,
|
|
this.#networkRequestsOptions.pagination,
|
|
);
|
|
structuredContent.pagination = paginationData.pagination;
|
|
response.push(...paginationData.info);
|
|
if (data.networkRequests) {
|
|
structuredContent.networkRequests = [];
|
|
for (const formatter of paginationData.items) {
|
|
response.push(formatter.toString());
|
|
structuredContent.networkRequests.push(formatter.toJSON());
|
|
}
|
|
}
|
|
} else {
|
|
response.push('No requests found.');
|
|
}
|
|
}
|
|
|
|
if (this.#consoleDataOptions?.include) {
|
|
const messages = data.consoleMessages ?? [];
|
|
|
|
response.push('## Console messages');
|
|
if (messages.length) {
|
|
const paginationData = this.#dataWithPagination(
|
|
messages,
|
|
this.#consoleDataOptions.pagination,
|
|
);
|
|
structuredContent.pagination = paginationData.pagination;
|
|
response.push(...paginationData.info);
|
|
response.push(
|
|
...paginationData.items.map(message => message.toString()),
|
|
);
|
|
structuredContent.consoleMessages = paginationData.items.map(message =>
|
|
message.toJSON(),
|
|
);
|
|
} else {
|
|
response.push('<no console messages found>');
|
|
}
|
|
}
|
|
|
|
const text: TextContent = {
|
|
type: 'text',
|
|
text: response.join('\n'),
|
|
};
|
|
const images: ImageContent[] = this.#images.map(imageData => {
|
|
return {
|
|
type: 'image',
|
|
...imageData,
|
|
} as const;
|
|
});
|
|
|
|
return {
|
|
content: [text, ...images],
|
|
structuredContent,
|
|
};
|
|
}
|
|
|
|
#dataWithPagination<T>(data: T[], pagination?: PaginationOptions) {
|
|
const response = [];
|
|
const paginationResult = paginate<T>(data, pagination);
|
|
if (paginationResult.invalidPage) {
|
|
response.push('Invalid page number provided. Showing first page.');
|
|
}
|
|
|
|
const {startIndex, endIndex, currentPage, totalPages} = paginationResult;
|
|
response.push(
|
|
`Showing ${startIndex + 1}-${endIndex} of ${data.length} (Page ${currentPage + 1} of ${totalPages}).`,
|
|
);
|
|
if (pagination) {
|
|
if (paginationResult.hasNextPage) {
|
|
response.push(`Next page: ${currentPage + 1}`);
|
|
}
|
|
if (paginationResult.hasPreviousPage) {
|
|
response.push(`Previous page: ${currentPage - 1}`);
|
|
}
|
|
}
|
|
|
|
return {
|
|
info: response,
|
|
items: paginationResult.items,
|
|
pagination: {
|
|
currentPage: paginationResult.currentPage,
|
|
totalPages: paginationResult.totalPages,
|
|
hasNextPage: paginationResult.hasNextPage,
|
|
hasPreviousPage: paginationResult.hasPreviousPage,
|
|
startIndex: paginationResult.startIndex,
|
|
endIndex: paginationResult.endIndex,
|
|
invalidPage: paginationResult.invalidPage,
|
|
},
|
|
};
|
|
}
|
|
|
|
resetResponseLineForTesting() {
|
|
this.#textResponseLines = [];
|
|
}
|
|
}
|
|
function createStructuredPage(page: Page, context: McpContext) {
|
|
const isolatedContextName = context.getIsolatedContextName(page);
|
|
const entry: {
|
|
id: number | undefined;
|
|
url: string;
|
|
selected: boolean;
|
|
isolatedContext?: string;
|
|
} = {
|
|
id: context.getPageId(page),
|
|
url: page.url(),
|
|
selected: context.isPageSelected(page),
|
|
};
|
|
if (isolatedContextName) {
|
|
entry.isolatedContext = isolatedContextName;
|
|
}
|
|
return entry;
|
|
}
|