1+ import { spawn } from 'node:child_process' ;
2+ import { copyFileSync , cpSync , existsSync , mkdirSync , rmSync , writeFileSync } from 'node:fs' ;
3+ import { homedir , platform , tmpdir } from 'node:os' ;
4+ import { join } from 'node:path' ;
5+ import type { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp' ;
6+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types' ;
7+ import type { ToolDefinition } from '../types/tool' ;
8+ import { z } from 'zod' ;
9+
10+ const USER_DATA_DIR = join ( tmpdir ( ) , 'chrome-debug' ) ;
11+
12+ export const launchChromeToolDefinition : ToolDefinition = {
13+ name : 'launch_chrome' ,
14+ description : `Prepares and launches Chrome with remote debugging enabled so attach_browser() can connect.
15+
16+ Two modes:
17+
18+ newInstance (default): Opens a Chrome window alongside your existing one using a separate
19+ profile dir. Your current Chrome session is untouched.
20+
21+ freshSession: Launches Chrome with an empty profile (no cookies, no logins).
22+
23+ Use copyProfileFiles: true to carry over your cookies and logins into the debug session.
24+ Note: changes made during the session won't sync back to your main profile.
25+
26+ After this tool succeeds, call attach_browser() to connect.` ,
27+ inputSchema : {
28+ port : z . number ( ) . default ( 9222 ) . describe ( 'Remote debugging port (default: 9222)' ) ,
29+ mode : z . enum ( [ 'newInstance' , 'freshSession' ] ) . default ( 'newInstance' ) . describe (
30+ 'newInstance: open alongside existing Chrome | freshSession: clean profile'
31+ ) ,
32+ copyProfileFiles : z . boolean ( ) . default ( false ) . describe (
33+ 'Copy your Default Chrome profile (cookies, logins) into the debug session.'
34+ ) ,
35+ } ,
36+ } ;
37+
38+ function isMac ( ) : boolean {
39+ return platform ( ) === 'darwin' ;
40+ }
41+
42+ function chromeExec ( ) : string {
43+ if ( isMac ( ) ) return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' ;
44+ if ( platform ( ) === 'win32' ) {
45+ const candidates = [
46+ join ( 'C:' , 'Program Files' , 'Google' , 'Chrome' , 'Application' , 'chrome.exe' ) ,
47+ join ( 'C:' , 'Program Files (x86)' , 'Google' , 'Chrome' , 'Application' , 'chrome.exe' ) ,
48+ ] ;
49+ return candidates . find ( ( p ) => existsSync ( p ) ) ?? candidates [ 0 ] ;
50+ }
51+ return 'google-chrome' ;
52+ }
53+
54+ function defaultProfileDir ( ) : string {
55+ const home = homedir ( ) ;
56+ if ( isMac ( ) ) return join ( home , 'Library' , 'Application Support' , 'Google' , 'Chrome' ) ;
57+ if ( platform ( ) === 'win32' ) return join ( home , 'AppData' , 'Local' , 'Google' , 'Chrome' , 'User Data' ) ;
58+ return join ( home , '.config' , 'google-chrome' ) ;
59+ }
60+
61+ function copyProfile ( ) : void {
62+ const srcDir = defaultProfileDir ( ) ;
63+ rmSync ( USER_DATA_DIR , { recursive : true , force : true } ) ;
64+ mkdirSync ( USER_DATA_DIR , { recursive : true } ) ;
65+ copyFileSync ( join ( srcDir , 'Local State' ) , join ( USER_DATA_DIR , 'Local State' ) ) ;
66+ cpSync ( join ( srcDir , 'Default' ) , join ( USER_DATA_DIR , 'Default' ) , { recursive : true } ) ;
67+
68+ // Remove singleton/lock files from the source Chrome instance.
69+ for ( const f of [ 'SingletonLock' , 'SingletonCookie' , 'SingletonSocket' ] ) {
70+ rmSync ( join ( USER_DATA_DIR , f ) , { force : true } ) ;
71+ }
72+
73+ // Remove session files — they reference the original profile's state and trigger
74+ // "Something went wrong when opening your profile" when Chrome opens the copy.
75+ for ( const f of [ 'Current Session' , 'Current Tabs' , 'Last Session' , 'Last Tabs' ] ) {
76+ rmSync ( join ( USER_DATA_DIR , 'Default' , f ) , { force : true } ) ;
77+ }
78+
79+ // First Run sentinel tells Chrome this is a fresh start — suppresses first-run dialogs.
80+ writeFileSync ( join ( USER_DATA_DIR , 'First Run' ) , '' ) ;
81+ }
82+
83+ function launchChrome ( port : number ) : void {
84+ spawn ( chromeExec ( ) , [
85+ `--remote-debugging-port=${ port } ` ,
86+ `--user-data-dir=${ USER_DATA_DIR } ` ,
87+ '--profile-directory=Default' ,
88+ '--no-first-run' ,
89+ '--disable-session-crashed-bubble' ,
90+ ] , { detached : true , stdio : 'ignore' } ) . unref ( ) ;
91+ }
92+
93+ async function waitForCDP ( port : number , timeoutMs = 15000 ) : Promise < void > {
94+ const deadline = Date . now ( ) + timeoutMs ;
95+ while ( Date . now ( ) < deadline ) {
96+ try {
97+ const res = await fetch ( `http://localhost:${ port } /json/version` ) ;
98+ if ( res . ok ) return ;
99+ } catch {
100+ // not ready yet
101+ }
102+ await new Promise ( ( r ) => setTimeout ( r , 300 ) ) ;
103+ }
104+ throw new Error ( `Chrome did not expose CDP on port ${ port } within ${ timeoutMs } ms` ) ;
105+ }
106+
107+ export const launchChromeTool : ToolCallback = async ( {
108+ port = 9222 ,
109+ mode = 'newInstance' ,
110+ copyProfileFiles = false ,
111+ } : {
112+ port ?: number ;
113+ mode ?: 'newInstance' | 'freshSession' ;
114+ copyProfileFiles ?: boolean ;
115+ } ) : Promise < CallToolResult > => {
116+ const warnings : string [ ] = [ ] ;
117+ const notes : string [ ] = [ ] ;
118+
119+ try {
120+ if ( copyProfileFiles ) {
121+ warnings . push ( '⚠️ Cookies and logins were copied at this moment. Changes during this session won\'t sync back to your main profile.' ) ;
122+ copyProfile ( ) ;
123+ } else {
124+ notes . push ( mode === 'newInstance'
125+ ? 'No profile copied — this instance starts with no cookies or logins.'
126+ : 'Fresh profile — no existing cookies or logins.' ) ;
127+ rmSync ( USER_DATA_DIR , { recursive : true , force : true } ) ;
128+ mkdirSync ( USER_DATA_DIR , { recursive : true } ) ;
129+ }
130+
131+ launchChrome ( port ) ;
132+ await waitForCDP ( port ) ;
133+
134+ const lines = [
135+ `Chrome launched on port ${ port } (mode: ${ mode } ).` ,
136+ ...warnings ,
137+ ...notes ,
138+ ] ;
139+
140+ return { content : [ { type : 'text' , text : lines . join ( '\n' ) } ] } ;
141+ } catch ( e ) {
142+ return {
143+ isError : true ,
144+ content : [ { type : 'text' , text : `Error launching Chrome: ${ e } ` } ] ,
145+ } ;
146+ }
147+ } ;
0 commit comments