Skip to content
Merged
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
120 changes: 42 additions & 78 deletions src/McpContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,6 @@ export class McpContext implements Context {
null;
#focusedPagePerContext = new Map<BrowserContext, Page>();

#requestPage?: ContextPage;

#nextPageId = 1;

#extensionServiceWorkerMap = new WeakMap<Target, string>();
Expand Down Expand Up @@ -196,17 +194,6 @@ export class McpContext implements Context {
return context;
}

// TODO: Refactor away mutable request state (e.g. per-request facade,
// per-request context object, or another approach). Once resolved, the
// global toolMutex could become per-BrowserContext for parallel execution.
setRequestPage(page?: ContextPage): void {
this.#requestPage = page;
}

#resolveTargetPage(): Page {
return this.#requestPage?.pptrPage ?? this.getSelectedPptrPage();
}

resolveCdpRequestId(page: McpPage, cdpRequestId: string): number | undefined {
if (!cdpRequestId) {
this.logger('no network request');
Expand Down Expand Up @@ -250,20 +237,28 @@ export class McpContext implements Context {
return;
}

getNetworkRequests(includePreservedRequests?: boolean): HTTPRequest[] {
const page = this.#resolveTargetPage();
return this.#networkCollector.getData(page, includePreservedRequests);
getNetworkRequests(
page: McpPage,
includePreservedRequests?: boolean,
): HTTPRequest[] {
return this.#networkCollector.getData(
page.pptrPage,
includePreservedRequests,
);
}

getConsoleData(
page: McpPage,
includePreservedMessages?: boolean,
): Array<ConsoleMessage | Error | DevTools.AggregatedIssue | UncaughtError> {
const page = this.#resolveTargetPage();
return this.#consoleCollector.getData(page, includePreservedMessages);
return this.#consoleCollector.getData(
page.pptrPage,
includePreservedMessages,
);
}

getDevToolsUniverse(): TargetUniverse | null {
return this.#devtoolsUniverseManager.get(this.#resolveTargetPage());
getDevToolsUniverse(page: McpPage): TargetUniverse | null {
return this.#devtoolsUniverseManager.get(page.pptrPage);
}

getConsoleMessageStableId(
Expand All @@ -273,15 +268,16 @@ export class McpContext implements Context {
}

getConsoleMessageById(
page: McpPage,
id: number,
): ConsoleMessage | Error | DevTools.AggregatedIssue | UncaughtError {
return this.#consoleCollector.getById(this.#resolveTargetPage(), id);
return this.#consoleCollector.getById(page.pptrPage, id);
}

async newPage(
background?: boolean,
isolatedContextName?: string,
): Promise<ContextPage> {
): Promise<McpPage> {
let page: Page;
if (isolatedContextName !== undefined) {
let ctx = this.#isolatedContexts.get(isolatedContextName);
Expand Down Expand Up @@ -316,15 +312,13 @@ export class McpContext implements Context {
await page.close({runBeforeUnload: false});
}

getNetworkRequestById(reqid: number): HTTPRequest {
return this.#networkCollector.getById(this.#resolveTargetPage(), reqid);
getNetworkRequestById(page: McpPage, reqid: number): HTTPRequest {
return this.#networkCollector.getById(page.pptrPage, reqid);
}

async restoreEmulation(targetPage?: Page) {
const page = targetPage ?? this.getSelectedPptrPage();
const mcpPage = this.#getMcpPage(page);
const currentSetting = mcpPage.emulationSettings;
await this.emulate(currentSetting, targetPage);
async restoreEmulation(page: McpPage) {
const currentSetting = page.emulationSettings;
await this.emulate(currentSetting, page.pptrPage);
}

async emulate(
Expand Down Expand Up @@ -440,30 +434,6 @@ export class McpContext implements Context {
}
}

getNetworkConditions(): string | null {
return this.#getMcpPage(this.#resolveTargetPage()).networkConditions;
}

getCpuThrottlingRate(): number {
return this.#getMcpPage(this.#resolveTargetPage()).cpuThrottlingRate;
}

getGeolocation(): GeolocationOptions | null {
return this.#getMcpPage(this.#resolveTargetPage()).geolocation;
}

getViewport(): Viewport | null {
return this.#getMcpPage(this.#resolveTargetPage()).viewport;
}

getUserAgent(): string | null {
return this.#getMcpPage(this.#resolveTargetPage()).userAgent;
}

getColorScheme(): 'dark' | 'light' | null {
return this.#getMcpPage(this.#resolveTargetPage()).colorScheme;
}

setIsRunningPerformanceTrace(x: boolean): void {
this.#isRunningTrace = x;
}
Expand Down Expand Up @@ -573,23 +543,20 @@ export class McpContext implements Context {
}

#updateSelectedPageTimeouts() {
const page = this.getSelectedPptrPage();
const page = this.#getSelectedMcpPage();
// For waiters 5sec timeout should be sufficient.
// Increased in case we throttle the CPU
const cpuMultiplier = this.getCpuThrottlingRate();
page.setDefaultTimeout(DEFAULT_TIMEOUT * cpuMultiplier);
const cpuMultiplier = page.cpuThrottlingRate;
page.pptrPage.setDefaultTimeout(DEFAULT_TIMEOUT * cpuMultiplier);
// 10sec should be enough for the load event to be emitted during
// navigations.
// Increased in case we throttle the network requests
const networkMultiplier = getNetworkMultiplierFromString(
this.getNetworkConditions(),
page.networkConditions,
);
page.pptrPage.setDefaultNavigationTimeout(
NAVIGATION_TIMEOUT * networkMultiplier,
);
page.setDefaultNavigationTimeout(NAVIGATION_TIMEOUT * networkMultiplier);
}

getNavigationTimeout() {
const page = this.#resolveTargetPage();
return page.getDefaultNavigationTimeout();
}

// Linear scan over per-page snapshots. The page count is small (typically
Expand Down Expand Up @@ -858,11 +825,10 @@ export class McpContext implements Context {
return this.#mcpPages.get(page)?.devToolsPage;
}

async getDevToolsData(): Promise<DevToolsData> {
async getDevToolsData(page: McpPage): Promise<DevToolsData> {
try {
this.logger('Getting DevTools UI data');
const selectedPage = this.#resolveTargetPage();
const devtoolsPage = this.getDevToolsPage(selectedPage);
const devtoolsPage = this.getDevToolsPage(page.pptrPage);
if (!devtoolsPage) {
this.logger('No DevTools page detected');
return {};
Expand Down Expand Up @@ -896,21 +862,19 @@ export class McpContext implements Context {
* Creates a text snapshot of a page.
*/
async createTextSnapshot(
page: McpPage,
verbose = false,
devtoolsData: DevToolsData | undefined = undefined,
targetPage?: Page,
): Promise<void> {
const page = targetPage ?? this.getSelectedPptrPage();
const mcpPage = this.#getMcpPage(page);
const rootNode = await page.accessibility.snapshot({
const rootNode = await page.pptrPage.accessibility.snapshot({
includeIframes: true,
interestingOnly: !verbose,
});
if (!rootNode) {
return;
}

const {uniqueBackendNodeIdToMcpId} = mcpPage;
const {uniqueBackendNodeIdToMcpId} = page;

const snapshotId = this.#nextSnapshotId++;
// Iterate through the whole accessibility node tree and assign node ids that
Expand Down Expand Up @@ -961,12 +925,12 @@ export class McpContext implements Context {
hasSelectedElement: false,
verbose,
};
mcpPage.textSnapshot = snapshot;
const data = devtoolsData ?? (await this.getDevToolsData());
page.textSnapshot = snapshot;
const data = devtoolsData ?? (await this.getDevToolsData(page));
if (data?.cdpBackendNodeId) {
snapshot.hasSelectedElement = true;
snapshot.selectedElementUid = this.resolveCdpElementId(
mcpPage,
page,
data?.cdpBackendNodeId,
);
}
Expand Down Expand Up @@ -1041,13 +1005,13 @@ export class McpContext implements Context {
action: () => Promise<unknown>,
options?: {timeout?: number},
): Promise<void> {
const page = this.getSelectedPptrPage();
const cpuMultiplier = this.getCpuThrottlingRate();
const page = this.#getSelectedMcpPage();
const cpuMultiplier = page.cpuThrottlingRate;
const networkMultiplier = getNetworkMultiplierFromString(
this.getNetworkConditions(),
page.networkConditions,
);
const waitForHelper = this.getWaitForHelper(
page,
page.pptrPage,
cpuMultiplier,
networkMultiplier,
);
Expand Down
47 changes: 32 additions & 15 deletions src/McpResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,10 +266,13 @@ export class McpResponse implements Response {

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,
this.#snapshotParams.page?.pptrPage,
);
const textSnapshot = context.getTextSnapshot(
this.#snapshotParams.page?.pptrPage,
Expand All @@ -290,7 +293,11 @@ export class McpResponse implements Response {

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, {
Expand All @@ -307,22 +314,24 @@ export class McpResponse implements Response {
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();
const devTools = context.getDevToolsUniverse(this.#page);
detailedConsoleMessage = await ConsoleFormatter.from(consoleMessage, {
id: consoleMessageStableId,
fetchDetailedData: true,
devTools: devTools ?? undefined,
});
} else if (message instanceof DevTools.AggregatedIssue) {
if (!this.#page) {
throw new Error(`Response must have an McpPage`);
}
const formatter = new IssueFormatter(message, {
id: consoleMessageStableId,
requestIdResolver: context.resolveCdpRequestId.bind(
Expand All @@ -349,7 +358,12 @@ export class McpResponse implements Response {
}
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,
);

Expand All @@ -374,7 +388,7 @@ export class McpResponse implements Response {
context.getConsoleMessageStableId(item);
if ('args' in item || item instanceof UncaughtError) {
const consoleMessage = item as ConsoleMessage | UncaughtError;
const devTools = context.getDevToolsUniverse();
const devTools = context.getDevToolsUniverse(page);
return await ConsoleFormatter.from(consoleMessage, {
id: consoleMessageStableId,
fetchDetailedData: false,
Expand All @@ -399,7 +413,11 @@ export class McpResponse implements Response {

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,
);

Expand Down Expand Up @@ -493,39 +511,38 @@ export class McpResponse implements Response {
response.push(...this.#textResponseLines);
}

const networkConditions = context.getNetworkConditions();
const networkConditions = this.#page?.networkConditions;
if (networkConditions) {
const timeout = this.#page!.pptrPage.getDefaultNavigationTimeout();
response.push(`## Network emulation`);
response.push(`Emulating: ${networkConditions}`);
response.push(
`Default navigation timeout set to ${context.getNavigationTimeout()} ms`,
);
response.push(`Default navigation timeout set to ${timeout} ms`);
structuredContent.networkConditions = networkConditions;
structuredContent.navigationTimeout = context.getNavigationTimeout();
structuredContent.navigationTimeout = timeout;
}

const viewport = context.getViewport();
const viewport = this.#page?.viewport;
if (viewport) {
response.push(`## Viewport emulation`);
response.push(`Emulating viewport: ${JSON.stringify(viewport)}`);
structuredContent.viewport = viewport;
}

const userAgent = context.getUserAgent();
const userAgent = this.#page?.userAgent;
if (userAgent) {
response.push(`## UserAgent emulation`);
response.push(`Emulating userAgent: ${userAgent}`);
structuredContent.userAgent = userAgent;
}

const cpuThrottlingRate = context.getCpuThrottlingRate();
const cpuThrottlingRate = this.#page?.cpuThrottlingRate ?? 1;
if (cpuThrottlingRate > 1) {
response.push(`## CPU emulation`);
response.push(`Emulating: ${cpuThrottlingRate}x slowdown`);
structuredContent.cpuThrottlingRate = cpuThrottlingRate;
}

const colorScheme = context.getColorScheme();
const colorScheme = this.#page?.colorScheme;
if (colorScheme) {
response.push(`## Color Scheme emulation`);
response.push(`Emulating: ${colorScheme}`);
Expand Down
Loading