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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,9 +364,12 @@ 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 |

`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 |
Expand Down
9 changes: 8 additions & 1 deletion src/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
4 changes: 4 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ 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)
(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
OPERA_CLI_BROWSER_URL Connect to an existing Chrome instance instead of launching one
Expand Down
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
61 changes: 61 additions & 0 deletions test/bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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).not.toContain("--categoryExtensions");
});

it("rejects whitespace-only OPERA_CLI_MCP_ARGS", () => {
process.env.OPERA_CLI_MCP_ARGS = " ";
const args = buildTransportArgs();
expect(args).not.toContain("--categoryExtensions");
});

it("rejects whitespace-only OPERA_CLI_CHROME_ARGS", () => {
process.env.OPERA_CLI_CHROME_ARGS = " ";
const args = buildTransportArgs();
expect(args.filter((a) => a.startsWith("--chrome-arg="))).toHaveLength(0);
});

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";
Expand Down
Loading