feat: add Web Push Notification support with pull-based retrieval

Adds comprehensive Web Notification API support:
- Intercepts Notification constructor via page.addInitScript()
- Bridges notification data to Node.js via page.exposeFunction()
- Stores notifications in Tab and Context for pull-based retrieval

New MCP tools:
- browser_configure_notifications: grant/deny permissions per origin
- browser_list_notifications: query captured notifications
- browser_handle_notification: click or close notifications
- browser_wait_notification: wait for matching notification
- browser_clear_notifications: clear notification history

Architecture uses modal state pattern for notification handling,
consistent with existing dialog and file chooser patterns.
This commit is contained in:
Ryan Malloy 2026-01-12 19:31:26 -07:00
parent eaf8349203
commit 1c23c5f2f7
5 changed files with 440 additions and 3 deletions

View file

@ -24,7 +24,7 @@ import { EnvironmentIntrospector } from './environmentIntrospection.js';
import { RequestInterceptor, RequestInterceptorOptions } from './requestInterceptor.js';
import { ArtifactManagerRegistry } from './artifactManager.js';
import type { Tool } from './tools/tool.js';
import type { Tool, WebNotification } from './tools/tool.js';
import type { FullConfig } from './config.js';
import type { BrowserContextFactory } from './browserContextFactory.js';
import type { InjectionConfig } from './tools/codeInjection.js';
@ -94,6 +94,9 @@ export class Context {
// Code injection for debug toolbar and custom scripts
injectionConfig: InjectionConfig | undefined;
// Browser notifications storage
private _notifications: WebNotification[] = [];
constructor(tools: Tool[], config: FullConfig, browserContextFactory: BrowserContextFactory, environmentIntrospector?: EnvironmentIntrospector) {
this.tools = tools;
this.config = config;
@ -157,6 +160,23 @@ export class Context {
return this._currentTab;
}
// Notification management methods
addNotification(notification: WebNotification): void {
this._notifications.push(notification);
}
notifications(): WebNotification[] {
return this._notifications;
}
getNotification(id: string): WebNotification | undefined {
return this._notifications.find(n => n.id === id);
}
clearNotifications(): void {
this._notifications.length = 0;
}
async newTab(): Promise<Tab> {
const { browserContext } = await this._ensureBrowserContext();
const page = await browserContext.newPage();
@ -358,6 +378,17 @@ export class Context {
return this._browserContextPromise;
}
/**
* Returns the existing browser context if one has been created, or undefined.
* Does not create a new context - use for operations that need an existing context.
*/
async existingBrowserContext(): Promise<playwright.BrowserContext | undefined> {
if (!this._browserContextPromise)
return undefined;
const { browserContext } = await this._browserContextPromise;
return browserContext;
}
private async _setupBrowserContext(): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise<void> }> {
if (this._closeBrowserContextPromise)
throw new Error('Another browser context is being closed.');