From 13fc43422356673776f4dcbd78a22fd0d7d729f7 Mon Sep 17 00:00:00 2001 From: Mateusz Kupczyk Date: Mon, 14 Sep 2026 09:46:27 +0200 Subject: [PATCH 1/2] feat: forward MCP server flags via OPERA_CLI_MCP_ARGS env var Add OPERA_CLI_MCP_ARGS to buildTransportArgs so users can pass flags like --categoryExtensions to the MCP server at bridge startup without modifying source code. Closes the chrome-extension:// navigation gate for the bridge. --- README.md | 1 + src/bridge.ts | 9 ++++++- src/cli.ts | 3 +++ src/config.ts | 1 + test/bridge.test.ts | 61 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 74 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9b6183a..ebd6392 100644 --- a/README.md +++ b/README.md @@ -364,6 +364,7 @@ session is active or the no-session status/help block when one is not. | `OPERA_CLI_USER_DATA_DIR` | — | Persistent Chrome profile directory (skips isolated mode) | | `OPERA_CLI_HEADED` | `1` when an Opera binary is configured | `1` headed, `0` headless. Opera AI needs a window to sign in | | `OPERA_CLI_CHROME_ARGS` | — | Extra Chrome flags, space-separated | +| `OPERA_CLI_MCP_ARGS` | — | Extra MCP server flags, space-separated | | `OPERA_CLI_ENABLE_HOOKS` | — | Set to `1` to auto-install session hooks on startup | | `OPERA_CLI_TAKEOVER` | — | Set to `1` to restart a running Opera without asking | diff --git a/src/bridge.ts b/src/bridge.ts index e20f899..f64fbb2 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -709,11 +709,18 @@ export function buildTransportArgs(): string[] { const extraChromeArgs = process.env.OPERA_CLI_CHROME_ARGS; if (extraChromeArgs) { - for (const arg of extraChromeArgs.trim().split(/\s+/)) { + for (const arg of extraChromeArgs.trim().split(/\s+/).filter(Boolean)) { args.push(`--chrome-arg=${arg}`); } } + const extraMcpArgs = process.env.OPERA_CLI_MCP_ARGS; + if (extraMcpArgs) { + for (const arg of extraMcpArgs.trim().split(/\s+/).filter(Boolean)) { + args.push(arg); + } + } + return args; } diff --git a/src/cli.ts b/src/cli.ts index 6f6e0d7..f807cec 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -133,6 +133,9 @@ environment: OPERA_CLI_CHROME_ARGS Whitespace-separated Chrome flags forwarded to the browser (no shell-style quoting; flags with spaces are not supported) e.g. "--enable-gpu --ignore-gpu-blocklist" + OPERA_CLI_MCP_ARGS Whitespace-separated MCP server flags forwarded to the server + (no shell-style quoting; flags with spaces are not supported) + e.g. "--categoryExtensions" OPERA_CLI_PORT Base bridge port (default: 9225); the next 9 ports are tried in turn if it is occupied OPERA_CLI_BROWSER_URL Connect to an existing Chrome instance instead of launching one diff --git a/src/config.ts b/src/config.ts index a624b0a..73e5df0 100644 --- a/src/config.ts +++ b/src/config.ts @@ -28,6 +28,7 @@ export const KNOWN_CONFIG_KEYS = [ "OPERA_CLI_USER_DATA_DIR", "OPERA_CLI_HEADED", "OPERA_CLI_CHROME_ARGS", + "OPERA_CLI_MCP_ARGS", "OPERA_CLI_ENABLE_HOOKS", "OPERA_CLI_TAKEOVER", "OPERA_CLI_DEV", diff --git a/test/bridge.test.ts b/test/bridge.test.ts index d7a0ed2..940e459 100644 --- a/test/bridge.test.ts +++ b/test/bridge.test.ts @@ -139,11 +139,13 @@ describe("buildTransportArgs", () => { beforeEach(() => { savedEnv.OPERA_CLI_HEADED = process.env.OPERA_CLI_HEADED; savedEnv.OPERA_CLI_CHROME_ARGS = process.env.OPERA_CLI_CHROME_ARGS; + savedEnv.OPERA_CLI_MCP_ARGS = process.env.OPERA_CLI_MCP_ARGS; savedEnv.OPERA_CLI_BROWSER_URL = process.env.OPERA_CLI_BROWSER_URL; savedEnv.OPERA_CLI_USER_DATA_DIR = process.env.OPERA_CLI_USER_DATA_DIR; savedEnv.OPERA_CLI_EXECUTABLE_PATH = process.env.OPERA_CLI_EXECUTABLE_PATH; delete process.env.OPERA_CLI_HEADED; delete process.env.OPERA_CLI_CHROME_ARGS; + delete process.env.OPERA_CLI_MCP_ARGS; delete process.env.OPERA_CLI_BROWSER_URL; delete process.env.OPERA_CLI_USER_DATA_DIR; delete process.env.OPERA_CLI_EXECUTABLE_PATH; @@ -152,6 +154,7 @@ describe("buildTransportArgs", () => { afterEach(() => { process.env.OPERA_CLI_HEADED = savedEnv.OPERA_CLI_HEADED; process.env.OPERA_CLI_CHROME_ARGS = savedEnv.OPERA_CLI_CHROME_ARGS; + process.env.OPERA_CLI_MCP_ARGS = savedEnv.OPERA_CLI_MCP_ARGS; process.env.OPERA_CLI_BROWSER_URL = savedEnv.OPERA_CLI_BROWSER_URL; process.env.OPERA_CLI_USER_DATA_DIR = savedEnv.OPERA_CLI_USER_DATA_DIR; process.env.OPERA_CLI_EXECUTABLE_PATH = savedEnv.OPERA_CLI_EXECUTABLE_PATH; @@ -184,6 +187,64 @@ describe("buildTransportArgs", () => { expect(args.filter((a) => a.startsWith("--chrome-arg="))).toHaveLength(3); }); + it("forwards mcp server flags via OPERA_CLI_MCP_ARGS", () => { + process.env.OPERA_CLI_MCP_ARGS = "--categoryExtensions"; + const args = buildTransportArgs(); + expect(args).toContain("--categoryExtensions"); + }); + + it("forwards multiple mcp server flags", () => { + process.env.OPERA_CLI_MCP_ARGS = "--categoryExtensions --experimentalVision"; + const args = buildTransportArgs(); + expect(args).toContain("--categoryExtensions"); + expect(args).toContain("--experimentalVision"); + }); + + it("handles whitespace in mcp args like chrome args", () => { + process.env.OPERA_CLI_MCP_ARGS = " --flag-a\t--flag-b\n--flag-c "; + const args = buildTransportArgs(); + expect(args).toContain("--flag-a"); + expect(args).toContain("--flag-b"); + expect(args).toContain("--flag-c"); + expect(args.filter((a) => a.startsWith("--flag"))).toHaveLength(3); + }); + + it("defaults to no extra mcp args when OPERA_CLI_MCP_ARGS is unset", () => { + delete process.env.OPERA_CLI_MCP_ARGS; + const args = buildTransportArgs(); + expect(args).toEqual(["--no-page-id-routing", "--isolated", "--headless"]); + }); + + it("rejects whitespace-only OPERA_CLI_MCP_ARGS", () => { + process.env.OPERA_CLI_MCP_ARGS = " "; + const args = buildTransportArgs(); + expect(args).toEqual(["--no-page-id-routing", "--isolated", "--headless"]); + }); + + it("rejects whitespace-only OPERA_CLI_CHROME_ARGS", () => { + process.env.OPERA_CLI_CHROME_ARGS = " "; + const args = buildTransportArgs(); + expect(args).toEqual(["--no-page-id-routing", "--isolated", "--headless"]); + }); + + it("combines mcp args with chrome args", () => { + process.env.OPERA_CLI_MCP_ARGS = "--categoryExtensions"; + process.env.OPERA_CLI_CHROME_ARGS = "--some-flag"; + const args = buildTransportArgs(); + expect(args).toContain("--categoryExtensions"); + expect(args).toContain("--chrome-arg=--some-flag"); + }); + + it("passes mcp args alongside --browserUrl", () => { + process.env.OPERA_CLI_BROWSER_URL = "http://127.0.0.1:9222"; + process.env.OPERA_CLI_MCP_ARGS = "--categoryExtensions"; + const args = buildTransportArgs(); + expect(args).toContain("--browserUrl=http://127.0.0.1:9222"); + expect(args).toContain("--categoryExtensions"); + expect(args).not.toContain("--isolated"); + expect(args).not.toContain("--headless"); + }); + it("combines headed mode with chrome args", () => { process.env.OPERA_CLI_HEADED = "1"; process.env.OPERA_CLI_CHROME_ARGS = "--enable-unsafe-webgpu"; From 3748b400815453e10c5dec00ea538b563cf5d04f Mon Sep 17 00:00:00 2001 From: Mateusz Kupczyk Date: Mon, 14 Sep 2026 12:43:29 +0200 Subject: [PATCH 2/2] fixup! feat: forward MCP server flags via OPERA_CLI_MCP_ARGS env var --- README.md | 2 ++ src/cli.ts | 1 + test/bridge.test.ts | 6 +++--- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ebd6392..6212ea9 100644 --- a/README.md +++ b/README.md @@ -368,6 +368,8 @@ session is active or the no-session status/help block when one is not. | `OPERA_CLI_ENABLE_HOOKS` | — | Set to `1` to auto-install session hooks on startup | | `OPERA_CLI_TAKEOVER` | — | Set to `1` to restart a running Opera without asking | +`OPERA_CLI_MCP_ARGS` values are raw server arguments, forwarded verbatim after the bridge's own flags: a misspelled or unknown flag goes straight to the MCP server and can fail bridge startup, and a valid one can override bridge-managed flags such as `--isolated`, `--headless`, or `--userDataDir`. `OPERA_CLI_CHROME_ARGS` is namespaced (`--chrome-arg=…`), so it only affects the browser launch. + State is stored in `~/.opera-browser-cli/`: | File | Purpose | diff --git a/src/cli.ts b/src/cli.ts index f807cec..bdefe80 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -135,6 +135,7 @@ environment: e.g. "--enable-gpu --ignore-gpu-blocklist" OPERA_CLI_MCP_ARGS Whitespace-separated MCP server flags forwarded to the server (no shell-style quoting; flags with spaces are not supported) + (raw server args, appended after bridge-set flags, so they can override --isolated/--headless/--userDataDir) e.g. "--categoryExtensions" OPERA_CLI_PORT Base bridge port (default: 9225); the next 9 ports are tried in turn if it is occupied diff --git a/test/bridge.test.ts b/test/bridge.test.ts index 940e459..25d4a72 100644 --- a/test/bridge.test.ts +++ b/test/bridge.test.ts @@ -212,19 +212,19 @@ describe("buildTransportArgs", () => { it("defaults to no extra mcp args when OPERA_CLI_MCP_ARGS is unset", () => { delete process.env.OPERA_CLI_MCP_ARGS; const args = buildTransportArgs(); - expect(args).toEqual(["--no-page-id-routing", "--isolated", "--headless"]); + expect(args).not.toContain("--categoryExtensions"); }); it("rejects whitespace-only OPERA_CLI_MCP_ARGS", () => { process.env.OPERA_CLI_MCP_ARGS = " "; const args = buildTransportArgs(); - expect(args).toEqual(["--no-page-id-routing", "--isolated", "--headless"]); + expect(args).not.toContain("--categoryExtensions"); }); it("rejects whitespace-only OPERA_CLI_CHROME_ARGS", () => { process.env.OPERA_CLI_CHROME_ARGS = " "; const args = buildTransportArgs(); - expect(args).toEqual(["--no-page-id-routing", "--isolated", "--headless"]); + expect(args.filter((a) => a.startsWith("--chrome-arg="))).toHaveLength(0); }); it("combines mcp args with chrome args", () => {