Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion src/McpContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import type {
PredefinedNetworkConditions,
} from 'puppeteer-core';

import {NetworkCollector, PageCollector} from './PageCollector.js';
import {NetworkCollector, PageCollector, PreservedNetworkRequest} from './PageCollector.js';
import {listPages} from './tools/pages.js';
import {takeSnapshot} from './tools/snapshot.js';
import {CLOSE_PAGE_ERROR} from './tools/ToolDefinition.js';
Expand Down Expand Up @@ -134,6 +134,32 @@ export class McpContext implements Context {
return this.#networkCollector.getData(page);
}

getPreservedNetworkRequests(): PreservedNetworkRequest[] {
const page = this.getSelectedPage();
return this.#networkCollector.getPreservedData(page);
}

enableNetworkLogPreservation(options?: {
includeRequestBodies?: boolean;
includeResponseBodies?: boolean;
maxRequests?: number;
}): void {
this.#networkCollector.enablePreservation(options);
}

disableNetworkLogPreservation(): void {
this.#networkCollector.disablePreservation();
}

isNetworkLogPreservationEnabled(): boolean {
return this.#networkCollector.isPreservationEnabled();
}

clearPreservedNetworkLogs(): void {
const page = this.getSelectedPage();
this.#networkCollector.clearPreservedData(page);
}

getConsoleData(): Array<ConsoleMessage | Error> {
const page = this.getSelectedPage();
return this.#consoleCollector.getData(page);
Expand Down
9 changes: 8 additions & 1 deletion src/McpResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,6 @@ Call ${handleDialog.name} to handle it before continuing.`);
if (this.#networkRequestsOptions?.include) {
let requests = context.getNetworkRequests();

// Apply resource type filtering if specified
if (this.#networkRequestsOptions.resourceTypes?.length) {
const normalizedTypes = new Set(
this.#networkRequestsOptions.resourceTypes,
Expand All @@ -234,7 +233,15 @@ Call ${handleDialog.name} to handle it before continuing.`);
});
}

const preservationEnabled = context.isNetworkLogPreservationEnabled();

response.push('## Network requests');
if (preservationEnabled) {
response.push('🔒 **Preservation Mode: ACTIVE** (requests persist across navigations)');
} else {
response.push('📋 Normal mode (logs cleared on navigation). Enable preservation to keep history.');
}

if (requests.length) {
const data = this.#dataWithPagination(
requests,
Expand Down
135 changes: 126 additions & 9 deletions src/PageCollector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@
* SPDX-License-Identifier: Apache-2.0
*/

import type {Browser, HTTPRequest, Page} from 'puppeteer-core';
import type {Browser, HTTPRequest, Page, HTTPResponse} from 'puppeteer-core';

export interface PreservedNetworkRequest {
request: HTTPRequest;
timestamp: number;
requestBody?: string;
responseBody?: string;
}

export class PageCollector<T> {
#browser: Browser;
#initializer: (page: Page, collector: (item: T) => void) => void;
/**
* The Array in this map should only be set once
* As we use the reference to it.
* Use methods that manipulate the array in place.
*/
protected storage = new WeakMap<Page, T[]>();

constructor(
Expand All @@ -24,6 +26,10 @@ export class PageCollector<T> {
this.#initializer = initializer;
}

protected getBrowser(): Browser {
return this.#browser;
}

async init() {
const pages = await this.#browser.pages();
for (const page of pages) {
Expand Down Expand Up @@ -77,7 +83,86 @@ export class PageCollector<T> {
}

export class NetworkCollector extends PageCollector<HTTPRequest> {
#preservationEnabled = false;
#includeRequestBodies = true;
#includeResponseBodies = true;
#maxRequests?: number;
#preservedData = new WeakMap<Page, PreservedNetworkRequest[]>();

enablePreservation(options?: {
includeRequestBodies?: boolean;
includeResponseBodies?: boolean;
maxRequests?: number;
}): void {
this.#preservationEnabled = true;
this.#includeRequestBodies = options?.includeRequestBodies ?? true;
this.#includeResponseBodies = options?.includeResponseBodies ?? true;
this.#maxRequests = options?.maxRequests;
}

disablePreservation(): void {
this.#preservationEnabled = false;
}

isPreservationEnabled(): boolean {
return this.#preservationEnabled;
}

clearPreservedData(page: Page): void {
const preserved = this.#preservedData.get(page);
if (preserved) {
preserved.length = 0;
}
}

getPreservedData(page: Page): PreservedNetworkRequest[] {
return this.#preservedData.get(page) ?? [];
}

async #captureRequestData(request: HTTPRequest): Promise<PreservedNetworkRequest> {
const preserved: PreservedNetworkRequest = {
request,
timestamp: Date.now(),
};

if (this.#includeRequestBodies) {
try {
const postData = request.postData();
if (postData) {
preserved.requestBody = postData;
}
} catch (error) {
}
}

if (this.#includeResponseBodies) {
try {
const response = request.response();
if (response) {
const buffer = await response.buffer();
const contentType = response.headers()['content-type'] || '';

if (contentType.includes('text/') ||
contentType.includes('application/json') ||
contentType.includes('application/xml') ||
contentType.includes('application/javascript')) {
preserved.responseBody = buffer.toString('utf-8');
} else {
preserved.responseBody = `[Binary data: ${contentType}, ${buffer.length} bytes]`;
}
}
} catch (error) {
}
}

return preserved;
}

override cleanup(page: Page) {
if (this.#preservationEnabled) {
return;
}

const requests = this.storage.get(page) ?? [];
if (!requests) {
return;
Expand All @@ -87,9 +172,41 @@ export class NetworkCollector extends PageCollector<HTTPRequest> {
? request.isNavigationRequest()
: false;
});
// Keep all requests since the last navigation request including that
// navigation request itself.
// Keep the reference
requests.splice(0, Math.max(lastRequestIdx, 0));
}

public override addPage(page: Page): void {
super.addPage(page);

if (this.#preservationEnabled) {
if (!this.#preservedData.has(page)) {
this.#preservedData.set(page, []);
}

page.on('requestfinished', async (request: HTTPRequest) => {
const preserved = this.#preservedData.get(page);
if (!preserved) return;

const data = await this.#captureRequestData(request);
preserved.push(data);

if (this.#maxRequests && preserved.length > this.#maxRequests) {
preserved.shift();
}
});
}
}

override async init() {
await super.init();

if (this.#preservationEnabled) {
const pages = await this.getBrowser().pages();
for (const page of pages) {
if (!this.#preservedData.has(page)) {
this.#preservedData.set(page, []);
}
}
}
}
}
10 changes: 10 additions & 0 deletions src/tools/ToolDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import type {Dialog, ElementHandle, Page} from 'puppeteer-core';
import z from 'zod';

import type {PreservedNetworkRequest} from '../PageCollector.js';
import type {TraceResult} from '../trace-processing/parse.js';

import type {ToolCategories} from './categories.js';
Expand Down Expand Up @@ -79,6 +80,15 @@ export type Context = Readonly<{
filename: string,
): Promise<{filename: string}>;
waitForEventsAfterAction(action: () => Promise<unknown>): Promise<void>;
enableNetworkLogPreservation(options?: {
includeRequestBodies?: boolean;
includeResponseBodies?: boolean;
maxRequests?: number;
}): void;
disableNetworkLogPreservation(): void;
isNetworkLogPreservationEnabled(): boolean;
clearPreservedNetworkLogs(): void;
getPreservedNetworkRequests(): PreservedNetworkRequest[];
}>;

export function defineTool<Schema extends z.ZodRawShape>(
Expand Down
Loading
Loading