source dump of claude code
at main 293 lines 11 kB view raw
1import { 2 type ClaudeForChromeContext, 3 createClaudeForChromeMcpServer, 4 type Logger, 5 type PermissionMode, 6} from '@ant/claude-for-chrome-mcp' 7import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' 8import { format } from 'util' 9import { shutdownDatadog } from '../../services/analytics/datadog.js' 10import { shutdown1PEventLogging } from '../../services/analytics/firstPartyEventLogger.js' 11import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' 12import { 13 type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 14 logEvent, 15} from '../../services/analytics/index.js' 16import { initializeAnalyticsSink } from '../../services/analytics/sink.js' 17import { getClaudeAIOAuthTokens } from '../auth.js' 18import { enableConfigs, getGlobalConfig, saveGlobalConfig } from '../config.js' 19import { logForDebugging } from '../debug.js' 20import { isEnvTruthy } from '../envUtils.js' 21import { sideQuery } from '../sideQuery.js' 22import { getAllSocketPaths, getSecureSocketPath } from './common.js' 23 24const EXTENSION_DOWNLOAD_URL = 'https://claude.ai/chrome' 25const BUG_REPORT_URL = 26 'https://github.com/anthropics/claude-code/issues/new?labels=bug,claude-in-chrome' 27 28// String metadata keys safe to forward to analytics. Keys like error_message 29// are excluded because they could contain page content or user data. 30const SAFE_BRIDGE_STRING_KEYS = new Set([ 31 'bridge_status', 32 'error_type', 33 'tool_name', 34]) 35 36const PERMISSION_MODES: readonly PermissionMode[] = [ 37 'ask', 38 'skip_all_permission_checks', 39 'follow_a_plan', 40] 41 42function isPermissionMode(raw: string): raw is PermissionMode { 43 return PERMISSION_MODES.some(m => m === raw) 44} 45 46/** 47 * Resolves the Chrome bridge URL based on environment and feature flag. 48 * Bridge is used when the feature flag is enabled; ant users always get 49 * bridge. API key / 3P users fall back to native messaging. 50 */ 51function getChromeBridgeUrl(): string | undefined { 52 const bridgeEnabled = 53 process.env.USER_TYPE === 'ant' || 54 getFeatureValue_CACHED_MAY_BE_STALE('tengu_copper_bridge', false) 55 56 if (!bridgeEnabled) { 57 return undefined 58 } 59 60 if ( 61 isEnvTruthy(process.env.USE_LOCAL_OAUTH) || 62 isEnvTruthy(process.env.LOCAL_BRIDGE) 63 ) { 64 return 'ws://localhost:8765' 65 } 66 67 if (isEnvTruthy(process.env.USE_STAGING_OAUTH)) { 68 return 'wss://bridge-staging.claudeusercontent.com' 69 } 70 71 return 'wss://bridge.claudeusercontent.com' 72} 73 74function isLocalBridge(): boolean { 75 return ( 76 isEnvTruthy(process.env.USE_LOCAL_OAUTH) || 77 isEnvTruthy(process.env.LOCAL_BRIDGE) 78 ) 79} 80 81/** 82 * Build the ClaudeForChromeContext used by both the subprocess MCP server 83 * and the in-process path in the MCP client. 84 */ 85export function createChromeContext( 86 env?: Record<string, string>, 87): ClaudeForChromeContext { 88 const logger = new DebugLogger() 89 const chromeBridgeUrl = getChromeBridgeUrl() 90 logger.info(`Bridge URL: ${chromeBridgeUrl ?? 'none (using native socket)'}`) 91 const rawPermissionMode = 92 env?.CLAUDE_CHROME_PERMISSION_MODE ?? 93 process.env.CLAUDE_CHROME_PERMISSION_MODE 94 let initialPermissionMode: PermissionMode | undefined 95 if (rawPermissionMode) { 96 if (isPermissionMode(rawPermissionMode)) { 97 initialPermissionMode = rawPermissionMode 98 } else { 99 logger.warn( 100 `Invalid CLAUDE_CHROME_PERMISSION_MODE "${rawPermissionMode}". Valid values: ${PERMISSION_MODES.join(', ')}`, 101 ) 102 } 103 } 104 return { 105 serverName: 'Claude in Chrome', 106 logger, 107 socketPath: getSecureSocketPath(), 108 getSocketPaths: getAllSocketPaths, 109 clientTypeId: 'claude-code', 110 onAuthenticationError: () => { 111 logger.warn( 112 'Authentication error occurred. Please ensure you are logged into the Claude browser extension with the same claude.ai account as Claude Code.', 113 ) 114 }, 115 onToolCallDisconnected: () => { 116 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}` 117 }, 118 onExtensionPaired: (deviceId: string, name: string) => { 119 saveGlobalConfig(config => { 120 if ( 121 config.chromeExtension?.pairedDeviceId === deviceId && 122 config.chromeExtension?.pairedDeviceName === name 123 ) { 124 return config 125 } 126 return { 127 ...config, 128 chromeExtension: { 129 pairedDeviceId: deviceId, 130 pairedDeviceName: name, 131 }, 132 } 133 }) 134 logger.info(`Paired with "${name}" (${deviceId.slice(0, 8)})`) 135 }, 136 getPersistedDeviceId: () => { 137 return getGlobalConfig().chromeExtension?.pairedDeviceId 138 }, 139 ...(chromeBridgeUrl && { 140 bridgeConfig: { 141 url: chromeBridgeUrl, 142 getUserId: async () => { 143 return getGlobalConfig().oauthAccount?.accountUuid 144 }, 145 getOAuthToken: async () => { 146 return getClaudeAIOAuthTokens()?.accessToken ?? '' 147 }, 148 ...(isLocalBridge() && { devUserId: 'dev_user_local' }), 149 }, 150 }), 151 ...(initialPermissionMode && { initialPermissionMode }), 152 // Wire inference for the browser_task tool — the chrome-mcp server runs 153 // a lightning-mode agent loop in Node and calls the extension's 154 // lightning_turn tool once per iteration for execution. 155 // 156 // Ant-only: the extension's lightning_turn is build-time-gated via 157 // import.meta.env.ANT_ONLY_BUILD — the whole lightning/ module graph is 158 // tree-shaken from the public extension build (build:prod greps for a 159 // marker to verify). Without this injection, the Node MCP server's 160 // ListTools also filters browser_task + lightning_turn out, so external 161 // users never see the tools advertised. Three independent gates. 162 // 163 // Types inlined: AnthropicMessagesRequest/Response live in 164 // @ant/claude-for-chrome-mcp@0.4.0 which isn't published yet. CI installs 165 // 0.3.0. The callAnthropicMessages field is also 0.4.0-only, but spreading 166 // an extra property into ClaudeForChromeContext is fine against either 167 // version — 0.3.0 sees an unknown field (allowed in spread), 0.4.0 sees a 168 // structurally-matching one. Once 0.4.0 is published, this can switch to 169 // the package's exported types and the dep can be bumped. 170 ...(process.env.USER_TYPE === 'ant' && { 171 callAnthropicMessages: async (req: { 172 model: string 173 max_tokens: number 174 system: string 175 messages: Parameters<typeof sideQuery>[0]['messages'] 176 stop_sequences?: string[] 177 signal?: AbortSignal 178 }): Promise<{ 179 content: Array<{ type: 'text'; text: string }> 180 stop_reason: string | null 181 usage?: { input_tokens: number; output_tokens: number } 182 }> => { 183 // sideQuery handles OAuth attribution fingerprint, proxy, model betas. 184 // skipSystemPromptPrefix: the lightning prompt is complete on its own; 185 // the CLI prefix would dilute the batching instructions. 186 // tools: [] is load-bearing — without it Sonnet emits 187 // <function_calls> XML before the text commands. Original 188 // lightning-harness.js (apps repo) does the same. 189 const response = await sideQuery({ 190 model: req.model, 191 system: req.system, 192 messages: req.messages, 193 max_tokens: req.max_tokens, 194 stop_sequences: req.stop_sequences, 195 signal: req.signal, 196 skipSystemPromptPrefix: true, 197 tools: [], 198 querySource: 'chrome_mcp', 199 }) 200 // BetaContentBlock is TextBlock | ThinkingBlock | ToolUseBlock | ... 201 // Only text blocks carry the model's command output. 202 const textBlocks: Array<{ type: 'text'; text: string }> = [] 203 for (const b of response.content) { 204 if (b.type === 'text') { 205 textBlocks.push({ type: 'text', text: b.text }) 206 } 207 } 208 return { 209 content: textBlocks, 210 stop_reason: response.stop_reason, 211 usage: { 212 input_tokens: response.usage.input_tokens, 213 output_tokens: response.usage.output_tokens, 214 }, 215 } 216 }, 217 }), 218 trackEvent: (eventName, metadata) => { 219 const safeMetadata: { 220 [key: string]: 221 | boolean 222 | number 223 | AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 224 | undefined 225 } = {} 226 if (metadata) { 227 for (const [key, value] of Object.entries(metadata)) { 228 // Rename 'status' to 'bridge_status' to avoid Datadog's reserved field 229 const safeKey = key === 'status' ? 'bridge_status' : key 230 if (typeof value === 'boolean' || typeof value === 'number') { 231 safeMetadata[safeKey] = value 232 } else if ( 233 typeof value === 'string' && 234 SAFE_BRIDGE_STRING_KEYS.has(safeKey) 235 ) { 236 // Only forward allowlisted string keys — fields like error_message 237 // could contain page content or user data 238 safeMetadata[safeKey] = 239 value as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 240 } 241 } 242 } 243 logEvent(eventName, safeMetadata) 244 }, 245 } 246} 247 248export async function runClaudeInChromeMcpServer(): Promise<void> { 249 enableConfigs() 250 initializeAnalyticsSink() 251 const context = createChromeContext() 252 253 const server = createClaudeForChromeMcpServer(context) 254 const transport = new StdioServerTransport() 255 256 // Exit when parent process dies (stdin pipe closes). 257 // Flush analytics before exiting so final-batch events (e.g. disconnect) aren't lost. 258 let exiting = false 259 const shutdownAndExit = async (): Promise<void> => { 260 if (exiting) { 261 return 262 } 263 exiting = true 264 await shutdown1PEventLogging() 265 await shutdownDatadog() 266 // eslint-disable-next-line custom-rules/no-process-exit 267 process.exit(0) 268 } 269 process.stdin.on('end', () => void shutdownAndExit()) 270 process.stdin.on('error', () => void shutdownAndExit()) 271 272 logForDebugging('[Claude in Chrome] Starting MCP server') 273 await server.connect(transport) 274 logForDebugging('[Claude in Chrome] MCP server started') 275} 276 277class DebugLogger implements Logger { 278 silly(message: string, ...args: unknown[]): void { 279 logForDebugging(format(message, ...args), { level: 'debug' }) 280 } 281 debug(message: string, ...args: unknown[]): void { 282 logForDebugging(format(message, ...args), { level: 'debug' }) 283 } 284 info(message: string, ...args: unknown[]): void { 285 logForDebugging(format(message, ...args), { level: 'info' }) 286 } 287 warn(message: string, ...args: unknown[]): void { 288 logForDebugging(format(message, ...args), { level: 'warn' }) 289 } 290 error(message: string, ...args: unknown[]): void { 291 logForDebugging(format(message, ...args), { level: 'error' }) 292 } 293}