diff --git a/README.md b/README.md
index a4e7cf0538..a1d84e9a6b 100644
--- a/README.md
+++ b/README.md
@@ -47,6 +47,19 @@ verboo /login
- **Tool calling**: Multi-step tool loops with model calls, tool execution, and follow-up responses
- **Images**: URL and base64 image inputs for providers that support vision
+## Verboo in Chrome
+
+The Verboo desktop app installs and maintains the local `verboo-in-chrome` MCP
+on macOS, Windows, and Linux. Configure it in **Settings > Integrations >
+Chrome**. Once the managed MCP entry is present, the CLI connects to it
+automatically and exposes the `verboo-in-chrome` skill for browser tasks.
+
+Use `verboo --no-chrome` to disable the integration for one session,
+`verboo --chrome` to require it for one session, and `/chrome` to inspect its
+status. The transport stays local, does not copy CLI credentials into the
+extension, and continues to enforce the extension's site permissions and
+approval flow.
+
## Web Search and Fetch
`WebSearch` uses DuckDuckGo by default.
diff --git a/src/commands/chrome/chrome.tsx b/src/commands/chrome/chrome.tsx
index 6b933ba24c..d30dbe7aa4 100644
--- a/src/commands/chrome/chrome.tsx
+++ b/src/commands/chrome/chrome.tsx
@@ -1,284 +1,75 @@
-import { c as _c } from "react-compiler-runtime";
-import React, { useState } from 'react';
-import { type OptionWithDescription, Select } from '../../components/CustomSelect/select.js';
-import { Dialog } from '../../components/design-system/Dialog.js';
-import { Box, Text } from '../../ink.js';
-import { useAppState } from '../../state/AppState.js';
-import { isClaudeAISubscriber } from '../../utils/auth.js';
-import { openBrowser } from '../../utils/browser.js';
-import { CLAUDE_IN_CHROME_MCP_SERVER_NAME, openInChrome } from '../../utils/claudeInChrome/common.js';
-import { isChromeExtensionInstalled } from '../../utils/claudeInChrome/setup.js';
-import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js';
-import { env } from '../../utils/env.js';
-import { isRunningOnHomespace } from '../../utils/envUtils.js';
-const CHROME_EXTENSION_URL = 'https://claude.ai/chrome';
-const CHROME_PERMISSIONS_URL = 'https://clau.de/chrome/permissions';
-const CHROME_RECONNECT_URL = 'https://clau.de/chrome/reconnect';
-type MenuAction = 'install-extension' | 'reconnect' | 'manage-permissions' | 'toggle-default';
+import type React from 'react'
+import { Box, Text } from '../../ink.js'
+import { useAppState } from '../../state/AppState.js'
+import { getGlobalConfig } from '../../utils/config.js'
+import {
+ VERBOO_IN_CHROME_MCP_SERVER_NAME,
+ isManagedVerbooInChromeConfig,
+ isVerbooInChromeMcpServer,
+} from '../../utils/verbooInChrome.js'
+import { Dialog } from '../../components/design-system/Dialog.js'
+
type Props = {
- onDone: (result?: string) => void;
- isExtensionInstalled: boolean;
- configEnabled: boolean | undefined;
- isClaudeAISubscriber: boolean;
- isWSL: boolean;
-};
-function ClaudeInChromeMenu(t0) {
- const $ = _c(41);
- const {
- onDone,
- isExtensionInstalled: installed,
- configEnabled,
- isClaudeAISubscriber,
- isWSL
- } = t0;
- const mcpClients = useAppState(_temp);
- const [selectKey, setSelectKey] = useState(0);
- const [enabledByDefault, setEnabledByDefault] = useState(configEnabled ?? false);
- const [showInstallHint, setShowInstallHint] = useState(false);
- const [isExtensionInstalled, setIsExtensionInstalled] = useState(installed);
- let t1;
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = false && isRunningOnHomespace();
- $[0] = t1;
- } else {
- t1 = $[0];
- }
- const isHomespace = t1;
- let t2;
- if ($[1] !== mcpClients) {
- t2 = mcpClients.find(_temp2);
- $[1] = mcpClients;
- $[2] = t2;
- } else {
- t2 = $[2];
- }
- const chromeClient = t2;
- const isConnected = chromeClient?.type === "connected";
- let t3;
- if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
- t3 = function openUrl(url) {
- if (isHomespace) {
- openBrowser(url);
- } else {
- openInChrome(url);
- }
- };
- $[3] = t3;
- } else {
- t3 = $[3];
- }
- const openUrl = t3;
- let t4;
- if ($[4] !== enabledByDefault) {
- t4 = function handleAction(action) {
- bb22: switch (action) {
- case "install-extension":
- {
- setSelectKey(_temp3);
- setShowInstallHint(true);
- openUrl(CHROME_EXTENSION_URL);
- break bb22;
- }
- case "reconnect":
- {
- setSelectKey(_temp4);
- isChromeExtensionInstalled().then(installed_0 => {
- setIsExtensionInstalled(installed_0);
- if (installed_0) {
- setShowInstallHint(false);
- }
- });
- openUrl(CHROME_RECONNECT_URL);
- break bb22;
- }
- case "manage-permissions":
- {
- setSelectKey(_temp5);
- openUrl(CHROME_PERMISSIONS_URL);
- break bb22;
- }
- case "toggle-default":
- {
- const newValue = !enabledByDefault;
- saveGlobalConfig(current => ({
- ...current,
- claudeInChromeDefaultEnabled: newValue
- }));
- setEnabledByDefault(newValue);
- }
- }
- };
- $[4] = enabledByDefault;
- $[5] = t4;
- } else {
- t4 = $[5];
- }
- const handleAction = t4;
- let options;
- if ($[6] !== enabledByDefault || $[7] !== isExtensionInstalled) {
- options = [];
- const requiresExtensionSuffix = isExtensionInstalled ? "" : " (requires extension)";
- if (!isExtensionInstalled && !isHomespace) {
- let t5;
- if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
- t5 = {
- label: "Install Chrome extension",
- value: "install-extension"
- };
- $[9] = t5;
- } else {
- t5 = $[9];
- }
- options.push(t5);
- }
- let t5;
- if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
- t5 = Manage permissions;
- $[10] = t5;
- } else {
- t5 = $[10];
- }
- let t6;
- if ($[11] !== requiresExtensionSuffix) {
- t6 = {
- label: <>{t5}{requiresExtensionSuffix}>,
- value: "manage-permissions"
- };
- $[11] = requiresExtensionSuffix;
- $[12] = t6;
- } else {
- t6 = $[12];
- }
- let t7;
- if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
- t7 = Reconnect extension;
- $[13] = t7;
- } else {
- t7 = $[13];
- }
- let t8;
- if ($[14] !== requiresExtensionSuffix) {
- t8 = {
- label: <>{t7}{requiresExtensionSuffix}>,
- value: "reconnect"
- };
- $[14] = requiresExtensionSuffix;
- $[15] = t8;
- } else {
- t8 = $[15];
- }
- const t9 = `Enabled by default: ${enabledByDefault ? "Yes" : "No"}`;
- let t10;
- if ($[16] !== t9) {
- t10 = {
- label: t9,
- value: "toggle-default"
- };
- $[16] = t9;
- $[17] = t10;
- } else {
- t10 = $[17];
- }
- options.push(t6, t8, t10);
- $[6] = enabledByDefault;
- $[7] = isExtensionInstalled;
- $[8] = options;
- } else {
- options = $[8];
- }
- const isDisabled = isWSL || true && !isClaudeAISubscriber;
- let t5;
- if ($[18] !== onDone) {
- t5 = () => onDone();
- $[18] = onDone;
- $[19] = t5;
- } else {
- t5 = $[19];
- }
- let t6;
- if ($[20] === Symbol.for("react.memo_cache_sentinel")) {
- t6 = Claude in Chrome works with the Chrome extension to let you control your browser directly from Verboo Code. Navigate websites, fill forms, capture screenshots, record GIFs, and debug with console logs and network requests.;
- $[20] = t6;
- } else {
- t6 = $[20];
- }
- let t7;
- if ($[21] !== isWSL) {
- t7 = isWSL && Claude in Chrome is not supported in WSL at this time.;
- $[21] = isWSL;
- $[22] = t7;
- } else {
- t7 = $[22];
- }
- let t8;
- if ($[23] !== isClaudeAISubscriber) {
- t8 = true && !isClaudeAISubscriber && Claude in Chrome requires a claude.ai subscription.;
- $[23] = isClaudeAISubscriber;
- $[24] = t8;
- } else {
- t8 = $[24];
- }
- let t9;
- if ($[25] !== handleAction || $[26] !== isConnected || $[27] !== isDisabled || $[28] !== isExtensionInstalled || $[29] !== options || $[30] !== selectKey || $[31] !== showInstallHint) {
- t9 = !isDisabled && <>{!isHomespace && Status:{" "}{isConnected ? Enabled : Disabled}Extension:{" "}{isExtensionInstalled ? Installed : Not detected}}{showInstallHint && Once installed, select {"\"Reconnect extension\""} to connect.}Usage: claude --chrome or claude --no-chromeSite-level permissions are inherited from the Chrome extension. Manage permissions in the Chrome extension settings to control which sites Claude can browse, click, and type on.>;
- $[25] = handleAction;
- $[26] = isConnected;
- $[27] = isDisabled;
- $[28] = isExtensionInstalled;
- $[29] = options;
- $[30] = selectKey;
- $[31] = showInstallHint;
- $[32] = t9;
- } else {
- t9 = $[32];
- }
- let t10;
- if ($[33] === Symbol.for("react.memo_cache_sentinel")) {
- t10 = Learn more: https://code.claude.com/docs/en/chrome;
- $[33] = t10;
- } else {
- t10 = $[33];
- }
- let t11;
- if ($[34] !== t7 || $[35] !== t8 || $[36] !== t9) {
- t11 = {t6}{t7}{t8}{t9}{t10};
- $[34] = t7;
- $[35] = t8;
- $[36] = t9;
- $[37] = t11;
- } else {
- t11 = $[37];
- }
- let t12;
- if ($[38] !== t11 || $[39] !== t5) {
- t12 = ;
- $[38] = t11;
- $[39] = t5;
- $[40] = t12;
- } else {
- t12 = $[40];
- }
- return t12;
+ onDone: (result?: string) => void
}
-function _temp5(k) {
- return k + 1;
+
+function VerbooInChromeMenu({ onDone }: Props): React.ReactNode {
+ const mcpClients = useAppState(state => state.mcp.clients)
+ const chromeClient = mcpClients.find(client =>
+ isVerbooInChromeMcpServer(client.name),
+ )
+ const configured = isManagedVerbooInChromeConfig(
+ getGlobalConfig().mcpServers?.[VERBOO_IN_CHROME_MCP_SERVER_NAME],
+ )
+ const connected = chromeClient?.type === 'connected'
+
+ return (
+
+ )
}
-function _temp4(k_0) {
- return k_0 + 1;
+
+export const call = async function (
+ onDone: (result?: string) => void,
+): Promise {
+ return
}
-function _temp3(k_1) {
- return k_1 + 1;
-}
-function _temp2(c) {
- return c.name === CLAUDE_IN_CHROME_MCP_SERVER_NAME;
-}
-function _temp(s) {
- return s.mcp.clients;
-}
-export const call = async function (onDone: (result?: string) => void): Promise {
- const isExtensionInstalled = await isChromeExtensionInstalled();
- const config = getGlobalConfig();
- const isSubscriber = isClaudeAISubscriber();
- const isWSL = env.isWslEnvironment();
- return ;
-};
diff --git a/src/commands/chrome/index.test.ts b/src/commands/chrome/index.test.ts
new file mode 100644
index 0000000000..8c2788903d
--- /dev/null
+++ b/src/commands/chrome/index.test.ts
@@ -0,0 +1,9 @@
+import assert from 'node:assert/strict'
+import test from 'node:test'
+
+import command from './index.js'
+
+test('/chrome is available to Verboo users without Anthropic account gating', () => {
+ assert.equal(command.name, 'chrome')
+ assert.equal(command.availability, undefined)
+})
diff --git a/src/commands/chrome/index.ts b/src/commands/chrome/index.ts
index b5d55e05fa..39e8d93061 100644
--- a/src/commands/chrome/index.ts
+++ b/src/commands/chrome/index.ts
@@ -4,7 +4,6 @@ import type { Command } from '../../commands.js'
const command: Command = {
name: 'chrome',
description: 'Verboo in Chrome (Beta) settings',
- availability: ['claude-ai'],
isEnabled: () => !getIsNonInteractiveSession(),
type: 'local-jsx',
load: () => import('./chrome.js'),
diff --git a/src/hooks/useChromeExtensionNotification.tsx b/src/hooks/useChromeExtensionNotification.tsx
index eeaef6fb3e..76a280ac6d 100644
--- a/src/hooks/useChromeExtensionNotification.tsx
+++ b/src/hooks/useChromeExtensionNotification.tsx
@@ -1,49 +1,42 @@
-import * as React from 'react';
-import { Text } from '../ink.js';
-import { isClaudeAISubscriber } from '../utils/auth.js';
-import { isChromeExtensionInstalled, shouldEnableClaudeInChrome } from '../utils/claudeInChrome/setup.js';
-import { isRunningOnHomespace } from '../utils/envUtils.js';
-import { useStartupNotification } from './notifs/useStartupNotification.js';
-function getChromeFlag(): boolean | undefined {
- if (process.argv.includes('--chrome')) {
- return true;
- }
- if (process.argv.includes('--no-chrome')) {
- return false;
- }
- return undefined;
+import * as React from 'react'
+import { Text } from '../ink.js'
+import { getGlobalConfig } from '../utils/config.js'
+import {
+ VERBOO_IN_CHROME_MCP_SERVER_NAME,
+ isManagedVerbooInChromeConfig,
+ isVerbooInChromeDisabledByCli,
+} from '../utils/verbooInChrome.js'
+import { useStartupNotification } from './notifs/useStartupNotification.js'
+
+function wasExplicitlyRequested(): boolean {
+ return process.argv.includes('--chrome')
}
-export function useChromeExtensionNotification() {
- useStartupNotification(_temp);
-}
-async function _temp() {
- const chromeFlag = getChromeFlag();
- if (!shouldEnableClaudeInChrome(chromeFlag)) {
- return null;
- }
- if (true && !isClaudeAISubscriber()) {
- return {
- key: "chrome-requires-subscription",
- jsx: Verboo in Chrome requires a claude.ai subscription,
- priority: "immediate",
- timeoutMs: 5000
- };
- }
- const installed = await isChromeExtensionInstalled();
- if (!installed && !isRunningOnHomespace()) {
- return {
- key: "chrome-extension-not-detected",
- jsx: Chrome extension not detected · https://claude.ai/chrome to install,
- priority: "immediate",
- timeoutMs: 3000
- };
- }
- if (chromeFlag === undefined) {
+
+export function useChromeExtensionNotification(): void {
+ useStartupNotification(async () => {
+ if (isVerbooInChromeDisabledByCli()) return null
+
+ const configured = isManagedVerbooInChromeConfig(
+ getGlobalConfig().mcpServers?.[VERBOO_IN_CHROME_MCP_SERVER_NAME],
+ )
+ if (!configured) {
+ if (!wasExplicitlyRequested()) return null
+ return {
+ key: 'verboo-in-chrome-not-configured',
+ jsx: (
+
+ Verboo in Chrome is not configured in the desktop app
+
+ ),
+ priority: 'immediate',
+ timeoutMs: 5000,
+ }
+ }
+
return {
- key: "claude-in-chrome-default-enabled",
- text: "Verboo in Chrome enabled \xB7 /chrome",
- priority: "low"
- };
- }
- return null;
+ key: 'verboo-in-chrome-enabled',
+ text: 'Verboo in Chrome enabled · /chrome',
+ priority: 'low',
+ }
+ })
}
diff --git a/src/main.tsx b/src/main.tsx
index 173831fed4..dcf6789a93 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -111,6 +111,7 @@ import type { Message as MessageType } from './types/message.js';
import { assertMinVersion } from './utils/autoUpdater.js';
import { CLAUDE_IN_CHROME_SKILL_HINT, CLAUDE_IN_CHROME_SKILL_HINT_WITH_WEBBROWSER } from './utils/claudeInChrome/prompt.js';
import { setupClaudeInChrome, shouldAutoEnableClaudeInChrome, shouldEnableClaudeInChrome } from './utils/claudeInChrome/setup.js';
+import { getVerbooInChromePrompt, resolveVerbooInChromeMcpConfigs } from './utils/verbooInChrome.js';
import { getContextWindowForModel } from './utils/context.js';
import { loadConversationForResume } from './utils/conversationRecovery.js';
import { buildDeepLinkBanner } from './utils/deepLink/banner.js';
@@ -1597,14 +1598,17 @@ async function run(): Promise {
}
}
- // Extract Claude in Chrome option and enforce claude.ai subscriber check (unless user is ant)
+ // The Verboo build uses the desktop-managed `verboo-in-chrome` MCP loaded
+ // with the other user configs below. Keep the inherited Claude transport
+ // available only outside Verboo mode so --chrome never installs or starts
+ // the unrelated Anthropic native host.
const chromeOpts = options as {
chrome?: boolean;
};
// Store the explicit CLI flag so teammates can inherit it
setChromeFlagOverride(chromeOpts.chrome);
- const enableClaudeInChrome = shouldEnableClaudeInChrome(chromeOpts.chrome) && ("external" === 'ant' || isClaudeAISubscriber());
- const autoEnableClaudeInChrome = !enableClaudeInChrome && shouldAutoEnableClaudeInChrome();
+ const enableClaudeInChrome = !isVerbooMode() && shouldEnableClaudeInChrome(chromeOpts.chrome) && ("external" === 'ant' || isClaudeAISubscriber());
+ const autoEnableClaudeInChrome = !isVerbooMode() && !enableClaudeInChrome && shouldAutoEnableClaudeInChrome();
if (enableClaudeInChrome) {
const platform = getPlatform();
try {
@@ -2455,10 +2459,20 @@ async function run(): Promise {
servers: existingMcpConfigs
} = await mcpConfigPromise;
// CLI flag (--mcp-config) should override file-based configs, matching settings precedence
- const allMcpConfigs = {
+ const mergedMcpConfigs = {
...existingMcpConfigs,
...dynamicMcpConfig
};
+ const chromeResolution = resolveVerbooInChromeMcpConfigs(mergedMcpConfigs, chromeOpts.chrome);
+ if (chromeResolution.requestedButMissing) {
+ process.stderr.write('Error: Verboo in Chrome is not configured. Open Verboo desktop > Settings > Integrations > Chrome, then install or repair the integration.\n');
+ process.exit(1);
+ }
+ if (chromeResolution.enabled) {
+ const chromeSystemPrompt = getVerbooInChromePrompt();
+ appendSystemPrompt = appendSystemPrompt ? `${appendSystemPrompt}\n\n${chromeSystemPrompt}` : chromeSystemPrompt;
+ }
+ const allMcpConfigs = chromeResolution.configs;
// Separate SDK configs from regular MCP configs
const sdkMcpConfigs: Record = {};
diff --git a/src/skills/bundled/index.ts b/src/skills/bundled/index.ts
index 231b3b1ae0..617537ef17 100644
--- a/src/skills/bundled/index.ts
+++ b/src/skills/bundled/index.ts
@@ -8,6 +8,7 @@ import { registerKeybindingsSkill } from './keybindings.js'
import { registerLoopSkill } from './loop.js'
import { registerSimplifySkill } from './simplify.js'
import { registerUpdateConfigSkill } from './updateConfig.js'
+import { registerVerbooInChromeSkill } from './verbooInChrome.js'
/**
* Initialize all bundled skills.
@@ -25,6 +26,7 @@ export function initBundledSkills(): void {
registerSimplifySkill()
registerKarpathyGuidelinesSkill()
registerBatchSkill()
+ registerVerbooInChromeSkill()
if (feature('KAIROS') || feature('KAIROS_DREAM')) {
/* eslint-disable @typescript-eslint/no-require-imports */
const { registerDreamSkill } = require('./dream.js')
diff --git a/src/skills/bundled/verbooInChrome.test.ts b/src/skills/bundled/verbooInChrome.test.ts
new file mode 100644
index 0000000000..f054933849
--- /dev/null
+++ b/src/skills/bundled/verbooInChrome.test.ts
@@ -0,0 +1,36 @@
+import { afterEach, expect, test } from 'bun:test'
+
+import { clearBundledSkills, getBundledSkills } from '../bundledSkills.js'
+import { registerVerbooInChromeSkill } from './verbooInChrome.js'
+
+afterEach(() => {
+ clearBundledSkills()
+})
+
+test('Verboo in Chrome skill exposes the real MCP tool contract', async () => {
+ registerVerbooInChromeSkill()
+
+ const skill = getBundledSkills().find(
+ command => command.name === 'verboo-in-chrome',
+ )
+ expect(skill).toBeDefined()
+ expect(skill?.allowedTools).toEqual([
+ 'mcp__verboo-in-chrome__navigate',
+ 'mcp__verboo-in-chrome__read_page',
+ 'mcp__verboo-in-chrome__structured_extract',
+ 'mcp__verboo-in-chrome__click',
+ 'mcp__verboo-in-chrome__type',
+ 'mcp__verboo-in-chrome__screenshot',
+ 'mcp__verboo-in-chrome__tabs',
+ 'mcp__verboo-in-chrome__tab_group',
+ ])
+
+ const blocks = await skill!.getPromptForCommand(
+ 'check the deployment status',
+ {} as never,
+ )
+ const text = (blocks[0] as { text: string }).text
+ expect(text).toContain('check the deployment status')
+ expect(text).toContain('new background tab')
+ expect(text).not.toContain('mcp__claude-in-chrome__')
+})
diff --git a/src/skills/bundled/verbooInChrome.ts b/src/skills/bundled/verbooInChrome.ts
new file mode 100644
index 0000000000..5d8728307f
--- /dev/null
+++ b/src/skills/bundled/verbooInChrome.ts
@@ -0,0 +1,37 @@
+import { getGlobalConfig } from '../../utils/config.js'
+import {
+ VERBOO_IN_CHROME_MCP_SERVER_NAME,
+ VERBOO_IN_CHROME_TOOL_NAMES,
+ getVerbooInChromePrompt,
+ isManagedVerbooInChromeConfig,
+ isVerbooInChromeDisabledByCli,
+} from '../../utils/verbooInChrome.js'
+import { registerBundledSkill } from '../bundledSkills.js'
+
+const VERBOO_IN_CHROME_MCP_TOOLS = VERBOO_IN_CHROME_TOOL_NAMES.map(
+ tool => `mcp__verboo-in-chrome__${tool}`,
+)
+
+function isConfigured(): boolean {
+ if (isVerbooInChromeDisabledByCli()) return false
+ return isManagedVerbooInChromeConfig(
+ getGlobalConfig().mcpServers?.[VERBOO_IN_CHROME_MCP_SERVER_NAME],
+ )
+}
+
+export function registerVerbooInChromeSkill(): void {
+ registerBundledSkill({
+ name: 'verboo-in-chrome',
+ description:
+ 'Controls the user-approved Verboo Chrome extension to navigate pages, read content, click, type, capture screenshots, and manage tabs.',
+ whenToUse:
+ 'Use when the user asks you to browse or interact with a website through their Chrome session. Invoke this skill before any mcp__verboo-in-chrome__* tool.',
+ allowedTools: VERBOO_IN_CHROME_MCP_TOOLS,
+ userInvocable: true,
+ isEnabled: isConfigured,
+ async getPromptForCommand(args) {
+ const task = args ? `\n\n## Task\n\n${args}` : ''
+ return [{ type: 'text', text: `${getVerbooInChromePrompt()}${task}` }]
+ },
+ })
+}
diff --git a/src/utils/verbooInChrome.test.ts b/src/utils/verbooInChrome.test.ts
new file mode 100644
index 0000000000..37c9a7045f
--- /dev/null
+++ b/src/utils/verbooInChrome.test.ts
@@ -0,0 +1,125 @@
+import assert from 'node:assert/strict'
+import test from 'node:test'
+
+import type { ScopedMcpServerConfig } from '../services/mcp/types.js'
+import {
+ VERBOO_IN_CHROME_MCP_SERVER_NAME,
+ getVerbooInChromePrompt,
+ isManagedVerbooInChromeConfig,
+ isVerbooInChromeDisabledByCli,
+ resolveVerbooInChromeMcpConfigs,
+} from './verbooInChrome.js'
+
+const managedChromeConfig: ScopedMcpServerConfig = {
+ type: 'stdio',
+ command: '/managed/verboo-in-chrome',
+ args: ['mcp'],
+ env: {
+ VERBOO_IN_CHROME_MANAGED: '1',
+ VERBOO_IN_CHROME_VERSION: '0.7.0-beta',
+ },
+ scope: 'user',
+}
+
+test('recognizes only the desktop-managed Verboo in Chrome contract', () => {
+ assert.equal(isManagedVerbooInChromeConfig(managedChromeConfig), true)
+ assert.equal(
+ isManagedVerbooInChromeConfig({
+ ...managedChromeConfig,
+ env: { VERBOO_IN_CHROME_MANAGED: '0' },
+ }),
+ false,
+ )
+ assert.equal(
+ isManagedVerbooInChromeConfig({
+ type: 'http',
+ url: 'https://example.test/mcp',
+ scope: 'user',
+ }),
+ false,
+ )
+})
+
+test('auto-enables the managed server without rewriting its cross-platform command', () => {
+ const otherConfig: ScopedMcpServerConfig = {
+ type: 'stdio',
+ command: 'other-mcp',
+ args: [],
+ scope: 'user',
+ }
+ const configs = {
+ other: otherConfig,
+ [VERBOO_IN_CHROME_MCP_SERVER_NAME]: managedChromeConfig,
+ }
+
+ const resolved = resolveVerbooInChromeMcpConfigs(configs)
+
+ assert.equal(resolved.enabled, true)
+ assert.equal(resolved.requestedButMissing, false)
+ assert.equal(
+ resolved.configs[VERBOO_IN_CHROME_MCP_SERVER_NAME],
+ managedChromeConfig,
+ )
+ assert.equal(resolved.configs.other, otherConfig)
+})
+
+test('--no-chrome disables only Verboo in Chrome for the current process', () => {
+ const otherConfig: ScopedMcpServerConfig = {
+ type: 'stdio',
+ command: 'other-mcp',
+ args: [],
+ scope: 'user',
+ }
+ const resolved = resolveVerbooInChromeMcpConfigs(
+ {
+ other: otherConfig,
+ [VERBOO_IN_CHROME_MCP_SERVER_NAME]: managedChromeConfig,
+ },
+ false,
+ )
+
+ assert.equal(resolved.enabled, false)
+ assert.equal(resolved.requestedButMissing, false)
+ assert.equal(
+ VERBOO_IN_CHROME_MCP_SERVER_NAME in resolved.configs,
+ false,
+ )
+ assert.equal(resolved.configs.other, otherConfig)
+})
+
+test('--chrome fails closed when the desktop-managed MCP is unavailable', () => {
+ const resolved = resolveVerbooInChromeMcpConfigs({}, true)
+
+ assert.equal(resolved.enabled, false)
+ assert.equal(resolved.requestedButMissing, true)
+})
+
+test('the last per-session Chrome flag wins', () => {
+ assert.equal(
+ isVerbooInChromeDisabledByCli(['verboo', '--chrome', '--no-chrome']),
+ true,
+ )
+ assert.equal(
+ isVerbooInChromeDisabledByCli(['verboo', '--no-chrome', '--chrome']),
+ false,
+ )
+})
+
+test('browser guidance matches the tools exposed by the Verboo extension', () => {
+ const prompt = getVerbooInChromePrompt()
+
+ for (const tool of [
+ 'navigate',
+ 'read_page',
+ 'structured_extract',
+ 'click',
+ 'type',
+ 'screenshot',
+ 'tabs',
+ 'tab_group',
+ ]) {
+ assert.match(prompt, new RegExp(`mcp__verboo-in-chrome__${tool}`))
+ }
+ assert.match(prompt, /background/i)
+ assert.doesNotMatch(prompt, /mcp__claude-in-chrome__/)
+})
diff --git a/src/utils/verbooInChrome.ts b/src/utils/verbooInChrome.ts
new file mode 100644
index 0000000000..6021f7f638
--- /dev/null
+++ b/src/utils/verbooInChrome.ts
@@ -0,0 +1,109 @@
+import type { ScopedMcpServerConfig } from '../services/mcp/types.js'
+import { normalizeNameForMCP } from '../services/mcp/normalization.js'
+
+export const VERBOO_IN_CHROME_MCP_SERVER_NAME = 'verboo-in-chrome'
+export const VERBOO_IN_CHROME_MANAGED_MARKER = 'VERBOO_IN_CHROME_MANAGED'
+
+export const VERBOO_IN_CHROME_TOOL_NAMES = [
+ 'navigate',
+ 'read_page',
+ 'structured_extract',
+ 'click',
+ 'type',
+ 'screenshot',
+ 'tabs',
+ 'tab_group',
+] as const
+
+export function isVerbooInChromeMcpServer(name: string): boolean {
+ return normalizeNameForMCP(name) === VERBOO_IN_CHROME_MCP_SERVER_NAME
+}
+
+export function isManagedVerbooInChromeConfig(config: unknown): boolean {
+ if (!config || typeof config !== 'object') return false
+
+ const candidate = config as {
+ type?: string
+ command?: string
+ args?: unknown
+ env?: unknown
+ }
+ if (candidate.type !== undefined && candidate.type !== 'stdio') return false
+ if (typeof candidate.command !== 'string' || candidate.command.length === 0) {
+ return false
+ }
+ if (
+ !Array.isArray(candidate.args) ||
+ candidate.args.length !== 1 ||
+ candidate.args[0] !== 'mcp'
+ ) {
+ return false
+ }
+ if (!candidate.env || typeof candidate.env !== 'object') return false
+
+ return (
+ (candidate.env as Record)[
+ VERBOO_IN_CHROME_MANAGED_MARKER
+ ] === '1'
+ )
+}
+
+export function resolveVerbooInChromeMcpConfigs(
+ configs: Record,
+ chromeFlag?: boolean,
+): {
+ configs: Record
+ enabled: boolean
+ requestedButMissing: boolean
+} {
+ const chromeEntry = Object.entries(configs).find(([name]) =>
+ isVerbooInChromeMcpServer(name),
+ )
+ const managed = chromeEntry
+ ? isManagedVerbooInChromeConfig(chromeEntry[1])
+ : false
+
+ if (chromeFlag === false) {
+ return {
+ configs: Object.fromEntries(
+ Object.entries(configs).filter(
+ ([name]) => !isVerbooInChromeMcpServer(name),
+ ),
+ ),
+ enabled: false,
+ requestedButMissing: false,
+ }
+ }
+
+ return {
+ configs,
+ enabled: managed,
+ requestedButMissing: chromeFlag === true && !managed,
+ }
+}
+
+export function isVerbooInChromeDisabledByCli(
+ argv: readonly string[] = process.argv,
+): boolean {
+ const chromeFlags = argv.filter(
+ arg => arg === '--chrome' || arg === '--no-chrome',
+ )
+ return chromeFlags.at(-1) === '--no-chrome'
+}
+
+export function getVerbooInChromePrompt(): string {
+ const tools = VERBOO_IN_CHROME_TOOL_NAMES.map(
+ name => `- mcp__verboo-in-chrome__${name}`,
+ ).join('\n')
+
+ return `# Verboo in Chrome browser automation
+
+You have access to the locally managed Verboo in Chrome MCP tools. Browser actions still pass through the extension's site permissions and approval flow.
+
+Available tools:
+${tools}
+
+At the start of a browser task, call mcp__verboo-in-chrome__tabs with action \"list\" to get fresh tab IDs. Never reuse tab IDs from another task. Unless the user explicitly asks you to control an existing tab, open a new background tab with mcp__verboo-in-chrome__tabs and keep the user's foreground tab unchanged. Use mcp__verboo-in-chrome__navigate only after selecting the intended tab.
+
+Stay focused on the requested task. If the extension is unavailable or an action fails repeatedly, stop after 2-3 attempts, explain what failed, and ask the user how to proceed.`
+}