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.
@@ -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
- const extensionPaths = [];
177
- const extensionsDir = getChromeExtensionsDirectory(channel);
178
- console.error(`🔍 Discovering system Chrome extensions in: ${extensionsDir}`);
179
- try {
180
- if (!fs.existsSync(extensionsDir)) {
181
- console.warn(`System Chrome extensions directory not found: ${extensionsDir}`);
182
- return extensionPaths;
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
- const extensionIds = fs.readdirSync(extensionsDir, { withFileTypes: true });
185
- for (const extensionEntry of extensionIds) {
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
- catch (error) {
224
- console.error(`Error discovering system extensions: ${error instanceof Error ? error.message : String(error)}`);
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
- if (!executablePath) {
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: ${executablePath || 'auto-detected'}`);
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
@@ -65,6 +65,7 @@ async function getContext() {
65
65
  loadExtension: args.loadExtension,
66
66
  loadExtensionsDir: args.loadExtensionsDir,
67
67
  loadSystemExtensions: args.loadSystemExtensions,
68
+ chromeProfile: args.chromeProfile,
68
69
  userDataDir: args.userDataDir,
69
70
  logFile,
70
71
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-devtools-mcp-for-extension",
3
- "version": "0.9.10",
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",