From 00ae84c2e5262de2ddbda6779e5ce81ecc193d39 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 14 Mar 2026 01:26:39 -0700 Subject: [PATCH] feat(browser): support headless MCP profile resolution --- src/browser/config.test.ts | 19 +++++++++++++++++++ src/browser/config.ts | 12 +++++++++--- src/browser/profile-capabilities.ts | 2 +- ...r-context.headless-default-profile.test.ts | 4 ++-- src/browser/server-context.ts | 4 ++-- src/config/types.browser.ts | 2 +- 6 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/browser/config.test.ts b/src/browser/config.test.ts index 5c16dd54dc6..09a54af27a1 100644 --- a/src/browser/config.test.ts +++ b/src/browser/config.test.ts @@ -26,6 +26,7 @@ describe("browser config", () => { expect(user?.driver).toBe("existing-session"); expect(user?.cdpPort).toBe(0); expect(user?.cdpUrl).toBe(""); + expect(user?.mcpTargetUrl).toBeUndefined(); const chromeRelay = resolveProfile(resolved, "chrome-relay"); expect(chromeRelay?.driver).toBe("extension"); expect(chromeRelay?.cdpPort).toBe(18792); @@ -121,6 +122,24 @@ describe("browser config", () => { expect(profile?.cdpIsLoopback).toBe(false); }); + it("supports MCP browser URLs for existing-session profiles", () => { + const resolved = resolveBrowserConfig({ + profiles: { + user: { + driver: "existing-session", + cdpUrl: "http://127.0.0.1:9222", + color: "#00AA00", + }, + }, + }); + + const profile = resolveProfile(resolved, "user"); + expect(profile?.driver).toBe("existing-session"); + expect(profile?.cdpUrl).toBe(""); + expect(profile?.mcpTargetUrl).toBe("http://127.0.0.1:9222"); + expect(profile?.cdpIsLoopback).toBe(true); + }); + it("uses profile cdpUrl when provided", () => { const resolved = resolveBrowserConfig({ profiles: { diff --git a/src/browser/config.ts b/src/browser/config.ts index 8bcd51d0a68..9845ebc3f56 100644 --- a/src/browser/config.ts +++ b/src/browser/config.ts @@ -45,6 +45,7 @@ export type ResolvedBrowserProfile = { cdpUrl: string; cdpHost: string; cdpIsLoopback: boolean; + mcpTargetUrl?: string; color: string; driver: "openclaw" | "extension" | "existing-session"; attachOnly: boolean; @@ -363,13 +364,18 @@ export function resolveProfile( : "openclaw"; if (driver === "existing-session") { - // existing-session uses Chrome MCP auto-connect; no CDP port/URL needed + const parsed = rawProfileUrl + ? parseHttpUrl(rawProfileUrl, `browser.profiles.${profileName}.cdpUrl`) + : null; + // existing-session uses Chrome MCP. It can either auto-connect to a local desktop + // session or connect to a debuggable browser URL/WS endpoint when explicitly configured. return { name: profileName, cdpPort: 0, cdpUrl: "", - cdpHost: "", - cdpIsLoopback: true, + cdpHost: parsed?.parsed.hostname ?? "", + cdpIsLoopback: parsed ? isLoopbackHost(parsed.parsed.hostname) : true, + ...(parsed ? { mcpTargetUrl: parsed.normalized } : {}), color: profile.color, driver, attachOnly: true, diff --git a/src/browser/profile-capabilities.ts b/src/browser/profile-capabilities.ts index b736a77d943..7543bcc7c13 100644 --- a/src/browser/profile-capabilities.ts +++ b/src/browser/profile-capabilities.ts @@ -41,7 +41,7 @@ export function getBrowserProfileCapabilities( if (profile.driver === "existing-session") { return { mode: "local-existing-session", - isRemote: false, + isRemote: !profile.cdpIsLoopback, usesChromeMcp: true, requiresRelay: false, requiresAttachedTab: false, diff --git a/src/browser/server-context.headless-default-profile.test.ts b/src/browser/server-context.headless-default-profile.test.ts index 4896c23647e..654a66af2cc 100644 --- a/src/browser/server-context.headless-default-profile.test.ts +++ b/src/browser/server-context.headless-default-profile.test.ts @@ -54,12 +54,12 @@ describe("browser server-context headless implicit default profile", () => { expect(ctx.forProfile().profile.name).toBe("openclaw"); }); - it("falls back from existing-session to openclaw when no profile is specified", () => { + it("keeps existing-session as the implicit default in headless mode", () => { const ctx = createBrowserRouteContext({ getState: () => makeState("user"), }); - expect(ctx.forProfile().profile.name).toBe("openclaw"); + expect(ctx.forProfile().profile.name).toBe("user"); }); it("keeps explicit interactive profile requests unchanged in headless mode", () => { diff --git a/src/browser/server-context.ts b/src/browser/server-context.ts index 16052027c39..6c8efb35b8b 100644 --- a/src/browser/server-context.ts +++ b/src/browser/server-context.ts @@ -53,7 +53,7 @@ function resolveImplicitProfileName(state: BrowserServerState): string { } const capabilities = getBrowserProfileCapabilities(defaultProfile); - if (!capabilities.requiresRelay && !capabilities.usesChromeMcp) { + if (!capabilities.requiresRelay) { return defaultProfileName; } @@ -63,7 +63,7 @@ function resolveImplicitProfileName(state: BrowserServerState): string { } const managedCapabilities = getBrowserProfileCapabilities(managedProfile); - if (managedCapabilities.requiresRelay || managedCapabilities.usesChromeMcp) { + if (managedCapabilities.requiresRelay) { return defaultProfileName; } diff --git a/src/config/types.browser.ts b/src/config/types.browser.ts index 5f8e28a0ebe..fcf73073fb6 100644 --- a/src/config/types.browser.ts +++ b/src/config/types.browser.ts @@ -1,7 +1,7 @@ export type BrowserProfileConfig = { /** CDP port for this profile. Allocated once at creation, persisted permanently. */ cdpPort?: number; - /** CDP URL for this profile (use for remote Chrome). */ + /** CDP URL for this profile (use for remote Chrome, or as browserUrl/wsEndpoint for existing-session MCP attach). */ cdpUrl?: string; /** Profile driver (default: openclaw). */ driver?: "openclaw" | "clawd" | "extension" | "existing-session";