chrome-devtools-mcp-for-extension 0.9.10 → 0.9.12
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/build/src/browser.js +245 -52
- package/build/src/cli.js +5 -0
- package/build/src/main.js +1 -0
- package/package.json +1 -1
package/build/src/browser.js
CHANGED
|
@@ -98,6 +98,195 @@ function scanExtensionsDirectory(extensionsDir) {
|
|
|
98
98
|
}
|
|
99
99
|
return extensionPaths;
|
|
100
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Get system Chrome executable path
|
|
103
|
+
*/
|
|
104
|
+
function getSystemChromeExecutable(channel) {
|
|
105
|
+
const platform = os.platform();
|
|
106
|
+
if (platform === 'darwin') {
|
|
107
|
+
// macOS
|
|
108
|
+
if (channel === 'canary') {
|
|
109
|
+
return '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary';
|
|
110
|
+
}
|
|
111
|
+
else if (channel === 'beta') {
|
|
112
|
+
return '/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta';
|
|
113
|
+
}
|
|
114
|
+
else if (channel === 'dev') {
|
|
115
|
+
return '/Applications/Google Chrome Dev.app/Contents/MacOS/Google Chrome Dev';
|
|
116
|
+
}
|
|
117
|
+
return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
|
118
|
+
}
|
|
119
|
+
else if (platform === 'win32') {
|
|
120
|
+
// Windows
|
|
121
|
+
const programFiles = process.env['ProgramFiles'] || 'C:\\Program Files';
|
|
122
|
+
const programFilesX86 = process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)';
|
|
123
|
+
if (channel === 'canary') {
|
|
124
|
+
return path.join(process.env.LOCALAPPDATA || '', 'Google', 'Chrome SxS', 'Application', 'chrome.exe');
|
|
125
|
+
}
|
|
126
|
+
else if (channel === 'beta') {
|
|
127
|
+
return path.join(programFiles, 'Google', 'Chrome Beta', 'Application', 'chrome.exe');
|
|
128
|
+
}
|
|
129
|
+
else if (channel === 'dev') {
|
|
130
|
+
return path.join(programFiles, 'Google', 'Chrome Dev', 'Application', 'chrome.exe');
|
|
131
|
+
}
|
|
132
|
+
// Try both Program Files locations
|
|
133
|
+
const path1 = path.join(programFiles, 'Google', 'Chrome', 'Application', 'chrome.exe');
|
|
134
|
+
const path2 = path.join(programFilesX86, 'Google', 'Chrome', 'Application', 'chrome.exe');
|
|
135
|
+
if (fs.existsSync(path1))
|
|
136
|
+
return path1;
|
|
137
|
+
if (fs.existsSync(path2))
|
|
138
|
+
return path2;
|
|
139
|
+
return path1; // Default fallback
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
// Linux
|
|
143
|
+
if (channel === 'canary' || channel === 'dev') {
|
|
144
|
+
return '/usr/bin/google-chrome-unstable';
|
|
145
|
+
}
|
|
146
|
+
else if (channel === 'beta') {
|
|
147
|
+
return '/usr/bin/google-chrome-beta';
|
|
148
|
+
}
|
|
149
|
+
// Try google-chrome first, fallback to chromium
|
|
150
|
+
if (fs.existsSync('/usr/bin/google-chrome')) {
|
|
151
|
+
return '/usr/bin/google-chrome';
|
|
152
|
+
}
|
|
153
|
+
return '/usr/bin/chromium-browser';
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Get System Chrome User Data directory (not Chrome for Testing)
|
|
158
|
+
*/
|
|
159
|
+
function getSystemChromeUserDataDir(channel) {
|
|
160
|
+
const homeDir = os.homedir();
|
|
161
|
+
const platform = os.platform();
|
|
162
|
+
if (platform === 'darwin') {
|
|
163
|
+
// macOS
|
|
164
|
+
let chromeDataPath = path.join(homeDir, 'Library', 'Application Support', 'Google', 'Chrome');
|
|
165
|
+
if (channel === 'canary') {
|
|
166
|
+
chromeDataPath = path.join(homeDir, 'Library', 'Application Support', 'Google', 'Chrome Canary');
|
|
167
|
+
}
|
|
168
|
+
else if (channel === 'beta') {
|
|
169
|
+
chromeDataPath = path.join(homeDir, 'Library', 'Application Support', 'Google', 'Chrome Beta');
|
|
170
|
+
}
|
|
171
|
+
else if (channel === 'dev') {
|
|
172
|
+
chromeDataPath = path.join(homeDir, 'Library', 'Application Support', 'Google', 'Chrome Dev');
|
|
173
|
+
}
|
|
174
|
+
return chromeDataPath;
|
|
175
|
+
}
|
|
176
|
+
else if (platform === 'win32') {
|
|
177
|
+
// Windows
|
|
178
|
+
let chromeDataPath = path.join(homeDir, 'AppData', 'Local', 'Google', 'Chrome', 'User Data');
|
|
179
|
+
if (channel === 'canary') {
|
|
180
|
+
chromeDataPath = path.join(homeDir, 'AppData', 'Local', 'Google', 'Chrome SxS', 'User Data');
|
|
181
|
+
}
|
|
182
|
+
else if (channel === 'beta') {
|
|
183
|
+
chromeDataPath = path.join(homeDir, 'AppData', 'Local', 'Google', 'Chrome Beta', 'User Data');
|
|
184
|
+
}
|
|
185
|
+
else if (channel === 'dev') {
|
|
186
|
+
chromeDataPath = path.join(homeDir, 'AppData', 'Local', 'Google', 'Chrome Dev', 'User Data');
|
|
187
|
+
}
|
|
188
|
+
return chromeDataPath;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
// Linux
|
|
192
|
+
let chromeDataPath = path.join(homeDir, '.config', 'google-chrome');
|
|
193
|
+
if (channel === 'canary') {
|
|
194
|
+
chromeDataPath = path.join(homeDir, '.config', 'google-chrome-unstable');
|
|
195
|
+
}
|
|
196
|
+
else if (channel === 'beta') {
|
|
197
|
+
chromeDataPath = path.join(homeDir, '.config', 'google-chrome-beta');
|
|
198
|
+
}
|
|
199
|
+
else if (channel === 'dev') {
|
|
200
|
+
chromeDataPath = path.join(homeDir, '.config', 'google-chrome-unstable');
|
|
201
|
+
}
|
|
202
|
+
// Check if google-chrome exists, fallback to chromium
|
|
203
|
+
if (!fs.existsSync(chromeDataPath)) {
|
|
204
|
+
const chromiumPath = path.join(homeDir, '.config', 'chromium');
|
|
205
|
+
if (fs.existsSync(chromiumPath)) {
|
|
206
|
+
return chromiumPath;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return chromeDataPath;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Read Local State file to get last used profile
|
|
214
|
+
*/
|
|
215
|
+
function readLocalState(userDataDir) {
|
|
216
|
+
const localStatePath = path.join(userDataDir, 'Local State');
|
|
217
|
+
try {
|
|
218
|
+
if (!fs.existsSync(localStatePath)) {
|
|
219
|
+
return {};
|
|
220
|
+
}
|
|
221
|
+
const content = fs.readFileSync(localStatePath, 'utf-8');
|
|
222
|
+
const json = JSON.parse(content);
|
|
223
|
+
const lastUsed = json?.profile?.last_used;
|
|
224
|
+
if (typeof lastUsed === 'string') {
|
|
225
|
+
return { lastUsed };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
console.warn(`Failed to read Local State: ${error instanceof Error ? error.message : String(error)}`);
|
|
230
|
+
}
|
|
231
|
+
return {};
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Compare version strings (e.g., "2.3.2_0" vs "2.3.1_0")
|
|
235
|
+
*/
|
|
236
|
+
function compareVersion(a, b) {
|
|
237
|
+
// Normalize: "2.3.2_0" → [2, 3, 2]
|
|
238
|
+
const normalize = (v) => v.split('_')[0].split('.').map(x => parseInt(x, 10) || 0);
|
|
239
|
+
const aParts = normalize(a);
|
|
240
|
+
const bParts = normalize(b);
|
|
241
|
+
const maxLen = Math.max(aParts.length, bParts.length);
|
|
242
|
+
for (let i = 0; i < maxLen; i++) {
|
|
243
|
+
const diff = (aParts[i] || 0) - (bParts[i] || 0);
|
|
244
|
+
if (diff !== 0)
|
|
245
|
+
return diff;
|
|
246
|
+
}
|
|
247
|
+
return 0;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Scan one profile's Extensions directory and return extension paths
|
|
251
|
+
*/
|
|
252
|
+
function scanExtensionsInProfile(profileDir) {
|
|
253
|
+
const extensionPaths = [];
|
|
254
|
+
const extensionsDir = path.join(profileDir, 'Extensions');
|
|
255
|
+
if (!fs.existsSync(extensionsDir)) {
|
|
256
|
+
return extensionPaths;
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
const extensionIds = fs.readdirSync(extensionsDir, { withFileTypes: true });
|
|
260
|
+
for (const extensionEntry of extensionIds) {
|
|
261
|
+
if (!extensionEntry.isDirectory())
|
|
262
|
+
continue;
|
|
263
|
+
const extensionIdPath = path.join(extensionsDir, extensionEntry.name);
|
|
264
|
+
try {
|
|
265
|
+
const versions = fs.readdirSync(extensionIdPath, { withFileTypes: true })
|
|
266
|
+
.filter(e => e.isDirectory())
|
|
267
|
+
.map(e => e.name);
|
|
268
|
+
if (versions.length === 0)
|
|
269
|
+
continue;
|
|
270
|
+
// Find the latest version
|
|
271
|
+
const latestVersion = versions.sort(compareVersion).pop();
|
|
272
|
+
const versionPath = path.join(extensionIdPath, latestVersion);
|
|
273
|
+
const manifestPath = path.join(versionPath, 'manifest.json');
|
|
274
|
+
const manifest = validateExtensionManifest(manifestPath);
|
|
275
|
+
if (manifest) {
|
|
276
|
+
extensionPaths.push(versionPath);
|
|
277
|
+
console.error(` ✅ ${manifest.name} v${manifest.version} (MV${manifest.manifest_version})`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
console.warn(`Error processing extension ${extensionEntry.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
console.error(`Error scanning extensions in ${extensionsDir}: ${error instanceof Error ? error.message : String(error)}`);
|
|
287
|
+
}
|
|
288
|
+
return extensionPaths;
|
|
289
|
+
}
|
|
101
290
|
/**
|
|
102
291
|
* Get the Chrome extensions directory path for the current platform
|
|
103
292
|
*/
|
|
@@ -171,58 +360,53 @@ function validateExtensionManifest(manifestPath) {
|
|
|
171
360
|
}
|
|
172
361
|
/**
|
|
173
362
|
* Discover Chrome extensions installed in the system
|
|
363
|
+
* Uses Local State to determine the active profile, or uses specified profile
|
|
174
364
|
*/
|
|
175
|
-
function discoverSystemExtensions(channel) {
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
console.error(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
365
|
+
function discoverSystemExtensions(channel, chromeProfile) {
|
|
366
|
+
console.error(`🔍 Discovering system Chrome extensions...`);
|
|
367
|
+
const userDataDir = getSystemChromeUserDataDir(channel);
|
|
368
|
+
console.error(`📁 Chrome User Data: ${userDataDir}`);
|
|
369
|
+
// Determine target profile
|
|
370
|
+
let targetProfile;
|
|
371
|
+
if (chromeProfile) {
|
|
372
|
+
// CLI-specified profile takes priority
|
|
373
|
+
targetProfile = chromeProfile;
|
|
374
|
+
console.error(`🎯 Using CLI-specified profile: ${targetProfile}`);
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
// Read Local State to get last used profile
|
|
378
|
+
const { lastUsed } = readLocalState(userDataDir);
|
|
379
|
+
targetProfile = lastUsed || 'Default';
|
|
380
|
+
if (lastUsed) {
|
|
381
|
+
console.error(`🎯 Using last-used profile from Local State: ${targetProfile}`);
|
|
183
382
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if (!extensionEntry.isDirectory())
|
|
187
|
-
continue;
|
|
188
|
-
const extensionIdPath = path.join(extensionsDir, extensionEntry.name);
|
|
189
|
-
try {
|
|
190
|
-
// Each extension ID directory contains version subdirectories
|
|
191
|
-
const versions = fs.readdirSync(extensionIdPath, { withFileTypes: true });
|
|
192
|
-
// Find the latest/most recent version
|
|
193
|
-
let latestVersion = '';
|
|
194
|
-
let latestPath = '';
|
|
195
|
-
for (const versionEntry of versions) {
|
|
196
|
-
if (!versionEntry.isDirectory())
|
|
197
|
-
continue;
|
|
198
|
-
const versionPath = path.join(extensionIdPath, versionEntry.name);
|
|
199
|
-
const manifestPath = path.join(versionPath, 'manifest.json');
|
|
200
|
-
const manifest = validateExtensionManifest(manifestPath);
|
|
201
|
-
if (manifest) {
|
|
202
|
-
// Use the first valid version found (Chrome keeps the latest active)
|
|
203
|
-
if (!latestVersion || versionEntry.name > latestVersion) {
|
|
204
|
-
latestVersion = versionEntry.name;
|
|
205
|
-
latestPath = versionPath;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
if (latestPath && latestVersion) {
|
|
210
|
-
const manifest = validateExtensionManifest(path.join(latestPath, 'manifest.json'));
|
|
211
|
-
if (manifest) {
|
|
212
|
-
extensionPaths.push(latestPath);
|
|
213
|
-
console.error(` ✅ Found: ${manifest.name} v${manifest.version} (Manifest v${manifest.manifest_version})`);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
catch (error) {
|
|
218
|
-
console.warn(`Error processing extension ${extensionEntry.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
219
|
-
}
|
|
383
|
+
else {
|
|
384
|
+
console.error(`🎯 Local State not found, using Default profile`);
|
|
220
385
|
}
|
|
221
|
-
console.error(`📦 System extension discovery complete: ${extensionPaths.length} valid extensions found`);
|
|
222
386
|
}
|
|
223
|
-
|
|
224
|
-
|
|
387
|
+
// Check if target profile exists
|
|
388
|
+
const profileDir = path.join(userDataDir, targetProfile);
|
|
389
|
+
if (!fs.existsSync(profileDir)) {
|
|
390
|
+
console.warn(`⚠️ Profile directory not found: ${targetProfile}`);
|
|
391
|
+
// Fallback to Default
|
|
392
|
+
if (targetProfile !== 'Default') {
|
|
393
|
+
console.error(`📁 Falling back to Default profile`);
|
|
394
|
+
targetProfile = 'Default';
|
|
395
|
+
const defaultProfileDir = path.join(userDataDir, targetProfile);
|
|
396
|
+
if (!fs.existsSync(defaultProfileDir)) {
|
|
397
|
+
console.error(`❌ Default profile also not found. No extensions will be loaded.`);
|
|
398
|
+
return [];
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
console.error(`❌ Default profile not found. No extensions will be loaded.`);
|
|
403
|
+
return [];
|
|
404
|
+
}
|
|
225
405
|
}
|
|
406
|
+
// Scan the target profile
|
|
407
|
+
console.error(`📂 Scanning profile: ${targetProfile}`);
|
|
408
|
+
const extensionPaths = scanExtensionsInProfile(path.join(userDataDir, targetProfile));
|
|
409
|
+
console.error(`📦 Total: ${extensionPaths.length} extension(s) found in profile "${targetProfile}"`);
|
|
226
410
|
return extensionPaths;
|
|
227
411
|
}
|
|
228
412
|
// Store development extension paths globally for later retrieval
|
|
@@ -231,7 +415,7 @@ export function getDevelopmentExtensionPaths() {
|
|
|
231
415
|
return developmentExtensionPaths;
|
|
232
416
|
}
|
|
233
417
|
export async function launch(options) {
|
|
234
|
-
const { channel, executablePath, customDevTools, headless, isolated, loadExtension, loadExtensionsDir, loadSystemExtensions, } = options;
|
|
418
|
+
const { channel, executablePath, customDevTools, headless, isolated, loadExtension, loadExtensionsDir, loadSystemExtensions, chromeProfile, } = options;
|
|
235
419
|
// Reset development extension paths
|
|
236
420
|
developmentExtensionPaths = [];
|
|
237
421
|
const profileDirName = channel && channel !== 'stable'
|
|
@@ -294,7 +478,7 @@ export async function launch(options) {
|
|
|
294
478
|
// System extension discovery (default: true unless isolated flag is set)
|
|
295
479
|
const shouldLoadSystemExtensions = loadSystemExtensions ?? !isolated;
|
|
296
480
|
if (shouldLoadSystemExtensions) {
|
|
297
|
-
const systemExtensions = discoverSystemExtensions(channel);
|
|
481
|
+
const systemExtensions = discoverSystemExtensions(channel, chromeProfile);
|
|
298
482
|
if (systemExtensions.length > 0) {
|
|
299
483
|
extensionPaths.push(...systemExtensions);
|
|
300
484
|
console.error(`✅ Loaded ${systemExtensions.length} system Chrome extension(s)`);
|
|
@@ -318,8 +502,17 @@ export async function launch(options) {
|
|
|
318
502
|
// Add Google login automation detection bypass
|
|
319
503
|
args.push('--disable-blink-features=AutomationControlled');
|
|
320
504
|
console.error('Added Google login bypass: --disable-blink-features=AutomationControlled');
|
|
505
|
+
// Use system Chrome instead of Chrome for Testing when loading extensions
|
|
321
506
|
let puppeterChannel;
|
|
322
|
-
|
|
507
|
+
let effectiveExecutablePath = executablePath;
|
|
508
|
+
if (!executablePath && extensionPaths.length > 0) {
|
|
509
|
+
// Auto-detect system Chrome executable for extension support
|
|
510
|
+
effectiveExecutablePath = getSystemChromeExecutable(channel);
|
|
511
|
+
console.error(`🔍 Auto-detected system Chrome: ${effectiveExecutablePath}`);
|
|
512
|
+
console.error(`💡 Using system Chrome for extension support (not Chrome for Testing)`);
|
|
513
|
+
}
|
|
514
|
+
else if (!executablePath) {
|
|
515
|
+
// No extensions, use Chrome for Testing via channel
|
|
323
516
|
puppeterChannel =
|
|
324
517
|
channel && channel !== 'stable'
|
|
325
518
|
? `chrome-${channel}`
|
|
@@ -328,7 +521,7 @@ export async function launch(options) {
|
|
|
328
521
|
// Log complete Chrome configuration before launch
|
|
329
522
|
console.error('Chrome Launch Configuration:');
|
|
330
523
|
console.error(` Channel: ${puppeterChannel || 'default'}`);
|
|
331
|
-
console.error(` Executable: ${
|
|
524
|
+
console.error(` Executable: ${effectiveExecutablePath || 'auto-detected'}`);
|
|
332
525
|
console.error(` User Data Dir: ${userDataDir || 'temporary'}`);
|
|
333
526
|
console.error(` Profile Directory: ${profileDirectory}`);
|
|
334
527
|
console.error(` Profile Type: ${usingSystemProfile ? 'System Profile (auto-detected)' : 'Custom Profile'}`);
|
|
@@ -346,7 +539,7 @@ export async function launch(options) {
|
|
|
346
539
|
const browser = await puppeteer.launch({
|
|
347
540
|
...connectOptions,
|
|
348
541
|
channel: puppeterChannel,
|
|
349
|
-
executablePath,
|
|
542
|
+
executablePath: effectiveExecutablePath,
|
|
350
543
|
defaultViewport: null,
|
|
351
544
|
userDataDir,
|
|
352
545
|
pipe: true,
|
package/build/src/cli.js
CHANGED
|
@@ -60,6 +60,11 @@ export const cliOptions = {
|
|
|
60
60
|
default: false,
|
|
61
61
|
conflicts: 'browserUrl',
|
|
62
62
|
},
|
|
63
|
+
chromeProfile: {
|
|
64
|
+
type: 'string',
|
|
65
|
+
description: 'Specify Chrome profile name (e.g., "Default", "Profile 1", "Profile 2"). If not specified, uses last_used from Local State. Only effective when --loadSystemExtensions is true.',
|
|
66
|
+
conflicts: 'browserUrl',
|
|
67
|
+
},
|
|
63
68
|
userDataDir: {
|
|
64
69
|
type: 'string',
|
|
65
70
|
description: 'Specify a custom user data directory for Chrome to use instead of the default. Auto-detected if not specified.',
|
package/build/src/main.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-devtools-mcp-for-extension",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.12",
|
|
4
4
|
"description": "MCP server for Chrome extension development with Web Store automation. Fork of chrome-devtools-mcp with extension-specific tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": "./build/src/index.js",
|