mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-25 17:02:46 +00:00
feat(browser): add headless and target-url Chrome MCP modes
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import {
|
||||
buildChromeMcpLaunchPlanForTest,
|
||||
evaluateChromeMcpScript,
|
||||
listChromeMcpTabs,
|
||||
openChromeMcpTab,
|
||||
@@ -7,6 +9,10 @@ import {
|
||||
setChromeMcpSessionFactoryForTest,
|
||||
} from "./chrome-mcp.js";
|
||||
|
||||
vi.mock("../config/config.js", () => ({
|
||||
loadConfig: vi.fn(),
|
||||
}));
|
||||
|
||||
type ToolCall = {
|
||||
name: string;
|
||||
arguments?: Record<string, unknown>;
|
||||
@@ -79,6 +85,99 @@ function createFakeSession(): ChromeMcpSession {
|
||||
describe("chrome MCP page parsing", () => {
|
||||
beforeEach(async () => {
|
||||
await resetChromeMcpSessionsForTest();
|
||||
vi.mocked(loadConfig).mockReturnValue({
|
||||
browser: {
|
||||
profiles: {
|
||||
"chrome-live": {
|
||||
driver: "existing-session",
|
||||
attachOnly: true,
|
||||
color: "#00AA00",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("uses autoConnect for desktop existing-session profiles", () => {
|
||||
const plan = buildChromeMcpLaunchPlanForTest("chrome-live");
|
||||
expect(plan.mode).toBe("autoConnect");
|
||||
expect(plan.args).toContain("--autoConnect");
|
||||
});
|
||||
|
||||
it("uses headless launch flags for headless existing-session profiles", () => {
|
||||
vi.mocked(loadConfig).mockReturnValue({
|
||||
browser: {
|
||||
headless: true,
|
||||
noSandbox: true,
|
||||
executablePath: "/usr/bin/google-chrome-stable",
|
||||
extraArgs: ["--disable-dev-shm-usage"],
|
||||
profiles: {
|
||||
"chrome-live": {
|
||||
driver: "existing-session",
|
||||
attachOnly: true,
|
||||
color: "#00AA00",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const plan = buildChromeMcpLaunchPlanForTest("chrome-live");
|
||||
expect(plan.mode).toBe("headless");
|
||||
expect(plan.args).toEqual(
|
||||
expect.arrayContaining([
|
||||
"--headless",
|
||||
"--userDataDir",
|
||||
expect.stringContaining("/browser/chrome-live/user-data"),
|
||||
"--executablePath",
|
||||
"/usr/bin/google-chrome-stable",
|
||||
"--chromeArg",
|
||||
"--no-sandbox",
|
||||
"--chromeArg",
|
||||
"--disable-setuid-sandbox",
|
||||
"--chromeArg",
|
||||
"--disable-dev-shm-usage",
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses browserUrl for MCP profiles configured with an HTTP target", () => {
|
||||
vi.mocked(loadConfig).mockReturnValue({
|
||||
browser: {
|
||||
profiles: {
|
||||
"chrome-live": {
|
||||
driver: "existing-session",
|
||||
attachOnly: true,
|
||||
cdpUrl: "http://127.0.0.1:9222",
|
||||
color: "#00AA00",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const plan = buildChromeMcpLaunchPlanForTest("chrome-live");
|
||||
expect(plan.mode).toBe("browserUrl");
|
||||
expect(plan.args).toEqual(expect.arrayContaining(["--browserUrl", "http://127.0.0.1:9222"]));
|
||||
});
|
||||
|
||||
it("uses wsEndpoint for MCP profiles configured with a WebSocket target", () => {
|
||||
vi.mocked(loadConfig).mockReturnValue({
|
||||
browser: {
|
||||
profiles: {
|
||||
"chrome-live": {
|
||||
driver: "existing-session",
|
||||
attachOnly: true,
|
||||
cdpUrl: "ws://127.0.0.1:9222/devtools/browser/abc",
|
||||
color: "#00AA00",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const plan = buildChromeMcpLaunchPlanForTest("chrome-live");
|
||||
expect(plan.mode).toBe("browserUrl");
|
||||
expect(plan.args).toEqual(
|
||||
expect.arrayContaining(["--wsEndpoint", "ws://127.0.0.1:9222/devtools/browser/abc"]),
|
||||
);
|
||||
});
|
||||
|
||||
it("parses list_pages text responses when structuredContent is missing", async () => {
|
||||
|
||||
@@ -4,8 +4,11 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import type { ChromeMcpSnapshotNode } from "./chrome-mcp.snapshot.js";
|
||||
import { resolveOpenClawUserDataDir } from "./chrome.js";
|
||||
import type { BrowserTab } from "./client.js";
|
||||
import { resolveBrowserConfig, resolveProfile } from "./config.js";
|
||||
import { BrowserProfileUnavailableError, BrowserTabNotFoundError } from "./errors.js";
|
||||
|
||||
type ChromeMcpStructuredPage = {
|
||||
@@ -32,7 +35,6 @@ const DEFAULT_CHROME_MCP_COMMAND = "npx";
|
||||
const DEFAULT_CHROME_MCP_ARGS = [
|
||||
"-y",
|
||||
"chrome-devtools-mcp@latest",
|
||||
"--autoConnect",
|
||||
// Direct chrome-devtools-mcp launches do not enable structuredContent by default.
|
||||
"--experimentalStructuredContent",
|
||||
"--experimental-page-id-routing",
|
||||
@@ -42,6 +44,50 @@ const sessions = new Map<string, ChromeMcpSession>();
|
||||
const pendingSessions = new Map<string, Promise<ChromeMcpSession>>();
|
||||
let sessionFactory: ChromeMcpSessionFactory | null = null;
|
||||
|
||||
type ChromeMcpLaunchPlan = {
|
||||
args: string[];
|
||||
mode: "autoConnect" | "browserUrl" | "headless";
|
||||
};
|
||||
|
||||
function buildChromeMcpLaunchPlan(profileName: string): ChromeMcpLaunchPlan {
|
||||
const cfg = loadConfig();
|
||||
const resolved = resolveBrowserConfig(cfg.browser, cfg);
|
||||
const profile = resolveProfile(resolved, profileName);
|
||||
if (!profile || profile.driver !== "existing-session") {
|
||||
throw new BrowserProfileUnavailableError(
|
||||
`Chrome MCP profile "${profileName}" is missing or is not driver=existing-session.`,
|
||||
);
|
||||
}
|
||||
|
||||
const args = [...DEFAULT_CHROME_MCP_ARGS];
|
||||
if (profile.mcpTargetUrl) {
|
||||
const parsed = new URL(profile.mcpTargetUrl);
|
||||
args.push(
|
||||
parsed.protocol === "ws:" || parsed.protocol === "wss:" ? "--wsEndpoint" : "--browserUrl",
|
||||
profile.mcpTargetUrl,
|
||||
);
|
||||
return { args, mode: "browserUrl" };
|
||||
}
|
||||
|
||||
if (!resolved.headless) {
|
||||
args.push("--autoConnect");
|
||||
return { args, mode: "autoConnect" };
|
||||
}
|
||||
|
||||
args.push("--headless");
|
||||
args.push("--userDataDir", resolveOpenClawUserDataDir(profile.name));
|
||||
if (resolved.executablePath) {
|
||||
args.push("--executablePath", resolved.executablePath);
|
||||
}
|
||||
if (resolved.noSandbox) {
|
||||
args.push("--chromeArg", "--no-sandbox", "--chromeArg", "--disable-setuid-sandbox");
|
||||
}
|
||||
for (const arg of resolved.extraArgs) {
|
||||
args.push("--chromeArg", arg);
|
||||
}
|
||||
return { args, mode: "headless" };
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
@@ -169,9 +215,10 @@ function extractJsonMessage(result: ChromeMcpToolResult): unknown {
|
||||
}
|
||||
|
||||
async function createRealSession(profileName: string): Promise<ChromeMcpSession> {
|
||||
const launchPlan = buildChromeMcpLaunchPlan(profileName);
|
||||
const transport = new StdioClientTransport({
|
||||
command: DEFAULT_CHROME_MCP_COMMAND,
|
||||
args: DEFAULT_CHROME_MCP_ARGS,
|
||||
args: launchPlan.args,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = new Client(
|
||||
@@ -191,9 +238,15 @@ async function createRealSession(profileName: string): Promise<ChromeMcpSession>
|
||||
}
|
||||
} catch (err) {
|
||||
await client.close().catch(() => {});
|
||||
const hint =
|
||||
launchPlan.mode === "autoConnect"
|
||||
? "Make sure Chrome is running, enable chrome://inspect/#remote-debugging, and approve the connection."
|
||||
: launchPlan.mode === "browserUrl"
|
||||
? "Make sure the configured browserUrl/wsEndpoint is reachable and Chrome is running with remote debugging enabled."
|
||||
: "Make sure a Chrome executable is available, and use browser.noSandbox=true on Linux containers/root setups when needed.";
|
||||
throw new BrowserProfileUnavailableError(
|
||||
`Chrome MCP existing-session attach failed for profile "${profileName}". ` +
|
||||
`Make sure Chrome is running, enable chrome://inspect/#remote-debugging, and approve the connection. ` +
|
||||
`${hint} ` +
|
||||
`Details: ${String(err)}`,
|
||||
);
|
||||
}
|
||||
@@ -531,6 +584,10 @@ export async function waitForChromeMcpText(params: {
|
||||
});
|
||||
}
|
||||
|
||||
export function buildChromeMcpLaunchPlanForTest(profileName: string): ChromeMcpLaunchPlan {
|
||||
return buildChromeMcpLaunchPlan(profileName);
|
||||
}
|
||||
|
||||
export function setChromeMcpSessionFactoryForTest(factory: ChromeMcpSessionFactory | null): void {
|
||||
sessionFactory = factory;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user