Skip to content

feat(cors): Add CORS policy check and response to browsers. #450

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
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
91 changes: 48 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,49 +114,54 @@ Playwright MCP server supports following arguments. They can be provided in the

```
> npx @playwright/mcp@latest --help
--allowed-origins <origins> semicolon-separated list of origins to allow the
browser to request. Default is to allow all.
--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.
--block-service-workers block service workers
--browser <browser> browser or chrome channel to use, possible
values: chrome, firefox, webkit, msedge.
--caps <caps> comma-separated list of capabilities to enable,
possible values: tabs, pdf, history, wait, files,
install. Default is all.
--cdp-endpoint <endpoint> CDP endpoint to connect to.
--config <path> path to the configuration file.
--device <device> device to emulate, for example: "iPhone 15"
--executable-path <path> path to the browser executable.
--headless run browser in headless mode, headed by default
--host <host> host to bind server to. Default is localhost. Use
0.0.0.0 to bind to all interfaces.
--ignore-https-errors ignore https errors
--isolated keep the browser profile in memory, do not save
it to disk.
--no-image-responses do not send image responses to the client.
--no-sandbox disable the sandbox for all process types that
are normally sandboxed.
--output-dir <path> path to the directory for output files.
--port <port> port to listen on for SSE transport.
--proxy-bypass <bypass> comma-separated domains to bypass proxy, for
example ".com,chromium.org,.domain.com"
--proxy-server <proxy> specify proxy server, for example
"http://myproxy:3128" or "socks5://myproxy:8080"
--save-trace Whether to save the Playwright Trace of the
session into the output directory.
--storage-state <path> path to the storage state file for isolated
sessions.
--user-agent <ua string> specify user agent string
--user-data-dir <path> path to the user data directory. If not
specified, a temporary directory will be created.
--viewport-size <size> specify browser viewport size in pixels, for
example "1280, 720"
--vision Run server that uses screenshots (Aria snapshots
are used by default)
--allowed-origins <origins> semicolon-separated list of origins to allow
the browser to request. Default is to allow
all.
--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.
--block-service-workers block service workers
--browser <browser> browser or chrome channel to use, possible
values: chrome, firefox, webkit, msedge.
--caps <caps> comma-separated list of capabilities to enable,
possible values: tabs, pdf, history, wait,
files, install. Default is all.
--cdp-endpoint <endpoint> CDP endpoint to connect to.
--config <path> path to the configuration file.
--cors-allow-origins <origin> semicolon-separated list of allowed origins by
CORS policy. Can be regex.
--device <device> device to emulate, for example: "iPhone 15"
--executable-path <path> path to the browser executable.
--headless run browser in headless mode, headed by default
--host <host> host to bind server to. Default is localhost.
Use 0.0.0.0 to bind to all interfaces.
--ignore-https-errors ignore https errors
--isolated keep the browser profile in memory, do not save
it to disk.
--no-image-responses do not send image responses to the client.
--no-sandbox disable the sandbox for all process types that
are normally sandboxed.
--output-dir <path> path to the directory for output files.
--port <port> port to listen on for SSE transport.
--proxy-bypass <bypass> comma-separated domains to bypass proxy, for
example ".com,chromium.org,.domain.com"
--proxy-server <proxy> specify proxy server, for example
"http://myproxy:3128" or
"socks5://myproxy:8080"
--save-trace Whether to save the Playwright Trace of the
session into the output directory.
--storage-state <path> path to the storage state file for isolated
sessions.
--user-agent <ua string> specify user agent string
--user-data-dir <path> path to the user data directory. If not
specified, a temporary directory will be
created.
--viewport-size <size> specify browser viewport size in pixels, for
example "1280, 720"
--vision Run server that uses screenshots (Aria
snapshots are used by default)
```

<!--- End of options generated section -->
Expand Down
5 changes: 5 additions & 0 deletions config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ export type Config = {
* The host to bind the server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.
*/
host?: string;

/**
* Origins to allow by CORS policy. Can be regex.
*/
corsAllowOrigins?: RegExp[];
},

/**
Expand Down
2 changes: 2 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export type CLIOptions = {
caps?: string;
cdpEndpoint?: string;
config?: string;
corsAllowOrigins?: string[];
device?: string;
executablePath?: string;
headless?: boolean;
Expand Down Expand Up @@ -184,6 +185,7 @@ export async function configFromCLIOptions(cliOptions: CLIOptions): Promise<Conf
server: {
port: cliOptions.port,
host: cliOptions.host,
corsAllowOrigins: cliOptions.corsAllowOrigins ? cliOptions.corsAllowOrigins.map(o => new RegExp(o, 'i')) : undefined,
},
capabilities: cliOptions.caps?.split(',').map((c: string) => c.trim() as ToolCapability),
vision: !!cliOptions.vision,
Expand Down
1 change: 1 addition & 0 deletions src/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ program
.option('--caps <caps>', 'comma-separated list of capabilities to enable, possible values: tabs, pdf, history, wait, files, install. Default is all.')
.option('--cdp-endpoint <endpoint>', 'CDP endpoint to connect to.')
.option('--config <path>', 'path to the configuration file.')
.option('--cors-allow-origins <origin>', 'semicolon-separated list of allowed origins by CORS policy. Can be regex.', semicolonSeparatedList)
.option('--device <device>', 'device to emulate, for example: "iPhone 15"')
.option('--executable-path <path>', 'path to the browser executable.')
.option('--headless', 'run browser in headless mode, headed by default')
Expand Down
29 changes: 29 additions & 0 deletions src/transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,31 @@ export async function startStdioTransport(config: FullConfig, connectionList: Co
connectionList.push(connection);
}

function checkCors(config: FullConfig, req: http.IncomingMessage, res: http.ServerResponse): boolean {
if (config.server?.corsAllowOrigins === undefined)
return false;

const origin = req.headers.origin;
if (origin === undefined)
return false;

if (config.server.corsAllowOrigins.some(re => re.test(origin))) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
res.setHeader('Access-Control-Max-Age', 2592000);
res.setHeader('Access-Control-Allow-Headers', 'mcp-session-id, content-type');
return true;
}

return false;
}

async function handleSSE(config: FullConfig, req: http.IncomingMessage, res: http.ServerResponse, url: URL, sessions: Map<string, SSEServerTransport>, connectionList: Connection[]) {
if (checkCors(config, req, res) && req.method === 'OPTIONS') {
res.statusCode = 204;
return res.end();
}

if (req.method === 'POST') {
const sessionId = url.searchParams.get('sessionId');
if (!sessionId) {
Expand Down Expand Up @@ -80,6 +104,11 @@ async function handleStreamable(config: FullConfig, req: http.IncomingMessage, r
return await transport.handleRequest(req, res);
}

if (checkCors(config, req, res) && req.method === 'OPTIONS') {
res.statusCode = 204;
return res.end();
}

if (req.method === 'POST') {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => crypto.randomUUID(),
Expand Down
Loading