| import { |
| type ClaudeForChromeContext, |
| createClaudeForChromeMcpServer, |
| type Logger, |
| type PermissionMode, |
| } from '@ant/claude-for-chrome-mcp' |
| import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' |
| import { format } from 'util' |
| import { shutdownDatadog } from '../../services/analytics/datadog.js' |
| import { shutdown1PEventLogging } from '../../services/analytics/firstPartyEventLogger.js' |
| import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' |
| import { |
| type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| logEvent, |
| } from '../../services/analytics/index.js' |
| import { initializeAnalyticsSink } from '../../services/analytics/sink.js' |
| import { getClaudeAIOAuthTokens } from '../auth.js' |
| import { enableConfigs, getGlobalConfig, saveGlobalConfig } from '../config.js' |
| import { logForDebugging } from '../debug.js' |
| import { isEnvTruthy } from '../envUtils.js' |
| import { sideQuery } from '../sideQuery.js' |
| import { getAllSocketPaths, getSecureSocketPath } from './common.js' |
|
|
| const EXTENSION_DOWNLOAD_URL = 'https://claude.ai/chrome' |
| const BUG_REPORT_URL = |
| 'https://github.com/anthropics/claude-code/issues/new?labels=bug,claude-in-chrome' |
|
|
| |
| |
| const SAFE_BRIDGE_STRING_KEYS = new Set([ |
| 'bridge_status', |
| 'error_type', |
| 'tool_name', |
| ]) |
|
|
| const PERMISSION_MODES: readonly PermissionMode[] = [ |
| 'ask', |
| 'skip_all_permission_checks', |
| 'follow_a_plan', |
| ] |
|
|
| function isPermissionMode(raw: string): raw is PermissionMode { |
| return PERMISSION_MODES.some(m => m === raw) |
| } |
|
|
| |
| |
| |
| |
| |
| function getChromeBridgeUrl(): string | undefined { |
| const bridgeEnabled = |
| process.env.USER_TYPE === 'ant' || |
| getFeatureValue_CACHED_MAY_BE_STALE('tengu_copper_bridge', false) |
|
|
| if (!bridgeEnabled) { |
| return undefined |
| } |
|
|
| if ( |
| isEnvTruthy(process.env.USE_LOCAL_OAUTH) || |
| isEnvTruthy(process.env.LOCAL_BRIDGE) |
| ) { |
| return 'ws://localhost:8765' |
| } |
|
|
| if (isEnvTruthy(process.env.USE_STAGING_OAUTH)) { |
| return 'wss://bridge-staging.claudeusercontent.com' |
| } |
|
|
| return 'wss://bridge.claudeusercontent.com' |
| } |
|
|
| function isLocalBridge(): boolean { |
| return ( |
| isEnvTruthy(process.env.USE_LOCAL_OAUTH) || |
| isEnvTruthy(process.env.LOCAL_BRIDGE) |
| ) |
| } |
|
|
| |
| |
| |
| |
| export function createChromeContext( |
| env?: Record<string, string>, |
| ): ClaudeForChromeContext { |
| const logger = new DebugLogger() |
| const chromeBridgeUrl = getChromeBridgeUrl() |
| logger.info(`Bridge URL: ${chromeBridgeUrl ?? 'none (using native socket)'}`) |
| const rawPermissionMode = |
| env?.CLAUDE_CHROME_PERMISSION_MODE ?? |
| process.env.CLAUDE_CHROME_PERMISSION_MODE |
| let initialPermissionMode: PermissionMode | undefined |
| if (rawPermissionMode) { |
| if (isPermissionMode(rawPermissionMode)) { |
| initialPermissionMode = rawPermissionMode |
| } else { |
| logger.warn( |
| `Invalid CLAUDE_CHROME_PERMISSION_MODE "${rawPermissionMode}". Valid values: ${PERMISSION_MODES.join(', ')}`, |
| ) |
| } |
| } |
| return { |
| serverName: 'Claude in Chrome', |
| logger, |
| socketPath: getSecureSocketPath(), |
| getSocketPaths: getAllSocketPaths, |
| clientTypeId: 'claude-code', |
| onAuthenticationError: () => { |
| logger.warn( |
| 'Authentication error occurred. Please ensure you are logged into the Claude browser extension with the same claude.ai account as Claude Code.', |
| ) |
| }, |
| onToolCallDisconnected: () => { |
| return `Browser extension is not connected. Please ensure the Claude browser extension is installed and running (${EXTENSION_DOWNLOAD_URL}), and that you are logged into claude.ai with the same account as Claude Code. If this is your first time connecting to Chrome, you may need to restart Chrome for the installation to take effect. If you continue to experience issues, please report a bug: ${BUG_REPORT_URL}` |
| }, |
| onExtensionPaired: (deviceId: string, name: string) => { |
| saveGlobalConfig(config => { |
| if ( |
| config.chromeExtension?.pairedDeviceId === deviceId && |
| config.chromeExtension?.pairedDeviceName === name |
| ) { |
| return config |
| } |
| return { |
| ...config, |
| chromeExtension: { |
| pairedDeviceId: deviceId, |
| pairedDeviceName: name, |
| }, |
| } |
| }) |
| logger.info(`Paired with "${name}" (${deviceId.slice(0, 8)})`) |
| }, |
| getPersistedDeviceId: () => { |
| return getGlobalConfig().chromeExtension?.pairedDeviceId |
| }, |
| ...(chromeBridgeUrl && { |
| bridgeConfig: { |
| url: chromeBridgeUrl, |
| getUserId: async () => { |
| return getGlobalConfig().oauthAccount?.accountUuid |
| }, |
| getOAuthToken: async () => { |
| return getClaudeAIOAuthTokens()?.accessToken ?? '' |
| }, |
| ...(isLocalBridge() && { devUserId: 'dev_user_local' }), |
| }, |
| }), |
| ...(initialPermissionMode && { initialPermissionMode }), |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| ...(process.env.USER_TYPE === 'ant' && { |
| callAnthropicMessages: async (req: { |
| model: string |
| max_tokens: number |
| system: string |
| messages: Parameters<typeof sideQuery>[0]['messages'] |
| stop_sequences?: string[] |
| signal?: AbortSignal |
| }): Promise<{ |
| content: Array<{ type: 'text'; text: string }> |
| stop_reason: string | null |
| usage?: { input_tokens: number; output_tokens: number } |
| }> => { |
| |
| |
| |
| |
| |
| |
| const response = await sideQuery({ |
| model: req.model, |
| system: req.system, |
| messages: req.messages, |
| max_tokens: req.max_tokens, |
| stop_sequences: req.stop_sequences, |
| signal: req.signal, |
| skipSystemPromptPrefix: true, |
| tools: [], |
| querySource: 'chrome_mcp', |
| }) |
| |
| |
| const textBlocks: Array<{ type: 'text'; text: string }> = [] |
| for (const b of response.content) { |
| if (b.type === 'text') { |
| textBlocks.push({ type: 'text', text: b.text }) |
| } |
| } |
| return { |
| content: textBlocks, |
| stop_reason: response.stop_reason, |
| usage: { |
| input_tokens: response.usage.input_tokens, |
| output_tokens: response.usage.output_tokens, |
| }, |
| } |
| }, |
| }), |
| trackEvent: (eventName, metadata) => { |
| const safeMetadata: { |
| [key: string]: |
| | boolean |
| | number |
| | AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS |
| | undefined |
| } = {} |
| if (metadata) { |
| for (const [key, value] of Object.entries(metadata)) { |
| |
| const safeKey = key === 'status' ? 'bridge_status' : key |
| if (typeof value === 'boolean' || typeof value === 'number') { |
| safeMetadata[safeKey] = value |
| } else if ( |
| typeof value === 'string' && |
| SAFE_BRIDGE_STRING_KEYS.has(safeKey) |
| ) { |
| |
| |
| safeMetadata[safeKey] = |
| value as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS |
| } |
| } |
| } |
| logEvent(eventName, safeMetadata) |
| }, |
| } |
| } |
|
|
| export async function runClaudeInChromeMcpServer(): Promise<void> { |
| enableConfigs() |
| initializeAnalyticsSink() |
| const context = createChromeContext() |
|
|
| const server = createClaudeForChromeMcpServer(context) |
| const transport = new StdioServerTransport() |
|
|
| |
| |
| let exiting = false |
| const shutdownAndExit = async (): Promise<void> => { |
| if (exiting) { |
| return |
| } |
| exiting = true |
| await shutdown1PEventLogging() |
| await shutdownDatadog() |
| |
| process.exit(0) |
| } |
| process.stdin.on('end', () => void shutdownAndExit()) |
| process.stdin.on('error', () => void shutdownAndExit()) |
|
|
| logForDebugging('[Claude in Chrome] Starting MCP server') |
| await server.connect(transport) |
| logForDebugging('[Claude in Chrome] MCP server started') |
| } |
|
|
| class DebugLogger implements Logger { |
| silly(message: string, ...args: unknown[]): void { |
| logForDebugging(format(message, ...args), { level: 'debug' }) |
| } |
| debug(message: string, ...args: unknown[]): void { |
| logForDebugging(format(message, ...args), { level: 'debug' }) |
| } |
| info(message: string, ...args: unknown[]): void { |
| logForDebugging(format(message, ...args), { level: 'info' }) |
| } |
| warn(message: string, ...args: unknown[]): void { |
| logForDebugging(format(message, ...args), { level: 'warn' }) |
| } |
| error(message: string, ...args: unknown[]): void { |
| logForDebugging(format(message, ...args), { level: 'error' }) |
| } |
| } |
|
|