chrome-devtools-mcp-for-extension 0.9.9 → 0.9.11

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,140 @@ function scanExtensionsDirectory(extensionsDir) {
98
98
  }
99
99
  return extensionPaths;
100
100
  }
101
+ /**
102
+ * Get System Chrome User Data directory (not Chrome for Testing)
103
+ */
104
+ function getSystemChromeUserDataDir(channel) {
105
+ const homeDir = os.homedir();
106
+ const platform = os.platform();
107
+ if (platform === 'darwin') {
108
+ // macOS
109
+ let chromeDataPath = path.join(homeDir, 'Library', 'Application Support', 'Google', 'Chrome');
110
+ if (channel === 'canary') {
111
+ chromeDataPath = path.join(homeDir, 'Library', 'Application Support', 'Google', 'Chrome Canary');
112
+ }
113
+ else if (channel === 'beta') {
114
+ chromeDataPath = path.join(homeDir, 'Library', 'Application Support', 'Google', 'Chrome Beta');
115
+ }
116
+ else if (channel === 'dev') {
117
+ chromeDataPath = path.join(homeDir, 'Library', 'Application Support', 'Google', 'Chrome Dev');
118
+ }
119
+ return chromeDataPath;
120
+ }
121
+ else if (platform === 'win32') {
122
+ // Windows
123
+ let chromeDataPath = path.join(homeDir, 'AppData', 'Local', 'Google', 'Chrome', 'User Data');
124
+ if (channel === 'canary') {
125
+ chromeDataPath = path.join(homeDir, 'AppData', 'Local', 'Google', 'Chrome SxS', 'User Data');
126
+ }
127
+ else if (channel === 'beta') {
128
+ chromeDataPath = path.join(homeDir, 'AppData', 'Local', 'Google', 'Chrome Beta', 'User Data');
129
+ }
130
+ else if (channel === 'dev') {
131
+ chromeDataPath = path.join(homeDir, 'AppData', 'Local', 'Google', 'Chrome Dev', 'User Data');
132
+ }
133
+ return chromeDataPath;
134
+ }
135
+ else {
136
+ // Linux
137
+ let chromeDataPath = path.join(homeDir, '.config', 'google-chrome');
138
+ if (channel === 'canary') {
139
+ chromeDataPath = path.join(homeDir, '.config', 'google-chrome-unstable');
140
+ }
141
+ else if (channel === 'beta') {
142
+ chromeDataPath = path.join(homeDir, '.config', 'google-chrome-beta');
143
+ }
144
+ else if (channel === 'dev') {
145
+ chromeDataPath = path.join(homeDir, '.config', 'google-chrome-unstable');
146
+ }
147
+ // Check if google-chrome exists, fallback to chromium
148
+ if (!fs.existsSync(chromeDataPath)) {
149
+ const chromiumPath = path.join(homeDir, '.config', 'chromium');
150
+ if (fs.existsSync(chromiumPath)) {
151
+ return chromiumPath;
152
+ }
153
+ }
154
+ return chromeDataPath;
155
+ }
156
+ }
157
+ /**
158
+ * Read Local State file to get last used profile
159
+ */
160
+ function readLocalState(userDataDir) {
161
+ const localStatePath = path.join(userDataDir, 'Local State');
162
+ try {
163
+ if (!fs.existsSync(localStatePath)) {
164
+ return {};
165
+ }
166
+ const content = fs.readFileSync(localStatePath, 'utf-8');
167
+ const json = JSON.parse(content);
168
+ const lastUsed = json?.profile?.last_used;
169
+ if (typeof lastUsed === 'string') {
170
+ return { lastUsed };
171
+ }
172
+ }
173
+ catch (error) {
174
+ console.warn(`Failed to read Local State: ${error instanceof Error ? error.message : String(error)}`);
175
+ }
176
+ return {};
177
+ }
178
+ /**
179
+ * Compare version strings (e.g., "2.3.2_0" vs "2.3.1_0")
180
+ */
181
+ function compareVersion(a, b) {
182
+ // Normalize: "2.3.2_0" → [2, 3, 2]
183
+ const normalize = (v) => v.split('_')[0].split('.').map(x => parseInt(x, 10) || 0);
184
+ const aParts = normalize(a);
185
+ const bParts = normalize(b);
186
+ const maxLen = Math.max(aParts.length, bParts.length);
187
+ for (let i = 0; i < maxLen; i++) {
188
+ const diff = (aParts[i] || 0) - (bParts[i] || 0);
189
+ if (diff !== 0)
190
+ return diff;
191
+ }
192
+ return 0;
193
+ }
194
+ /**
195
+ * Scan one profile's Extensions directory and return extension paths
196
+ */
197
+ function scanExtensionsInProfile(profileDir) {
198
+ const extensionPaths = [];
199
+ const extensionsDir = path.join(profileDir, 'Extensions');
200
+ if (!fs.existsSync(extensionsDir)) {
201
+ return extensionPaths;
202
+ }
203
+ try {
204
+ const extensionIds = fs.readdirSync(extensionsDir, { withFileTypes: true });
205
+ for (const extensionEntry of extensionIds) {
206
+ if (!extensionEntry.isDirectory())
207
+ continue;
208
+ const extensionIdPath = path.join(extensionsDir, extensionEntry.name);
209
+ try {
210
+ const versions = fs.readdirSync(extensionIdPath, { withFileTypes: true })
211
+ .filter(e => e.isDirectory())
212
+ .map(e => e.name);
213
+ if (versions.length === 0)
214
+ continue;
215
+ // Find the latest version
216
+ const latestVersion = versions.sort(compareVersion).pop();
217
+ const versionPath = path.join(extensionIdPath, latestVersion);
218
+ const manifestPath = path.join(versionPath, 'manifest.json');
219
+ const manifest = validateExtensionManifest(manifestPath);
220
+ if (manifest) {
221
+ extensionPaths.push(versionPath);
222
+ console.error(` ✅ ${manifest.name} v${manifest.version} (MV${manifest.manifest_version})`);
223
+ }
224
+ }
225
+ catch (error) {
226
+ console.warn(`Error processing extension ${extensionEntry.name}: ${error instanceof Error ? error.message : String(error)}`);
227
+ }
228
+ }
229
+ }
230
+ catch (error) {
231
+ console.error(`Error scanning extensions in ${extensionsDir}: ${error instanceof Error ? error.message : String(error)}`);
232
+ }
233
+ return extensionPaths;
234
+ }
101
235
  /**
102
236
  * Get the Chrome extensions directory path for the current platform
103
237
  */
@@ -171,58 +305,53 @@ function validateExtensionManifest(manifestPath) {
171
305
  }
172
306
  /**
173
307
  * Discover Chrome extensions installed in the system
308
+ * Uses Local State to determine the active profile, or uses specified profile
174
309
  */
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;
310
+ function discoverSystemExtensions(channel, chromeProfile) {
311
+ console.error(`🔍 Discovering system Chrome extensions...`);
312
+ const userDataDir = getSystemChromeUserDataDir(channel);
313
+ console.error(`📁 Chrome User Data: ${userDataDir}`);
314
+ // Determine target profile
315
+ let targetProfile;
316
+ if (chromeProfile) {
317
+ // CLI-specified profile takes priority
318
+ targetProfile = chromeProfile;
319
+ console.error(`🎯 Using CLI-specified profile: ${targetProfile}`);
320
+ }
321
+ else {
322
+ // Read Local State to get last used profile
323
+ const { lastUsed } = readLocalState(userDataDir);
324
+ targetProfile = lastUsed || 'Default';
325
+ if (lastUsed) {
326
+ console.error(`🎯 Using last-used profile from Local State: ${targetProfile}`);
183
327
  }
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
- }
328
+ else {
329
+ console.error(`🎯 Local State not found, using Default profile`);
220
330
  }
221
- console.error(`📦 System extension discovery complete: ${extensionPaths.length} valid extensions found`);
222
331
  }
223
- catch (error) {
224
- console.error(`Error discovering system extensions: ${error instanceof Error ? error.message : String(error)}`);
332
+ // Check if target profile exists
333
+ const profileDir = path.join(userDataDir, targetProfile);
334
+ if (!fs.existsSync(profileDir)) {
335
+ console.warn(`⚠️ Profile directory not found: ${targetProfile}`);
336
+ // Fallback to Default
337
+ if (targetProfile !== 'Default') {
338
+ console.error(`📁 Falling back to Default profile`);
339
+ targetProfile = 'Default';
340
+ const defaultProfileDir = path.join(userDataDir, targetProfile);
341
+ if (!fs.existsSync(defaultProfileDir)) {
342
+ console.error(`❌ Default profile also not found. No extensions will be loaded.`);
343
+ return [];
344
+ }
345
+ }
346
+ else {
347
+ console.error(`❌ Default profile not found. No extensions will be loaded.`);
348
+ return [];
349
+ }
225
350
  }
351
+ // Scan the target profile
352
+ console.error(`📂 Scanning profile: ${targetProfile}`);
353
+ const extensionPaths = scanExtensionsInProfile(path.join(userDataDir, targetProfile));
354
+ console.error(`📦 Total: ${extensionPaths.length} extension(s) found in profile "${targetProfile}"`);
226
355
  return extensionPaths;
227
356
  }
228
357
  // Store development extension paths globally for later retrieval
@@ -231,7 +360,7 @@ export function getDevelopmentExtensionPaths() {
231
360
  return developmentExtensionPaths;
232
361
  }
233
362
  export async function launch(options) {
234
- const { channel, executablePath, customDevTools, headless, isolated, loadExtension, loadExtensionsDir, loadSystemExtensions, } = options;
363
+ const { channel, executablePath, customDevTools, headless, isolated, loadExtension, loadExtensionsDir, loadSystemExtensions, chromeProfile, } = options;
235
364
  // Reset development extension paths
236
365
  developmentExtensionPaths = [];
237
366
  const profileDirName = channel && channel !== 'stable'
@@ -294,7 +423,7 @@ export async function launch(options) {
294
423
  // System extension discovery (default: true unless isolated flag is set)
295
424
  const shouldLoadSystemExtensions = loadSystemExtensions ?? !isolated;
296
425
  if (shouldLoadSystemExtensions) {
297
- const systemExtensions = discoverSystemExtensions(channel);
426
+ const systemExtensions = discoverSystemExtensions(channel, chromeProfile);
298
427
  if (systemExtensions.length > 0) {
299
428
  extensionPaths.push(...systemExtensions);
300
429
  console.error(`✅ Loaded ${systemExtensions.length} system Chrome extension(s)`);
@@ -336,6 +465,13 @@ export async function launch(options) {
336
465
  console.error(` Args: ${JSON.stringify(args, null, 2)}`);
337
466
  console.error(` Ignored Default Args: ["--disable-extensions", "--enable-automation"]`);
338
467
  try {
468
+ // IMPORTANT: Chrome extensions (especially MV3 content scripts and service workers)
469
+ // DO NOT work in headless mode. Always use headless:false when loading extensions.
470
+ // Reference: https://groups.google.com/a/chromium.org/g/headless-dev/c/nEoeUkoNI0o/m/9KZ4Os46AQAJ
471
+ const effectiveHeadless = extensionPaths.length > 0 ? false : headless;
472
+ if (extensionPaths.length > 0 && headless) {
473
+ console.warn('⚠️ WARNING: Extensions require headful mode. Forcing headless:false');
474
+ }
339
475
  const browser = await puppeteer.launch({
340
476
  ...connectOptions,
341
477
  channel: puppeterChannel,
@@ -343,9 +479,14 @@ export async function launch(options) {
343
479
  defaultViewport: null,
344
480
  userDataDir,
345
481
  pipe: true,
346
- headless,
482
+ headless: effectiveHeadless,
347
483
  args,
348
484
  ignoreDefaultArgs: ['--disable-extensions', '--enable-automation'],
485
+ // enableExtensions: Provide unpacked extension folder paths
486
+ // These paths can point to System Chrome's extension directories
487
+ // (e.g., ~/Library/.../Chrome/Default/Extensions/<id>/<version>/)
488
+ // No need to copy extensions to CfT profile
489
+ // Reference: https://pptr.dev/guides/chrome-extensions
349
490
  enableExtensions: extensionPaths.length > 0 ? extensionPaths : undefined,
350
491
  });
351
492
  // Log actual spawn args for debugging
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.9",
3
+ "version": "0.9.11",
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",