feat: add performance tool

This commit is contained in:
hangerye
2025-10-10 01:16:32 +08:00
parent 46a669978d
commit eef5d9f5f4
12 changed files with 816 additions and 29 deletions
@@ -269,20 +269,42 @@ class CloseTabsTool extends BaseBrowserToolExecutor {
// If URL is provided, close all tabs matching that URL
if (urlPattern) {
console.log(`Searching for tabs with URL: ${url}`);
if (!urlPattern.endsWith('/')) {
urlPattern += '/*';
try {
// Build a proper Chrome match pattern from a concrete URL.
// If caller already provided a match pattern with '*', use as-is.
if (!urlPattern.includes('*')) {
// Ignore search/hash; match by origin + pathname prefix.
// Use URL to normalize; fallback to simple suffixing when parsing fails.
try {
const u = new URL(urlPattern);
const basePath = u.pathname || '/';
const pathWithWildcard = basePath.endsWith('/') ? `${basePath}*` : `${basePath}/*`;
urlPattern = `${u.protocol}//${u.host}${pathWithWildcard}`;
} catch {
// Not a fully-qualified URL; ensure it ends with wildcard
urlPattern = urlPattern.endsWith('/') ? `${urlPattern}*` : `${urlPattern}/*`;
}
}
} catch {
// Best-effort: ensure we have some wildcard
urlPattern = urlPattern.endsWith('*')
? urlPattern
: urlPattern.endsWith('/')
? `${urlPattern}*`
: `${urlPattern}/*`;
}
const tabs = await chrome.tabs.query({ url });
const tabs = await chrome.tabs.query({ url: urlPattern });
if (!tabs || tabs.length === 0) {
console.log(`No tabs found with URL: ${url}`);
console.log(`No tabs found with URL pattern: ${urlPattern}`);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
message: `No tabs found with URL: ${url}`,
message: `No tabs found with URL pattern: ${urlPattern}`,
closedCount: 0,
}),
},
@@ -291,7 +313,7 @@ class CloseTabsTool extends BaseBrowserToolExecutor {
};
}
console.log(`Found ${tabs.length} tabs with URL: ${url}`);
console.log(`Found ${tabs.length} tabs with URL pattern: ${urlPattern}`);
const tabIdsToClose = tabs
.map((tab) => tab.id)
.filter((id): id is number => id !== undefined);
@@ -17,3 +17,8 @@ export { readPageTool } from './read-page';
export { computerTool } from './computer';
export { handleDialogTool } from './dialog';
export { userscriptTool } from './userscript';
export {
performanceStartTraceTool,
performanceStopTraceTool,
performanceAnalyzeInsightTool,
} from './performance';
@@ -0,0 +1,545 @@
import { createErrorResponse, ToolResult } from '@/common/tool-handler';
import { BaseBrowserToolExecutor } from '../base-browser';
import { TOOL_NAMES } from 'chrome-mcp-shared';
import { cdpSessionManager } from '@/utils/cdp-session-manager';
type OwnerTag = 'performance';
interface StartTraceParams {
reload?: boolean; // whether to reload the page after starting trace
autoStop?: boolean; // whether to auto stop after a short duration
durationMs?: number; // custom duration when autoStop is true (default 5000)
}
interface StopTraceParams {
saveToDownloads?: boolean; // save trace to Downloads as JSON (default true)
filenamePrefix?: string; // filename prefix (default 'performance_trace')
}
interface AnalyzeInsightParams {
insightName?: string; // placeholder for future deep insights
}
type DebuggeeEvent = (source: chrome.debugger.Debuggee, method: string, params?: any) => void;
interface TraceSessionState {
recording: boolean;
events: any[];
startedAt: number;
pageUrl?: string;
listener: DebuggeeEvent;
stopResolver?: (value: { completed: boolean }) => void;
stopPromise?: Promise<{ completed: boolean }>;
}
const sessions = new Map<number, TraceSessionState>();
const LAST_RESULTS = new Map<
number,
{
events: any[];
startedAt: number;
endedAt: number;
tabUrl: string;
saved?: { downloadId?: number; filename?: string; fullPath?: string };
metrics?: Record<string, number>;
}
>();
function tracingCategories(): string[] {
// Keep broadly consistent with other project
return [
'-*',
'blink.console',
'blink.user_timing',
'devtools.timeline',
'disabled-by-default-devtools.screenshot',
'disabled-by-default-devtools.timeline',
'disabled-by-default-devtools.timeline.invalidationTracking',
'disabled-by-default-devtools.timeline.frame',
'disabled-by-default-devtools.timeline.stack',
'disabled-by-default-v8.cpu_profiler',
'disabled-by-default-v8.cpu_profiler.hires',
'latencyInfo',
'loading',
'disabled-by-default-lighthouse',
'v8.execute',
'v8',
];
}
async function enablePerformanceMetrics(tabId: number): Promise<Record<string, number>> {
try {
await cdpSessionManager.sendCommand(tabId, 'Performance.enable');
const result = (await cdpSessionManager.sendCommand(tabId, 'Performance.getMetrics')) as {
metrics: Array<{ name: string; value: number }>;
};
await cdpSessionManager.sendCommand(tabId, 'Performance.disable');
const map: Record<string, number> = {};
for (const m of result.metrics || []) map[m.name] = m.value;
return map;
} catch (e) {
return {};
}
}
async function saveTraceToDownloads(
json: string,
filenamePrefix = 'performance_trace',
): Promise<{ downloadId?: number; filename?: string; fullPath?: string }> {
try {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `${filenamePrefix}_${timestamp}.json`;
const dataUrl = `data:application/json;base64,${btoa(unescape(encodeURIComponent(json)))}`;
const downloadId = await chrome.downloads.download({ url: dataUrl, filename, saveAs: false });
// Attempt to resolve full path
try {
await new Promise((r) => setTimeout(r, 120));
const [item] = await chrome.downloads.search({ id: downloadId });
return { downloadId, filename, fullPath: item?.filename };
} catch {
return { downloadId, filename };
}
} catch {
return {};
}
}
async function saveTraceToNativeTemp(
json: string,
filenamePrefix = 'performance_trace',
): Promise<{ filename?: string; fullPath?: string } | undefined> {
try {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `${filenamePrefix}_${timestamp}.json`;
const base64 = btoa(unescape(encodeURIComponent(json)));
const requestId = `trace-temp-${Date.now()}-${Math.random().toString(36).slice(2)}`;
const timeoutMs = 30000;
const resp = await new Promise<any>((resolve, reject) => {
const timer = setTimeout(() => {
chrome.runtime.onMessage.removeListener(listener);
reject(new Error('Native temp save timed out'));
}, timeoutMs);
const listener = (message: any) => {
if (
message &&
message.type === 'file_operation_response' &&
message.responseToRequestId === requestId
) {
clearTimeout(timer);
chrome.runtime.onMessage.removeListener(listener);
resolve(message.payload);
}
};
chrome.runtime.onMessage.addListener(listener);
chrome.runtime
.sendMessage({
type: 'forward_to_native',
message: {
type: 'file_operation',
requestId,
payload: {
action: 'prepareFile',
base64Data: base64,
fileName: filename,
},
},
})
.catch((err) => {
clearTimeout(timer);
chrome.runtime.onMessage.removeListener(listener);
reject(err);
});
});
if (resp && resp.success && resp.filePath) {
return { filename, fullPath: resp.filePath };
}
} catch {
// ignore, fallback will apply
}
return undefined;
}
async function cleanupNativeTempFile(filePath: string): Promise<void> {
if (!filePath) return;
try {
const requestId = `trace-clean-${Date.now()}-${Math.random().toString(36).slice(2)}`;
const timeoutMs = 10000;
await new Promise<void>((resolve) => {
const timer = setTimeout(() => {
chrome.runtime.onMessage.removeListener(listener);
resolve(); // best-effort
}, timeoutMs);
const listener = (message: any) => {
if (
message &&
message.type === 'file_operation_response' &&
message.responseToRequestId === requestId
) {
clearTimeout(timer);
chrome.runtime.onMessage.removeListener(listener);
resolve();
}
};
chrome.runtime.onMessage.addListener(listener);
chrome.runtime
.sendMessage({
type: 'forward_to_native',
message: {
type: 'file_operation',
requestId,
payload: {
action: 'cleanupFile',
filePath,
},
},
})
.catch(() => {
clearTimeout(timer);
chrome.runtime.onMessage.removeListener(listener);
resolve();
});
});
} catch {
// ignore
}
}
function getOrCreateStopPromise(session: TraceSessionState): Promise<{ completed: boolean }> {
if (session.stopPromise) return session.stopPromise;
session.stopPromise = new Promise((resolve) => {
session.stopResolver = resolve;
});
return session.stopPromise;
}
/**
* Start performance trace
*/
class PerformanceStartTraceTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.PERFORMANCE_START_TRACE;
async execute(args: StartTraceParams): Promise<ToolResult> {
const { reload = false, autoStop = false, durationMs = 5000 } = args || {};
try {
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!activeTab?.id) {
return createErrorResponse('No active tab found');
}
const tabId = activeTab.id;
const existed = sessions.get(tabId);
if (existed?.recording) {
return {
content: [{ type: 'text', text: 'Error: a performance trace is already running.' }],
isError: false,
};
}
await cdpSessionManager.attach(tabId, 'performance');
const state: TraceSessionState = {
recording: true,
events: [],
startedAt: Date.now(),
pageUrl: activeTab.url || '',
listener: (source, method, params) => {
if (source.tabId !== tabId) return;
if (method === 'Tracing.dataCollected' && params?.value) {
try {
state.events.push(...(params.value as any[]));
} catch {
// ignore
}
} else if (method === 'Tracing.tracingComplete') {
state.recording = false;
state.stopResolver?.({ completed: true });
}
},
};
chrome.debugger.onEvent.addListener(state.listener);
sessions.set(tabId, state);
// Start tracing with categories
const cats = tracingCategories().join(',');
await cdpSessionManager.sendCommand(tabId, 'Tracing.start', {
categories: cats,
options: 'record-as-much-as-possible',
transferMode: 'ReportEvents',
});
if (reload) {
try {
await cdpSessionManager.sendCommand(tabId, 'Page.reload', { ignoreCache: true });
} catch {
// best effort; ignore if fails
}
}
if (autoStop) {
setTimeout(
async () => {
try {
await cdpSessionManager.sendCommand(tabId, 'Tracing.end');
} catch {
// ignore
}
},
Math.max(1000, Math.min(durationMs, 60000)),
);
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: 'Performance trace is recording. Use performance_stop_trace to stop it.',
reload,
autoStop,
}),
},
],
isError: false,
};
} catch (e: any) {
return createErrorResponse(`Failed to start performance trace: ${e?.message || e}`);
}
}
}
/**
* Stop performance trace
*/
class PerformanceStopTraceTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.PERFORMANCE_STOP_TRACE;
async execute(args: StopTraceParams): Promise<ToolResult> {
const { saveToDownloads = true, filenamePrefix } = args || {};
try {
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!activeTab?.id) return createErrorResponse('No active tab found');
const tabId = activeTab.id;
const session = sessions.get(tabId);
if (!session) {
return {
content: [
{ type: 'text', text: 'No performance trace session found for the current tab.' },
],
isError: false,
};
}
let stopResult: { completed: boolean } = { completed: false };
if (session.recording) {
// End tracing and wait for completion signal
await cdpSessionManager.sendCommand(tabId, 'Tracing.end');
await getOrCreateStopPromise(session);
stopResult = await session.stopPromise!;
} else {
// Already auto-stopped; proceed to finalize without waiting
stopResult = { completed: true };
}
// Fetch metrics before detach
const metrics = await enablePerformanceMetrics(tabId);
// Cleanup event listener and detach
try {
chrome.debugger.onEvent.removeListener(session.listener);
} catch {
// ignore
}
try {
await cdpSessionManager.detach(tabId, 'performance');
} catch {
// ignore
}
const endedAt = Date.now();
const trace = { traceEvents: session.events };
const json = JSON.stringify(trace);
let saved: { downloadId?: number; filename?: string; fullPath?: string } | undefined;
if (saveToDownloads) {
saved = await saveTraceToDownloads(json, filenamePrefix || 'performance_trace');
} else {
// Persist to native temp directory so that analysis can run without Downloads permission
const tempSaved = await saveTraceToNativeTemp(json, filenamePrefix || 'performance_trace');
if (tempSaved) {
saved = { ...tempSaved } as any;
}
}
LAST_RESULTS.set(tabId, {
events: session.events,
startedAt: session.startedAt,
endedAt,
tabUrl: session.pageUrl || '',
saved,
metrics,
});
sessions.delete(tabId);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: 'The performance trace has been stopped.',
eventCount: session.events.length,
saved,
metrics,
startedAt: session.startedAt,
endedAt,
durationMs: endedAt - session.startedAt,
url: session.pageUrl || '',
tracingCompleted: stopResult?.completed === true,
}),
},
],
isError: false,
};
} catch (e: any) {
return createErrorResponse(`Failed to stop performance trace: ${e?.message || e}`);
}
}
}
/**
* Analyze last trace (lightweight)
* Note: Deep insights require DevTools front-end trace engine on the native side; this is a
* pragmatic first step returning basic metrics and a quick event histogram.
*/
class PerformanceAnalyzeInsightTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.PERFORMANCE_ANALYZE_INSIGHT;
async execute(args: AnalyzeInsightParams & { timeoutMs?: number }): Promise<ToolResult> {
const { insightName } = args || {};
try {
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!activeTab?.id) return createErrorResponse('No active tab found');
const tabId = activeTab.id;
const result = LAST_RESULTS.get(tabId);
if (!result) {
return {
content: [
{
type: 'text',
text: 'No recorded traces found. Start and stop a performance trace first.',
},
],
isError: false,
};
}
// Prefer native-side deep analysis when we have a saved file path
const fullPath = (result.saved && (result.saved as any).fullPath) || undefined;
if (fullPath) {
try {
const requestId = `trace-analyze-${Date.now()}-${Math.random().toString(36).slice(2)}`;
const timeoutMs = Math.max(10000, Math.min((args as any)?.timeoutMs ?? 60000, 300000));
const resp = await new Promise<any>((resolve, reject) => {
const timer = setTimeout(() => {
chrome.runtime.onMessage.removeListener(listener);
reject(new Error('Native trace analysis timed out'));
}, timeoutMs);
const listener = (message: any) => {
if (
message &&
message.type === 'file_operation_response' &&
message.responseToRequestId === requestId
) {
clearTimeout(timer);
chrome.runtime.onMessage.removeListener(listener);
resolve(message.payload);
}
};
chrome.runtime.onMessage.addListener(listener);
chrome.runtime
.sendMessage({
type: 'forward_to_native',
message: {
type: 'file_operation',
requestId,
payload: { action: 'analyzeTrace', traceFilePath: fullPath, insightName },
},
})
.catch((err) => {
clearTimeout(timer);
chrome.runtime.onMessage.removeListener(listener);
reject(err);
});
});
if (resp && resp.success) {
// Best-effort cleanup for temp files (Downloads paths are ignored by native cleaner)
await cleanupNativeTempFile(fullPath);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
url: result.tabUrl,
startedAt: result.startedAt,
endedAt: result.endedAt,
durationMs: result.endedAt - result.startedAt,
metrics: result.metrics || {},
saved: result.saved,
summary: resp.summary,
insight: resp.insight,
}),
},
],
isError: false,
};
}
// If native returned error, fall through to lightweight analysis
} catch (e) {
// Fallback to lightweight analysis below
}
}
// Lightweight fallback (when no saved file path)
const counts = new Map<string, number>();
for (const ev of result.events.slice(0, 100000)) {
const n = typeof (ev as any)?.name === 'string' ? (ev as any).name : 'unknown';
counts.set(n, (counts.get(n) || 0) + 1);
}
const top = [...counts.entries()]
.sort((a, b) => b[1] - a[1])
.slice(0, 20)
.map(([name, count]) => ({ name, count }));
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
info: 'Lightweight analysis (no saved file path). Native-side deep analysis unavailable.',
requestedInsight: insightName || null,
url: result.tabUrl,
startedAt: result.startedAt,
endedAt: result.endedAt,
durationMs: result.endedAt - result.startedAt,
metrics: result.metrics || {},
topEventNames: top,
saved: result.saved,
}),
},
],
isError: false,
};
} catch (e: any) {
return createErrorResponse(`Failed to analyze trace: ${e?.message || e}`);
}
}
}
export const performanceStartTraceTool = new PerformanceStartTraceTool();
export const performanceStopTraceTool = new PerformanceStopTraceTool();
export const performanceAnalyzeInsightTool = new PerformanceAnalyzeInsightTool();
+2 -1
View File
@@ -43,7 +43,8 @@
"is-admin": "^4.0.0",
"node-fetch": "2",
"pino": "^9.6.0",
"uuid": "^11.1.0"
"uuid": "^11.1.0",
"chrome-devtools-frontend": "^1.0.1299282"
},
"devDependencies": {
"@jest/globals": "^29.7.0",
+18 -3
View File
@@ -22,7 +22,7 @@ export class FileHandler {
* Handle file preparation request from the extension
*/
async handleFileRequest(request: any): Promise<any> {
const { action, fileUrl, base64Data, fileName, filePath } = request;
const { action, fileUrl, base64Data, fileName, filePath, traceFilePath, insightName } = request;
try {
switch (action) {
@@ -39,6 +39,21 @@ export class FileHandler {
case 'cleanupFile':
return await this.cleanupFile(filePath);
case 'analyzeTrace': {
const targetPath = traceFilePath || filePath;
if (!targetPath) {
return { success: false, error: 'traceFilePath is required' };
}
try {
// With tsconfig moduleResolution=NodeNext, relative ESM imports need explicit .js extension
const { analyzeTraceFile } = await import('./trace-analyzer.js');
const res = await analyzeTraceFile(targetPath, insightName);
return { success: true, ...res };
} catch (e: any) {
return { success: false, error: e?.message || String(e) };
}
}
default:
return {
success: false,
@@ -91,7 +106,7 @@ export class FileHandler {
try {
// Remove data URL prefix if present
const base64Content = base64Data.replace(/^data:.*?;base64,/, '');
// Convert base64 to buffer
const buffer = Buffer.from(base64Content, 'base64');
@@ -222,4 +237,4 @@ export class FileHandler {
}
}
export default new FileHandler();
export default new FileHandler();
+1 -1
View File
@@ -26,7 +26,7 @@ const handleToolCall = async (name: string, args: any): Promise<CallToolResult>
args,
},
NativeMessageType.CALL_TOOL,
30000, // 30秒超时
120000, // 延长到 120 秒,避免性能分析等长任务超时
);
if (response.status === 'success') {
return response.data;
+7
View File
@@ -0,0 +1,7 @@
// Single-file shim for all deep imports from chrome-devtools-frontend.
// This prevents TypeScript from type-checking the upstream DevTools source tree.
// Runtime still loads the real package; this file is types-only and local to TS.
declare module 'chrome-devtools-frontend/*' {
const anyExport: any;
export = anyExport;
}
+86
View File
@@ -0,0 +1,86 @@
import * as fs from 'fs';
// Import DevTools trace engine and formatters from chrome-devtools-frontend
// We intentionally use deep imports to match the package structure.
// These modules are ESM and require NodeNext module resolution.
// Types are loosely typed to minimize coupling with DevTools internals.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import * as TraceEngine from 'chrome-devtools-frontend/front_end/models/trace/trace.js';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { PerformanceTraceFormatter } from 'chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.js';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { PerformanceInsightFormatter } from 'chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.js';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { AgentFocus } from 'chrome-devtools-frontend/front_end/models/ai_assistance/performance/AIContext.js';
const engine = TraceEngine.TraceModel.Model.createWithAllHandlers();
function readJsonFile(path: string): any {
const text = fs.readFileSync(path, 'utf-8');
return JSON.parse(text);
}
export async function parseTrace(json: any): Promise<{
parsedTrace: any;
insights: any | null;
}> {
engine.resetProcessor();
const events = Array.isArray(json) ? json : json.traceEvents;
if (!events || !Array.isArray(events)) {
throw new Error('Invalid trace format: expected array or {traceEvents: []}');
}
await engine.parse(events);
const parsedTrace = engine.parsedTrace();
const insights = parsedTrace?.insights ?? null;
if (!parsedTrace) throw new Error('No parsed trace returned by engine');
return { parsedTrace, insights };
}
export function getTraceSummary(parsedTrace: any): string {
const focus = AgentFocus.fromParsedTrace(parsedTrace);
const formatter = new PerformanceTraceFormatter(focus);
return formatter.formatTraceSummary();
}
export function getInsightText(parsedTrace: any, insights: any, insightName: string): string {
if (!insights) throw new Error('No insights available for this trace');
const mainNavId = parsedTrace.data?.Meta?.mainFrameNavigations?.at(0)?.args?.data?.navigationId;
const NO_NAV = TraceEngine.Types.Events.NO_NAVIGATION;
const set = insights.get(mainNavId ?? NO_NAV);
if (!set) throw new Error('No insights for selected navigation');
const model = set.model || {};
if (!(insightName in model)) throw new Error(`Insight not found: ${insightName}`);
const formatter = new PerformanceInsightFormatter(
AgentFocus.fromParsedTrace(parsedTrace),
model[insightName],
);
return formatter.formatInsight();
}
export async function analyzeTraceFile(
filePath: string,
insightName?: string,
): Promise<{
summary: string;
insight?: string;
}> {
const json = readJsonFile(filePath);
const { parsedTrace, insights } = await parseTrace(json);
const summary = getTraceSummary(parsedTrace);
if (insightName) {
try {
const insight = getInsightText(parsedTrace, insights, insightName);
return { summary, insight };
} catch {
// If requested insight missing, still return summary
return { summary };
}
}
return { summary };
}
export default { analyzeTraceFile };
+28
View File
@@ -0,0 +1,28 @@
// Minimal ambient declarations to avoid compiling chrome-devtools-frontend sources.
// We intentionally treat these modules as `any` to keep our build lightweight and decoupled
// from DevTools' internal TypeScript and lib targets.
declare module 'chrome-devtools-frontend/front_end/models/trace/trace.js' {
// Shape used by our code: TraceModel + Types + Insights
export const TraceModel: any;
export const Types: any;
export const Insights: any;
}
declare module 'chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.js' {
export class PerformanceTraceFormatter {
constructor(...args: any[]);
formatTraceSummary(): string;
}
}
declare module 'chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.js' {
export class PerformanceInsightFormatter {
constructor(...args: any[]);
formatInsight(): string;
}
}
declare module 'chrome-devtools-frontend/front_end/models/ai_assistance/performance/AIContext.js' {
export const AgentFocus: any;
}
+22 -18
View File
@@ -1,19 +1,23 @@
{
"compilerOptions": {
"target": "ES2018",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2018", "DOM"],
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"declaration": true,
"sourceMap": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"]
}
"compilerOptions": {
"target": "ES2018",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2018", "DOM"],
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"declaration": true,
"sourceMap": true,
"resolveJsonModule": true,
"baseUrl": ".",
"paths": {
"chrome-devtools-frontend/*": ["src/shims/devtools.d.ts"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"]
}
+66
View File
@@ -31,6 +31,9 @@ export const TOOL_NAMES = {
COMPUTER: 'chrome_computer',
HANDLE_DIALOG: 'chrome_handle_dialog',
USERSCRIPT: 'chrome_userscript',
PERFORMANCE_START_TRACE: 'performance_start_trace',
PERFORMANCE_STOP_TRACE: 'performance_stop_trace',
PERFORMANCE_ANALYZE_INSIGHT: 'performance_analyze_insight',
},
};
@@ -44,6 +47,69 @@ export const TOOL_SCHEMAS: Tool[] = [
required: [],
},
},
{
name: TOOL_NAMES.BROWSER.PERFORMANCE_START_TRACE,
description:
'Starts a performance trace recording on the selected page. Optionally reloads the page and/or auto-stops after a short duration.',
inputSchema: {
type: 'object',
properties: {
reload: {
type: 'boolean',
description:
'Determines if, once tracing has started, the page should be automatically reloaded (ignore cache).',
},
autoStop: {
type: 'boolean',
description: 'Determines if the trace should be automatically stopped (default false).',
},
durationMs: {
type: 'number',
description: 'Auto-stop duration in milliseconds when autoStop is true (default 5000).',
},
},
required: [],
},
},
{
name: TOOL_NAMES.BROWSER.PERFORMANCE_STOP_TRACE,
description: 'Stops the active performance trace recording on the selected page.',
inputSchema: {
type: 'object',
properties: {
saveToDownloads: {
type: 'boolean',
description: 'Whether to save the trace as a JSON file in Downloads (default true).',
},
filenamePrefix: {
type: 'string',
description: 'Optional filename prefix for the downloaded trace JSON.',
},
},
required: [],
},
},
{
name: TOOL_NAMES.BROWSER.PERFORMANCE_ANALYZE_INSIGHT,
description:
'Provides a lightweight summary of the last recorded trace. For deep insights (CWV, breakdowns), integrate native-side DevTools trace engine.',
inputSchema: {
type: 'object',
properties: {
insightName: {
type: 'string',
description:
'Optional insight name for future deep analysis (e.g., "DocumentLatency"). Currently informational only.',
},
timeoutMs: {
type: 'number',
description:
'Timeout for deep analysis via native host (milliseconds). Default 60000. Increase for large traces.',
},
},
required: [],
},
},
{
name: TOOL_NAMES.BROWSER.READ_PAGE,
description:
+8
View File
@@ -111,6 +111,9 @@ importers:
chalk:
specifier: ^5.4.1
version: 5.4.1
chrome-devtools-frontend:
specifier: ^1.0.1299282
version: 1.0.1526630
chrome-mcp-shared:
specifier: workspace:*
version: link:../../packages/shared
@@ -1563,6 +1566,9 @@ packages:
chownr@1.1.4:
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
chrome-devtools-frontend@1.0.1526630:
resolution: {integrity: sha512-zl2taUBtNgA0Mlz1pnL0DfXCYq5M+G9Yo9hFx9b4Y2pJ8Q/J/qepq2/6hC3I/N3tP7KnuJ/+eJcBr2qMewwoag==}
chrome-launcher@1.1.2:
resolution: {integrity: sha512-YclTJey34KUm5jB1aEJCq807bSievi7Nb/TU4Gu504fUYi3jw3KCIaH6L7nFWQhdEgH3V+wCh+kKD1P5cXnfxw==}
engines: {node: '>=12.13.0'}
@@ -6138,6 +6144,8 @@ snapshots:
chownr@1.1.4: {}
chrome-devtools-frontend@1.0.1526630: {}
chrome-launcher@1.1.2:
dependencies:
'@types/node': 22.15.30