feat: add --(allowed|blocked)-origins (#319)

Useful to limit the agent when using the playwright-mcp server with an
agent in auto-invocation mode.

Not intended to be a security feature.
This commit is contained in:
Ross Wollman
2025-05-05 11:28:14 -07:00
committed by GitHub
parent 4694d60fc5
commit 42faa3ccf8
6 changed files with 149 additions and 0 deletions

View File

@@ -36,6 +36,8 @@ export type CLIOptions = {
host?: string;
vision?: boolean;
config?: string;
allowedOrigins?: string[];
blockedOrigins?: string[];
outputDir?: string;
};
@@ -50,6 +52,10 @@ const defaultConfig: Config = {
viewport: null,
},
},
network: {
allowedOrigins: undefined,
blockedOrigins: undefined,
},
};
export async function resolveConfig(cliOptions: CLIOptions): Promise<Config> {
@@ -110,6 +116,10 @@ export async function configFromCLIOptions(cliOptions: CLIOptions): Promise<Conf
},
capabilities: cliOptions.caps?.split(',').map((c: string) => c.trim() as ToolCapability),
vision: !!cliOptions.vision,
network: {
allowedOrigins: cliOptions.allowedOrigins,
blockedOrigins: cliOptions.blockedOrigins,
},
outputDir: cliOptions.outputDir,
};
}
@@ -171,5 +181,9 @@ function mergeConfig(base: Config, overrides: Config): Config {
...pickDefined(base),
...pickDefined(overrides),
browser,
network: {
...pickDefined(base.network),
...pickDefined(overrides.network),
},
};
}

View File

@@ -290,11 +290,26 @@ ${code.join('\n')}
}).catch(() => {});
}
private async _setupRequestInterception(context: playwright.BrowserContext) {
if (this.config.network?.allowedOrigins?.length) {
await context.route('**', route => route.abort('blockedbyclient'));
for (const origin of this.config.network.allowedOrigins)
await context.route(`*://${origin}/**`, route => route.continue());
}
if (this.config.network?.blockedOrigins?.length) {
for (const origin of this.config.network.blockedOrigins)
await context.route(`*://${origin}/**`, route => route.abort('blockedbyclient'));
}
}
private async _ensureBrowserContext() {
if (!this._browserContext) {
const context = await this._createBrowserContext();
this._browser = context.browser;
this._browserContext = context.browserContext;
await this._setupRequestInterception(this._browserContext);
for (const page of this._browserContext.pages())
this._onPageCreated(page);
this._browserContext.on('page', page => this._onPageCreated(page));

View File

@@ -37,6 +37,8 @@ program
.option('--user-data-dir <path>', 'Path to the user data directory')
.option('--port <port>', 'Port to listen on for SSE transport.')
.option('--host <host>', 'Host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.')
.option('--allowed-origins <origins>', 'Semicolon-separated list of origins to allow the browser to request. Default is to allow all.', semicolonSeparatedList)
.option('--blocked-origins <origins>', 'Semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed.', semicolonSeparatedList)
.option('--vision', 'Run server that uses screenshots (Aria snapshots are used by default)')
.option('--output-dir <path>', 'Path to the directory for output files.')
.option('--config <path>', 'Path to the configuration file.')
@@ -63,4 +65,8 @@ function setupExitWatchdog(serverList: ServerList) {
process.on('SIGTERM', handleExit);
}
function semicolonSeparatedList(value: string): string[] {
return value.split(';').map(v => v.trim());
}
program.parse(process.argv);