hangwin--mcp-chrome
159 行
5.1 KiB
TypeScript
159 行
5.1 KiB
TypeScript
import { createErrorResponse, ToolResult } from '@/common/tool-handler';
|
|
import { BaseBrowserToolExecutor } from '../base-browser';
|
|
import { TOOL_NAMES } from 'chrome-mcp-shared';
|
|
import { networkCaptureStartTool, networkCaptureStopTool } from './network-capture-web-request';
|
|
import { networkDebuggerStartTool, networkDebuggerStopTool } from './network-capture-debugger';
|
|
|
|
type NetworkCaptureBackend = 'webRequest' | 'debugger';
|
|
|
|
interface NetworkCaptureToolParams {
|
|
action: 'start' | 'stop';
|
|
needResponseBody?: boolean;
|
|
url?: string;
|
|
maxCaptureTime?: number;
|
|
inactivityTimeout?: number;
|
|
includeStatic?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Extract text content from ToolResult
|
|
*/
|
|
function getFirstText(result: ToolResult): string | undefined {
|
|
const first = result.content?.[0];
|
|
return first && first.type === 'text' ? first.text : undefined;
|
|
}
|
|
|
|
/**
|
|
* Decorate JSON result with additional fields
|
|
*/
|
|
function decorateJsonResult(result: ToolResult, extra: Record<string, unknown>): ToolResult {
|
|
const text = getFirstText(result);
|
|
if (typeof text !== 'string') return result;
|
|
|
|
try {
|
|
const parsed = JSON.parse(text);
|
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
return {
|
|
...result,
|
|
content: [{ type: 'text', text: JSON.stringify({ ...parsed, ...extra }) }],
|
|
};
|
|
}
|
|
} catch {
|
|
// If the underlying tool didn't return JSON, keep it as-is
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Check if debugger-based capture is active
|
|
*/
|
|
function isDebuggerCaptureActive(): boolean {
|
|
const captureData = (
|
|
networkDebuggerStartTool as unknown as { captureData?: Map<number, unknown> }
|
|
).captureData;
|
|
return captureData instanceof Map && captureData.size > 0;
|
|
}
|
|
|
|
/**
|
|
* Check if webRequest-based capture is active
|
|
*/
|
|
function isWebRequestCaptureActive(): boolean {
|
|
return networkCaptureStartTool.captureData.size > 0;
|
|
}
|
|
|
|
/**
|
|
* Unified Network Capture Tool
|
|
*
|
|
* Provides a single entry point for network capture, automatically selecting
|
|
* the appropriate backend based on the `needResponseBody` parameter:
|
|
* - needResponseBody=false (default): uses webRequest API (lightweight, no debugger conflict)
|
|
* - needResponseBody=true: uses Debugger API (captures response body, may conflict with DevTools)
|
|
*/
|
|
class NetworkCaptureTool extends BaseBrowserToolExecutor {
|
|
name = TOOL_NAMES.BROWSER.NETWORK_CAPTURE;
|
|
|
|
async execute(args: NetworkCaptureToolParams): Promise<ToolResult> {
|
|
const action = args?.action;
|
|
if (action !== 'start' && action !== 'stop') {
|
|
return createErrorResponse('Parameter [action] is required and must be one of: start, stop');
|
|
}
|
|
|
|
const wantBody = args?.needResponseBody === true;
|
|
const debuggerActive = isDebuggerCaptureActive();
|
|
const webActive = isWebRequestCaptureActive();
|
|
|
|
if (action === 'start') {
|
|
return this.handleStart(args, wantBody, debuggerActive, webActive);
|
|
}
|
|
|
|
return this.handleStop(args, debuggerActive, webActive);
|
|
}
|
|
|
|
private async handleStart(
|
|
args: NetworkCaptureToolParams,
|
|
wantBody: boolean,
|
|
debuggerActive: boolean,
|
|
webActive: boolean,
|
|
): Promise<ToolResult> {
|
|
// Prevent any capture conflict (cross-mode or same-mode)
|
|
if (debuggerActive || webActive) {
|
|
const activeMode = debuggerActive ? 'debugger' : 'webRequest';
|
|
return createErrorResponse(
|
|
`Network capture is already active in ${activeMode} mode. Stop it before starting a new capture.`,
|
|
);
|
|
}
|
|
|
|
const delegate = wantBody ? networkDebuggerStartTool : networkCaptureStartTool;
|
|
const backend: NetworkCaptureBackend = wantBody ? 'debugger' : 'webRequest';
|
|
|
|
const result = await delegate.execute({
|
|
url: args.url,
|
|
maxCaptureTime: args.maxCaptureTime,
|
|
inactivityTimeout: args.inactivityTimeout,
|
|
includeStatic: args.includeStatic,
|
|
});
|
|
|
|
return decorateJsonResult(result, { backend, needResponseBody: wantBody });
|
|
}
|
|
|
|
private async handleStop(
|
|
args: NetworkCaptureToolParams,
|
|
debuggerActive: boolean,
|
|
webActive: boolean,
|
|
): Promise<ToolResult> {
|
|
// Determine which backend to stop
|
|
let backendToStop: NetworkCaptureBackend | null = null;
|
|
|
|
// If user explicitly specified needResponseBody, try to stop that specific backend
|
|
if (args?.needResponseBody === true) {
|
|
backendToStop = debuggerActive ? 'debugger' : null;
|
|
} else if (args?.needResponseBody === false) {
|
|
backendToStop = webActive ? 'webRequest' : null;
|
|
}
|
|
|
|
// If no explicit preference or the specified backend isn't active, auto-detect
|
|
if (!backendToStop) {
|
|
if (debuggerActive) {
|
|
backendToStop = 'debugger';
|
|
} else if (webActive) {
|
|
backendToStop = 'webRequest';
|
|
}
|
|
}
|
|
|
|
if (!backendToStop) {
|
|
return createErrorResponse('No active network captures found in any tab.');
|
|
}
|
|
|
|
const delegateStop =
|
|
backendToStop === 'debugger' ? networkDebuggerStopTool : networkCaptureStopTool;
|
|
const result = await delegateStop.execute();
|
|
|
|
return decorateJsonResult(result, {
|
|
backend: backendToStop,
|
|
needResponseBody: backendToStop === 'debugger',
|
|
});
|
|
}
|
|
}
|
|
|
|
export const networkCaptureTool = new NetworkCaptureTool();
|