chrome-devtools-mcp-for-extension 0.18.0 â 0.18.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +232 -496
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/lantern/testing/MetricTestUtils.js +46 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/lantern/testing/testing.js +4 -0
- package/build/src/browser.js +21 -0
- package/build/src/config.js +13 -0
- package/build/src/graceful.js +125 -0
- package/build/src/login-helper.js +127 -19
- package/build/src/main.js +59 -0
- package/build/src/profile-resolver.js +78 -3
- package/build/src/project-root-state.js +13 -0
- package/build/src/roots-manager.js +16 -9
- package/build/src/selectors/gemini.json +24 -0
- package/build/src/selectors/loader.js +32 -0
- package/build/src/tools/diagnose-ui.js +9 -0
- package/build/src/tools/gemini-web.js +357 -0
- package/package.json +7 -5
- package/scripts/cli.mjs +44 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Copyright 2024 The Chromium Authors
|
|
2
|
+
// Use of this source code is governed by a BSD-style license that can be
|
|
3
|
+
// found in the LICENSE file.
|
|
4
|
+
// Unsure why this lint is failing, given `lantern/metrics/SpeedIndex.test.ts` does the same
|
|
5
|
+
// and is fine. Maybe `*.test.*` files are excluded from this rule?
|
|
6
|
+
// eslint-disable-next-line rulesdir/es-modules-import
|
|
7
|
+
import * as TraceLoader from '../../../../testing/TraceLoader.js';
|
|
8
|
+
import * as Trace from '../../trace.js';
|
|
9
|
+
import * as Lantern from '../lantern.js';
|
|
10
|
+
function toLanternTrace(traceEvents) {
|
|
11
|
+
return {
|
|
12
|
+
traceEvents: traceEvents,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
async function runTraceProcessor(context, trace) {
|
|
16
|
+
TraceLoader.TraceLoader.setTestTimeout(context);
|
|
17
|
+
const processor = Trace.Processor.TraceProcessor.createWithAllHandlers();
|
|
18
|
+
await processor.parse(trace.traceEvents, { isCPUProfile: false, isFreshRecording: true });
|
|
19
|
+
if (!processor.data) {
|
|
20
|
+
throw new Error('No data');
|
|
21
|
+
}
|
|
22
|
+
return processor.data;
|
|
23
|
+
}
|
|
24
|
+
async function getComputationDataFromFixture(context, { trace, settings, url }) {
|
|
25
|
+
settings = settings ?? {};
|
|
26
|
+
if (!settings.throttlingMethod) {
|
|
27
|
+
settings.throttlingMethod = 'simulate';
|
|
28
|
+
}
|
|
29
|
+
const data = await runTraceProcessor(context, trace);
|
|
30
|
+
const requests = Trace.LanternComputationData.createNetworkRequests(trace, data);
|
|
31
|
+
const networkAnalysis = Lantern.Core.NetworkAnalyzer.analyze(requests);
|
|
32
|
+
if (!networkAnalysis) {
|
|
33
|
+
throw new Error('no networkAnalysis');
|
|
34
|
+
}
|
|
35
|
+
const frameId = data.Meta.mainFrameId;
|
|
36
|
+
const navigationId = data.Meta.mainFrameNavigations[0].args.data?.navigationId;
|
|
37
|
+
if (!navigationId) {
|
|
38
|
+
throw new Error('no navigation id found');
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
simulator: Lantern.Simulation.Simulator.createSimulator({ ...settings, networkAnalysis }),
|
|
42
|
+
graph: Trace.LanternComputationData.createGraph(requests, trace, data, url),
|
|
43
|
+
processedNavigation: Trace.LanternComputationData.createProcessedNavigation(data, frameId, navigationId),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export { getComputationDataFromFixture, runTraceProcessor as runTrace, toLanternTrace, };
|
package/build/src/browser.js
CHANGED
|
@@ -8,6 +8,7 @@ import os from 'node:os';
|
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import puppeteer from 'puppeteer-core';
|
|
10
10
|
import { resolveUserDataDir } from './profile-resolver.js';
|
|
11
|
+
import { isProjectRootInitialized, getProjectRoot } from './project-root-state.js';
|
|
11
12
|
let browser;
|
|
12
13
|
const ignoredPrefixes = new Set([
|
|
13
14
|
'chrome://',
|
|
@@ -719,6 +720,26 @@ async function ensureBrowserLaunched(options) {
|
|
|
719
720
|
return browser;
|
|
720
721
|
}
|
|
721
722
|
export async function resolveBrowser(options) {
|
|
723
|
+
// CRITICAL: Project root must be initialized before launching Chrome
|
|
724
|
+
// This ensures proper profile isolation for multi-project environments
|
|
725
|
+
if (!isProjectRootInitialized() && !options.browserUrl && !options.userDataDir) {
|
|
726
|
+
const errorMsg = [
|
|
727
|
+
'â CRITICAL ERROR: Project root not initialized',
|
|
728
|
+
'',
|
|
729
|
+
'Chrome cannot be launched because the MCP client did not provide the project root directory.',
|
|
730
|
+
'This is required for proper Chrome profile isolation when using multiple projects.',
|
|
731
|
+
'',
|
|
732
|
+
'đ Required client behavior:',
|
|
733
|
+
'1. Client must call setProjectRoot() immediately after MCP server starts',
|
|
734
|
+
'2. Or set MCP_PROJECT_ROOT environment variable before launching MCP',
|
|
735
|
+
'3. Or use --project-root CLI flag',
|
|
736
|
+
'',
|
|
737
|
+
`Current state: projectRoot=${getProjectRoot() || 'undefined'}`,
|
|
738
|
+
`Process cwd: ${process.cwd()}`,
|
|
739
|
+
].join('\n');
|
|
740
|
+
console.error(errorMsg);
|
|
741
|
+
throw new Error('Project root not initialized - cannot launch Chrome without profile isolation');
|
|
742
|
+
}
|
|
722
743
|
const resolvedBrowser = options.browserUrl
|
|
723
744
|
? await ensureBrowserConnected(options.browserUrl)
|
|
724
745
|
: await ensureBrowserLaunched(options);
|
package/build/src/config.js
CHANGED
|
@@ -23,3 +23,16 @@ export const CHATGPT_CONFIG = {
|
|
|
23
23
|
*/
|
|
24
24
|
DEFAULT_MODEL: 'gpt-5-thinking',
|
|
25
25
|
};
|
|
26
|
+
/**
|
|
27
|
+
* Gemini configuration
|
|
28
|
+
*/
|
|
29
|
+
export const GEMINI_CONFIG = {
|
|
30
|
+
/**
|
|
31
|
+
* Default Gemini URL
|
|
32
|
+
*/
|
|
33
|
+
DEFAULT_URL: 'https://gemini.google.com/',
|
|
34
|
+
/**
|
|
35
|
+
* Base URL for Gemini
|
|
36
|
+
*/
|
|
37
|
+
BASE_URL: 'https://gemini.google.com/',
|
|
38
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Graceful Shutdown Module
|
|
8
|
+
*
|
|
9
|
+
* Ensures clean shutdown of MCP server:
|
|
10
|
+
* - Closes Puppeteer browser gracefully
|
|
11
|
+
* - Kills orphaned Chrome processes
|
|
12
|
+
* - Handles SIGTERM, SIGINT, disconnect, uncaught exceptions
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'node:fs/promises';
|
|
15
|
+
import process from 'node:process';
|
|
16
|
+
/**
|
|
17
|
+
* Setup graceful shutdown handlers
|
|
18
|
+
*/
|
|
19
|
+
export function setupGraceful(options) {
|
|
20
|
+
const { getBrowser, onBeforeExit, pidFile = process.env.MCP_BROWSER_PID_FILE, closeTimeoutMs = 3000, } = options;
|
|
21
|
+
let isShuttingDown = false;
|
|
22
|
+
/**
|
|
23
|
+
* Kill Chrome process using PID file
|
|
24
|
+
*/
|
|
25
|
+
async function killFromPidFile() {
|
|
26
|
+
if (!pidFile)
|
|
27
|
+
return;
|
|
28
|
+
try {
|
|
29
|
+
const txt = await fs.readFile(pidFile, 'utf8').catch(() => '');
|
|
30
|
+
const pid = Number(txt.trim());
|
|
31
|
+
if (pid > 0) {
|
|
32
|
+
try {
|
|
33
|
+
// Check if process exists
|
|
34
|
+
process.kill(pid, 0);
|
|
35
|
+
// Process exists, kill it
|
|
36
|
+
process.kill(pid, 'SIGKILL');
|
|
37
|
+
console.error(`[graceful] Killed Chrome process: ${pid}`);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
// Process doesn't exist (already dead)
|
|
41
|
+
console.error(`[graceful] Chrome process ${pid} already dead`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
await fs.rm(pidFile, { force: true });
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
console.error(`[graceful] Error killing Chrome from PID file:`, err);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Graceful exit handler
|
|
52
|
+
*/
|
|
53
|
+
async function gracefulExit(signal) {
|
|
54
|
+
if (isShuttingDown) {
|
|
55
|
+
console.error(`[graceful] Already shutting down, ignoring ${signal}`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
isShuttingDown = true;
|
|
59
|
+
console.error(`[graceful] Shutting down... (signal: ${signal || 'unknown'})`);
|
|
60
|
+
try {
|
|
61
|
+
// Step 1: onBeforeExit callback
|
|
62
|
+
if (onBeforeExit) {
|
|
63
|
+
try {
|
|
64
|
+
await onBeforeExit();
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
console.error(`[graceful] onBeforeExit error:`, err);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Step 2: Close browser gracefully (with timeout)
|
|
71
|
+
const browser = getBrowser?.();
|
|
72
|
+
if (browser) {
|
|
73
|
+
try {
|
|
74
|
+
console.error(`[graceful] Closing browser...`);
|
|
75
|
+
await Promise.race([
|
|
76
|
+
Promise.resolve(browser.close?.()),
|
|
77
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('browser.close() timeout')), closeTimeoutMs)),
|
|
78
|
+
]);
|
|
79
|
+
console.error(`[graceful] Browser closed successfully`);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
console.error(`[graceful] Browser close error:`, err);
|
|
83
|
+
// Continue to PID file cleanup
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Step 3: Kill orphaned Chrome (if browser.close() failed)
|
|
87
|
+
await killFromPidFile();
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
console.error(`[graceful] Error during shutdown:`, err);
|
|
91
|
+
}
|
|
92
|
+
console.error(`[graceful] Shutdown complete`);
|
|
93
|
+
process.exit(0);
|
|
94
|
+
}
|
|
95
|
+
// Register signal handlers
|
|
96
|
+
process.on('SIGTERM', () => gracefulExit('SIGTERM'));
|
|
97
|
+
process.on('SIGINT', () => gracefulExit('SIGINT'));
|
|
98
|
+
process.on('disconnect', () => gracefulExit('disconnect')); // Parent died
|
|
99
|
+
process.on('uncaughtException', async (err) => {
|
|
100
|
+
console.error('[graceful] Uncaught exception:', err);
|
|
101
|
+
await gracefulExit('uncaughtException');
|
|
102
|
+
});
|
|
103
|
+
process.on('unhandledRejection', async (err) => {
|
|
104
|
+
console.error('[graceful] Unhandled rejection:', err);
|
|
105
|
+
await gracefulExit('unhandledRejection');
|
|
106
|
+
});
|
|
107
|
+
return {
|
|
108
|
+
/**
|
|
109
|
+
* Announce Chrome PID to wrapper via PID file
|
|
110
|
+
*/
|
|
111
|
+
async announceBrowserPid(pid) {
|
|
112
|
+
if (!pidFile || !pid) {
|
|
113
|
+
console.error(`[graceful] Cannot announce browser PID: pidFile=${pidFile}, pid=${pid}`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
await fs.writeFile(pidFile, String(pid), 'utf8');
|
|
118
|
+
console.error(`[graceful] Announced browser PID ${pid} to ${pidFile}`);
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
console.error(`[graceful] Failed to write PID file:`, err);
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -9,38 +9,146 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export async function isLoginRequired(page) {
|
|
11
11
|
const currentUrl = page.url();
|
|
12
|
-
|
|
12
|
+
console.error(`[login-helper] Checking login status for URL: ${currentUrl}`);
|
|
13
|
+
// Method 1: Check URL patterns
|
|
13
14
|
if (currentUrl.includes('auth') ||
|
|
14
15
|
currentUrl.includes('login') ||
|
|
15
16
|
currentUrl.includes('signin')) {
|
|
17
|
+
console.error('[login-helper] â
Login required (URL contains auth/login/signin)');
|
|
16
18
|
return true;
|
|
17
19
|
}
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
// Gemini specific check
|
|
21
|
+
if (currentUrl.includes('gemini.google.com')) {
|
|
22
|
+
try {
|
|
23
|
+
const geminiContent = await page.evaluate(() => {
|
|
24
|
+
const bodyText = document.body.innerText.toLowerCase();
|
|
25
|
+
// Check for common login page text
|
|
26
|
+
const hasLoginText = bodyText.includes('sign in') ||
|
|
27
|
+
bodyText.includes('login') ||
|
|
28
|
+
bodyText.includes('google account');
|
|
29
|
+
// Check for Gemini composer
|
|
30
|
+
const hasComposer = !!(document.querySelector('div[contenteditable="true"]') ||
|
|
31
|
+
document.querySelector('textarea'));
|
|
32
|
+
return { hasLoginText, hasComposer };
|
|
33
|
+
});
|
|
34
|
+
console.error(`[login-helper] Gemini check: ${JSON.stringify(geminiContent)}`);
|
|
35
|
+
if (geminiContent.hasComposer) {
|
|
36
|
+
console.error('[login-helper] â Login NOT required (Gemini composer detected)');
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
console.error('[login-helper] â
Login required (Gemini composer missing)');
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
console.error(`[login-helper] Error checking Gemini login: ${error}`);
|
|
25
44
|
return true;
|
|
26
45
|
}
|
|
27
46
|
}
|
|
28
|
-
|
|
29
|
-
// Element check failed, continue to next method
|
|
30
|
-
}
|
|
31
|
-
// Method 3: Check for main content absence
|
|
47
|
+
// Method 2: Check page content for login indicators
|
|
32
48
|
try {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
//
|
|
49
|
+
const pageContent = await page.evaluate(() => {
|
|
50
|
+
// Check for common login page text
|
|
51
|
+
const bodyText = document.body.innerText.toLowerCase();
|
|
52
|
+
// ChatGPT-specific login indicators
|
|
53
|
+
const hasLoginText = bodyText.includes('log in') ||
|
|
54
|
+
bodyText.includes('sign in') ||
|
|
55
|
+
bodyText.includes('welcome to chatgpt');
|
|
56
|
+
// Check for login buttons - NATIVE DOM selectors only
|
|
57
|
+
const hasLoginButton = !!(document.querySelector('[data-testid*="login"]') ||
|
|
58
|
+
document.querySelector('[class*="login-button"]') ||
|
|
59
|
+
// Check for text content in buttons
|
|
60
|
+
Array.from(document.querySelectorAll('button')).some(btn => btn.textContent?.toLowerCase().includes('log in') ||
|
|
61
|
+
btn.textContent?.toLowerCase().includes('sign in')));
|
|
62
|
+
// Check for ChatGPT-specific composer (more strict detection)
|
|
63
|
+
// Look for the actual textarea/contenteditable that ChatGPT uses
|
|
64
|
+
let hasComposer = false;
|
|
65
|
+
// Method 1: Check for ChatGPT's main textarea (with validation)
|
|
66
|
+
const mainTextarea = document.querySelector('#prompt-textarea');
|
|
67
|
+
if (mainTextarea && mainTextarea instanceof HTMLTextAreaElement) {
|
|
68
|
+
// Additional validation: login page has a fallback textarea with class "_fallbackTextarea_"
|
|
69
|
+
// Real composer should NOT be disabled and should be in a proper composer container
|
|
70
|
+
const isFallback = mainTextarea.className.includes('fallback');
|
|
71
|
+
const isDisabled = mainTextarea.disabled;
|
|
72
|
+
if (!isFallback && !isDisabled) {
|
|
73
|
+
console.error('[login-helper] Found valid #prompt-textarea (not fallback)');
|
|
74
|
+
hasComposer = true;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
console.error('[login-helper] Found #prompt-textarea but it appears to be a fallback/disabled textarea');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Method 2: Check Shadow DOM for ChatGPT's composer
|
|
81
|
+
if (!hasComposer) {
|
|
82
|
+
const shadowHosts = Array.from(document.querySelectorAll('*'));
|
|
83
|
+
for (const host of shadowHosts) {
|
|
84
|
+
if (host.shadowRoot) {
|
|
85
|
+
const shadowTextarea = host.shadowRoot.querySelector('#prompt-textarea');
|
|
86
|
+
const shadowComposer = host.shadowRoot.querySelector('[data-testid="composer-textarea"]');
|
|
87
|
+
if (shadowTextarea || shadowComposer) {
|
|
88
|
+
console.error('[login-helper] Found composer in Shadow DOM');
|
|
89
|
+
hasComposer = true;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Method 3: Check for ChatGPT-specific ProseMirror editor (only if it's in main content area)
|
|
96
|
+
if (!hasComposer) {
|
|
97
|
+
const proseMirror = document.querySelector('.ProseMirror[contenteditable="true"]');
|
|
98
|
+
// Verify it's actually ChatGPT's main composer, not just any ProseMirror
|
|
99
|
+
if (proseMirror) {
|
|
100
|
+
const parent = proseMirror.closest('[class*="composer"], [class*="prompt"], [class*="input-area"]');
|
|
101
|
+
if (parent) {
|
|
102
|
+
console.error('[login-helper] Found ChatGPT ProseMirror composer');
|
|
103
|
+
hasComposer = true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
hasLoginText,
|
|
109
|
+
hasLoginButton,
|
|
110
|
+
hasComposer,
|
|
111
|
+
bodySnippet: bodyText.substring(0, 200)
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
console.error(`[login-helper] Page analysis: ${JSON.stringify(pageContent, null, 2)}`);
|
|
115
|
+
// Decision point 1: PRIORITY - Login button present = needs login (even if fallback composer exists)
|
|
116
|
+
if (pageContent.hasLoginButton) {
|
|
117
|
+
console.error('[login-helper] đ Decision: Login button detected (highest priority)');
|
|
118
|
+
console.error('[login-helper] hasLoginButton:', pageContent.hasLoginButton);
|
|
119
|
+
console.error('[login-helper] hasComposer:', pageContent.hasComposer);
|
|
120
|
+
console.error('[login-helper] â
Login required (login button detected)');
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
// Decision point 2: Login text present + NO valid composer = needs login
|
|
124
|
+
if (pageContent.hasLoginText && !pageContent.hasComposer) {
|
|
125
|
+
console.error('[login-helper] đ Decision: Login text detected + No valid composer');
|
|
126
|
+
console.error('[login-helper] hasLoginText:', pageContent.hasLoginText);
|
|
127
|
+
console.error('[login-helper] hasComposer:', pageContent.hasComposer);
|
|
128
|
+
console.error('[login-helper] â
Login required (login UI detected, no composer)');
|
|
37
129
|
return true;
|
|
38
130
|
}
|
|
131
|
+
// Decision point 3: Valid composer present = logged in
|
|
132
|
+
if (pageContent.hasComposer) {
|
|
133
|
+
console.error('[login-helper] đ Decision: Valid composer found (user is logged in)');
|
|
134
|
+
console.error('[login-helper] hasComposer:', pageContent.hasComposer);
|
|
135
|
+
console.error('[login-helper] â Login NOT required (composer detected)');
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
// Decision point 3: Ambiguous state - safe default is to assume login required
|
|
139
|
+
console.error('[login-helper] đ Decision: Unclear state (no login UI, no composer)');
|
|
140
|
+
console.error('[login-helper] hasLoginText:', pageContent.hasLoginText);
|
|
141
|
+
console.error('[login-helper] hasLoginButton:', pageContent.hasLoginButton);
|
|
142
|
+
console.error('[login-helper] hasComposer:', pageContent.hasComposer);
|
|
143
|
+
console.error('[login-helper] bodySnippet:', pageContent.bodySnippet);
|
|
144
|
+
console.error('[login-helper] â ī¸ Unclear state - assuming login required for safety');
|
|
145
|
+
return true;
|
|
39
146
|
}
|
|
40
|
-
catch {
|
|
41
|
-
|
|
147
|
+
catch (error) {
|
|
148
|
+
console.error(`[login-helper] Error during page content check: ${error}`);
|
|
149
|
+
// On error, assume login required for safety
|
|
150
|
+
return true;
|
|
42
151
|
}
|
|
43
|
-
return false;
|
|
44
152
|
}
|
|
45
153
|
/**
|
|
46
154
|
* Wait for user to complete login
|
package/build/src/main.js
CHANGED
|
@@ -3,6 +3,34 @@
|
|
|
3
3
|
* Copyright 2025 Google LLC
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
|
+
// Mock browser globals for chrome-devtools-frontend (Node.js environment)
|
|
7
|
+
// NOTE: chrome-devtools-frontend expects browser globals (location, self, localStorage)
|
|
8
|
+
// These are only needed for trace-processing modules, not for core MCP functionality
|
|
9
|
+
if (typeof globalThis.location === 'undefined') {
|
|
10
|
+
globalThis.location = {
|
|
11
|
+
search: '',
|
|
12
|
+
href: '',
|
|
13
|
+
protocol: 'file:',
|
|
14
|
+
host: '',
|
|
15
|
+
hostname: '',
|
|
16
|
+
port: '',
|
|
17
|
+
pathname: '',
|
|
18
|
+
hash: '',
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
if (typeof globalThis.self === 'undefined') {
|
|
22
|
+
globalThis.self = globalThis;
|
|
23
|
+
}
|
|
24
|
+
if (typeof globalThis.localStorage === 'undefined') {
|
|
25
|
+
globalThis.localStorage = {
|
|
26
|
+
getItem: () => null,
|
|
27
|
+
setItem: () => { },
|
|
28
|
+
removeItem: () => { },
|
|
29
|
+
clear: () => { },
|
|
30
|
+
key: () => null,
|
|
31
|
+
length: 0,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
6
34
|
import assert from 'node:assert';
|
|
7
35
|
import fs from 'node:fs';
|
|
8
36
|
import path from 'node:path';
|
|
@@ -17,6 +45,8 @@ import { McpResponse } from './McpResponse.js';
|
|
|
17
45
|
import { Mutex } from './Mutex.js';
|
|
18
46
|
import { resolveRoots } from './roots-manager.js';
|
|
19
47
|
import { runStartupCheck } from './startup-check.js';
|
|
48
|
+
import { setProjectRoot } from './project-root-state.js';
|
|
49
|
+
import { setupGraceful } from './graceful.js';
|
|
20
50
|
import * as bookmarkTools from './tools/bookmarks.js';
|
|
21
51
|
import * as chatgptWebTools from './tools/chatgpt-web.js';
|
|
22
52
|
import * as deepResearchChatGPTTools from './tools/deep_research_chatgpt.js';
|
|
@@ -24,6 +54,7 @@ import * as consoleTools from './tools/console.js';
|
|
|
24
54
|
import * as diagnoseUiTools from './tools/diagnose-ui.js';
|
|
25
55
|
import * as emulationTools from './tools/emulation.js';
|
|
26
56
|
import * as extensionTools from './tools/extensions.js';
|
|
57
|
+
import * as geminiWebTools from './tools/gemini-web.js';
|
|
27
58
|
import * as inputTools from './tools/input.js';
|
|
28
59
|
import * as networkTools from './tools/network.js';
|
|
29
60
|
import * as pagesTools from './tools/pages.js';
|
|
@@ -69,6 +100,17 @@ let context;
|
|
|
69
100
|
let uiHealthCheckRun = false; // Track if UI health check has been run
|
|
70
101
|
let cachedRootsInfo = null; // Cache roots info
|
|
71
102
|
let initializationComplete = false; // Track if MCP initialization is complete
|
|
103
|
+
// Setup graceful shutdown
|
|
104
|
+
const graceful = setupGraceful({
|
|
105
|
+
getBrowser: () => context?.browser,
|
|
106
|
+
onBeforeExit: async () => {
|
|
107
|
+
logger('[graceful] Running pre-exit cleanup...');
|
|
108
|
+
// Close log file if exists
|
|
109
|
+
if (logFile) {
|
|
110
|
+
logFile.close();
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
});
|
|
72
114
|
async function getContext() {
|
|
73
115
|
// Wait for initialization to complete before resolving roots
|
|
74
116
|
if (!initializationComplete) {
|
|
@@ -86,6 +128,17 @@ async function getContext() {
|
|
|
86
128
|
autoCwd: process.cwd(),
|
|
87
129
|
});
|
|
88
130
|
}
|
|
131
|
+
// Initialize project root for profile isolation
|
|
132
|
+
// Priority: CLI flag > MCP_PROJECT_ROOT env > Roots protocol > cwd
|
|
133
|
+
const rootFromRoots = cachedRootsInfo?.rootsUris?.[0]
|
|
134
|
+
? cachedRootsInfo.rootsUris[0].replace('file://', '')
|
|
135
|
+
: undefined;
|
|
136
|
+
const projectRootToSet = args.projectRoot ||
|
|
137
|
+
process.env.MCP_PROJECT_ROOT ||
|
|
138
|
+
rootFromRoots ||
|
|
139
|
+
process.cwd();
|
|
140
|
+
setProjectRoot(projectRootToSet);
|
|
141
|
+
logger(`[project-root] Initialized: ${projectRootToSet}`);
|
|
89
142
|
const browserOptions = {
|
|
90
143
|
browserUrl: args.browserUrl,
|
|
91
144
|
headless: args.headless,
|
|
@@ -102,6 +155,11 @@ async function getContext() {
|
|
|
102
155
|
rootsInfo: cachedRootsInfo, // Pass roots info to browser
|
|
103
156
|
};
|
|
104
157
|
const browser = await resolveBrowser(browserOptions);
|
|
158
|
+
// Announce browser PID for graceful shutdown
|
|
159
|
+
const browserPid = browser.process()?.pid;
|
|
160
|
+
if (browserPid) {
|
|
161
|
+
await graceful.announceBrowserPid(browserPid);
|
|
162
|
+
}
|
|
105
163
|
// Browser factory function for reconnection
|
|
106
164
|
const browserFactory = async () => {
|
|
107
165
|
logger('Reconnecting browser...');
|
|
@@ -188,6 +246,7 @@ const tools = [
|
|
|
188
246
|
...Object.values(bookmarkTools),
|
|
189
247
|
...Object.values(chatgptWebTools),
|
|
190
248
|
...Object.values(deepResearchChatGPTTools),
|
|
249
|
+
...Object.values(geminiWebTools),
|
|
191
250
|
...Object.values(consoleTools),
|
|
192
251
|
...Object.values(diagnoseUiTools),
|
|
193
252
|
...Object.values(emulationTools),
|
|
@@ -15,6 +15,7 @@ import path from 'node:path';
|
|
|
15
15
|
import crypto from 'node:crypto';
|
|
16
16
|
import { detectProjectName, detectProjectRoot } from './project-detector.js';
|
|
17
17
|
import { detectClientType } from './client-detector.js';
|
|
18
|
+
import { getProjectRoot } from './project-root-state.js';
|
|
18
19
|
const CACHE_ROOT = path.join(os.homedir(), '.cache', 'chrome-devtools-mcp');
|
|
19
20
|
// --- Public API ---
|
|
20
21
|
export function resolveUserDataDir(opts) {
|
|
@@ -47,7 +48,25 @@ export function resolveUserDataDir(opts) {
|
|
|
47
48
|
clientId = sanitize(detected);
|
|
48
49
|
console.error(`[profiles] Auto-detected client from parent process: ${clientId}`);
|
|
49
50
|
}
|
|
50
|
-
//
|
|
51
|
+
// 0a) MCP_SHARED_PROFILE â shared profile across all projects
|
|
52
|
+
// - Uses a single profile for all projects to avoid re-login
|
|
53
|
+
if (opts.env.MCP_SHARED_PROFILE === 'true') {
|
|
54
|
+
const key = 'shared';
|
|
55
|
+
const p = projectProfilePath(key, clientId, channel);
|
|
56
|
+
const result = {
|
|
57
|
+
path: p,
|
|
58
|
+
reason: 'MCP_USER_DATA_DIR', // Treat as explicit user choice
|
|
59
|
+
projectKey: key,
|
|
60
|
+
projectName: 'shared',
|
|
61
|
+
hash: '00000000',
|
|
62
|
+
clientId,
|
|
63
|
+
channel,
|
|
64
|
+
};
|
|
65
|
+
console.error(`[profiles] resolved(MCP_SHARED_PROFILE): ${result.path} (client=${clientId})`);
|
|
66
|
+
console.error(`[profiles] âšī¸ Using shared profile - login state persists across all projects`);
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
// 0b) CI detection â ephemeral session directory (unless MCP_PERSIST_PROFILES)
|
|
51
70
|
// - This happens before other priorities to keep CI clean by default.
|
|
52
71
|
if (isCI(opts.env) && !opts.env.MCP_PERSIST_PROFILES) {
|
|
53
72
|
const sessionId = `${process.pid}-${Date.now()}`;
|
|
@@ -122,9 +141,65 @@ export function resolveUserDataDir(opts) {
|
|
|
122
141
|
console.error(`[profiles] resolved(MCP_PROJECT_ID): ${result.path} (projectId=${projectId}, client=${clientId})`);
|
|
123
142
|
return result;
|
|
124
143
|
}
|
|
125
|
-
//
|
|
144
|
+
// 3.5) ENV: MCP_PROJECT_ROOT (MCP Roots protocol - actual project directory)
|
|
145
|
+
const projectRoot = opts.env.MCP_PROJECT_ROOT;
|
|
146
|
+
if (projectRoot) {
|
|
147
|
+
console.error(`[profiles] MCP_PROJECT_ROOT detected: "${projectRoot}"`);
|
|
148
|
+
try {
|
|
149
|
+
const name = detectProjectName(projectRoot);
|
|
150
|
+
console.error(`[profiles] MCP_PROJECT_ROOT name="${name}"`);
|
|
151
|
+
const realRoot = realpathSafe(projectRoot);
|
|
152
|
+
const hash = shortHash(realRoot);
|
|
153
|
+
const key = `${sanitize(name)}_${hash}`;
|
|
154
|
+
const p = projectProfilePath(key, clientId, channel);
|
|
155
|
+
const result = {
|
|
156
|
+
path: p,
|
|
157
|
+
reason: 'MCP_PROJECT_ROOT',
|
|
158
|
+
projectKey: key,
|
|
159
|
+
projectName: sanitize(name),
|
|
160
|
+
hash,
|
|
161
|
+
clientId,
|
|
162
|
+
channel,
|
|
163
|
+
};
|
|
164
|
+
console.error(`[profiles] resolved(MCP_PROJECT_ROOT): ${result.path} (root=${projectRoot}, name=${name}, hash=${hash}, client=${clientId})`);
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
catch (e) {
|
|
168
|
+
console.error(`[profiles] MCP_PROJECT_ROOT resolution failed: ${e}`);
|
|
169
|
+
// Fall through to AUTO detection
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// 4) INITIALIZED_PROJECT_ROOT: Use globally initialized project root (highest priority for AUTO mode)
|
|
173
|
+
const initializedRoot = getProjectRoot();
|
|
174
|
+
if (initializedRoot) {
|
|
175
|
+
console.error(`[profiles] Using initialized project root: "${initializedRoot}"`);
|
|
176
|
+
try {
|
|
177
|
+
const name = detectProjectName(initializedRoot);
|
|
178
|
+
console.error(`[profiles] Initialized root name="${name}"`);
|
|
179
|
+
const realRoot = realpathSafe(initializedRoot);
|
|
180
|
+
const hash = shortHash(realRoot);
|
|
181
|
+
const key = `${sanitize(name)}_${hash}`;
|
|
182
|
+
const p = projectProfilePath(key, clientId, channel);
|
|
183
|
+
const result = {
|
|
184
|
+
path: p,
|
|
185
|
+
reason: 'AUTO',
|
|
186
|
+
projectKey: key,
|
|
187
|
+
projectName: sanitize(name),
|
|
188
|
+
hash,
|
|
189
|
+
clientId,
|
|
190
|
+
channel,
|
|
191
|
+
};
|
|
192
|
+
console.error(`[profiles] resolved(INITIALIZED_ROOT): ${result.path} (root=${initializedRoot}, name=${name}, hash=${hash}, client=${clientId})`);
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
console.error(`[profiles] Initialized root resolution failed: ${e}`);
|
|
197
|
+
// Fall through to cwd-based AUTO detection
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// 5) AUTO: detect by root -> name -> hash (fallback if no initialized root)
|
|
126
201
|
try {
|
|
127
|
-
console.error(`[profiles] AUTO detection: cwd="${opts.cwd}"`);
|
|
202
|
+
console.error(`[profiles] AUTO detection (cwd fallback): cwd="${opts.cwd}"`);
|
|
128
203
|
const root = detectProjectRoot(opts.cwd);
|
|
129
204
|
console.error(`[profiles] AUTO detection: root="${root}"`);
|
|
130
205
|
const name = detectProjectName(root);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// src/project-root-state.ts
|
|
2
|
+
// Global state for MCP project root (set during initialization)
|
|
3
|
+
let projectRoot;
|
|
4
|
+
export function setProjectRoot(path) {
|
|
5
|
+
projectRoot = path;
|
|
6
|
+
console.error(`[MCP] Project root initialized: ${path}`);
|
|
7
|
+
}
|
|
8
|
+
export function getProjectRoot() {
|
|
9
|
+
return projectRoot;
|
|
10
|
+
}
|
|
11
|
+
export function isProjectRootInitialized() {
|
|
12
|
+
return projectRoot !== undefined;
|
|
13
|
+
}
|