chrome-devtools-mcp-for-extension 0.6.2 → 0.6.4

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.
@@ -4,7 +4,6 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  import fs from 'node:fs';
7
- import { promises as fsPromises } from 'node:fs';
8
7
  import os from 'node:os';
9
8
  import path from 'node:path';
10
9
  import puppeteer from 'puppeteer-core';
@@ -43,6 +42,33 @@ async function ensureBrowserConnected(browserURL) {
43
42
  });
44
43
  return browser;
45
44
  }
45
+ /**
46
+ * Get the last used Chrome profile directory name from Local State
47
+ */
48
+ function getLastUsedProfile(userDataDir) {
49
+ const localStatePath = path.join(userDataDir, 'Local State');
50
+ try {
51
+ const localStateContent = fs.readFileSync(localStatePath, 'utf8');
52
+ const localState = JSON.parse(localStateContent);
53
+ return localState?.profile?.last_used || 'Default';
54
+ }
55
+ catch (error) {
56
+ console.warn(`Could not read Local State: ${error instanceof Error ? error.message : String(error)}`);
57
+ return 'Default';
58
+ }
59
+ }
60
+ /**
61
+ * Check if Chrome profile is already in use (locked)
62
+ * Throws an error if the profile is locked
63
+ */
64
+ function assertProfileNotInUse(userDataDir) {
65
+ const lockFiles = ['SingletonLock', 'SingletonCookie', 'SingletonSocket'];
66
+ const hasLock = lockFiles.some(lockFile => fs.existsSync(path.join(userDataDir, lockFile)));
67
+ if (hasLock) {
68
+ throw new Error(`Chrome is already using this profile: ${userDataDir}\n` +
69
+ `Please close Chrome and try again.`);
70
+ }
71
+ }
46
72
  function scanExtensionsDirectory(extensionsDir) {
47
73
  const extensionPaths = [];
48
74
  try {
@@ -234,6 +260,7 @@ export async function launch(options) {
234
260
  : 'chrome-profile';
235
261
  let userDataDir = options.userDataDir;
236
262
  let usingSystemProfile = false;
263
+ let profileDirectory = 'Default';
237
264
  if (!isolated && !userDataDir) {
238
265
  // Try to use system Chrome profile directly
239
266
  const systemProfile = detectSystemChromeProfile(channel) || detectAnySystemChromeProfile();
@@ -241,9 +268,13 @@ export async function launch(options) {
241
268
  // Use system profile directly for better user experience
242
269
  userDataDir = systemProfile.path;
243
270
  usingSystemProfile = true;
271
+ // Check if profile is already in use
272
+ assertProfileNotInUse(userDataDir);
273
+ // Detect last used profile directory
274
+ profileDirectory = getLastUsedProfile(userDataDir);
244
275
  console.error(`✅ Using system Chrome profile: ${systemProfile.channel}`);
245
276
  console.error(` Path: ${userDataDir}`);
246
- console.error(` ⚠️ Note: Close regular Chrome if you encounter issues`);
277
+ console.error(` Profile Directory: ${profileDirectory}`);
247
278
  }
248
279
  else {
249
280
  // Fallback to isolated profile if no system Chrome found
@@ -256,7 +287,7 @@ export async function launch(options) {
256
287
  }
257
288
  const args = [
258
289
  '--hide-crash-restore-bubble',
259
- '--profile-directory=Default', // Ensure we use the Default profile
290
+ `--profile-directory=${profileDirectory}`,
260
291
  ];
261
292
  if (customDevTools) {
262
293
  args.push(`--custom-devtools-frontend=file://${customDevTools}`);
@@ -333,6 +364,7 @@ export async function launch(options) {
333
364
  console.error(` Channel: ${puppeterChannel || 'default'}`);
334
365
  console.error(` Executable: ${executablePath || 'auto-detected'}`);
335
366
  console.error(` User Data Dir: ${userDataDir || 'temporary'}`);
367
+ console.error(` Profile Directory: ${profileDirectory}`);
336
368
  console.error(` Profile Type: ${usingSystemProfile ? 'System Profile (auto-detected)' : 'Custom Profile'}`);
337
369
  console.error(` Headless: ${headless}`);
338
370
  console.error(` Args: ${JSON.stringify(args, null, 2)}`);
@@ -349,6 +381,11 @@ export async function launch(options) {
349
381
  args,
350
382
  ignoreDefaultArgs: ['--disable-extensions', '--enable-automation'],
351
383
  });
384
+ // Log actual spawn args for debugging
385
+ const spawnArgs = browser.process()?.spawnargs;
386
+ if (spawnArgs) {
387
+ console.error(`Actual spawn args: ${spawnArgs.join(' ')}`);
388
+ }
352
389
  if (options.logFile) {
353
390
  // FIXME: we are probably subscribing too late to catch startup logs. We
354
391
  // should expose the process earlier or expose the getRecentLogs() getter.
@@ -424,55 +461,17 @@ export async function launch(options) {
424
461
  return browser;
425
462
  }
426
463
  catch (error) {
427
- // When system profile is already in use, create a minimal copy for automation
428
- if (usingSystemProfile &&
429
- (error.message.includes('The browser is already running') ||
430
- error.message.includes('Target closed') ||
431
- error.message.includes('Connection closed'))) {
432
- console.error(`⚠️ Chrome is already running with system profile.`);
433
- console.error(`📋 Creating a temporary copy of system profile for automation...`);
434
- // Create a temporary copy of the system profile
435
- const tempProfileDir = path.join(os.tmpdir(), `chrome-mcp-${Date.now()}`);
436
- try {
437
- // Copy only essential data (bookmarks, extensions) without locking files
438
- await fsPromises.mkdir(tempProfileDir, { recursive: true });
439
- // Copy bookmarks if they exist
440
- const bookmarksPath = path.join(userDataDir, 'Default', 'Bookmarks');
441
- try {
442
- await fsPromises.access(bookmarksPath);
443
- const bookmarksDestDir = path.join(tempProfileDir, 'Default');
444
- await fsPromises.mkdir(bookmarksDestDir, { recursive: true });
445
- await fsPromises.copyFile(bookmarksPath, path.join(bookmarksDestDir, 'Bookmarks'));
446
- console.error(`✅ Bookmarks copied from system profile`);
447
- }
448
- catch {
449
- // Bookmarks don't exist or can't be accessed, skip
450
- }
451
- // Retry with the temporary profile by recursively calling launch
452
- console.error(`🔄 Retrying with temporary profile copy...`);
453
- const retryOptions = { ...options };
454
- retryOptions.userDataDir = tempProfileDir;
455
- return launch(retryOptions);
456
- }
457
- catch (copyError) {
458
- console.error(`❌ Failed to create temporary profile copy: ${copyError}`);
459
- // Fall through to regular error handling
460
- }
461
- }
462
- // For non-system profiles, maintain the blocking behavior
463
- if (!usingSystemProfile &&
464
- userDataDir &&
465
- (error.message.includes('The browser is already running') ||
466
- error.message.includes('Target closed') ||
467
- error.message.includes('Connection closed'))) {
468
- throw new Error(`The browser is already running for custom profile: ${userDataDir}. Use --isolated to run multiple browser instances.`, {
469
- cause: error,
470
- });
471
- }
472
- // If system profile failed for other reasons, suggest fallback options
464
+ // Fail fast with clear error message - no silent fallback
465
+ console.error(`❌ Failed to launch Chrome`);
466
+ console.error(` User Data Dir: ${userDataDir}`);
467
+ console.error(` Profile Directory: ${profileDirectory}`);
468
+ console.error(` Error: ${error instanceof Error ? error.message : String(error)}`);
473
469
  if (usingSystemProfile) {
474
- console.error(`❌ Failed to launch Chrome with system profile: ${userDataDir}`);
475
- console.error(`💡 Suggestion: Try with --isolated flag for temporary profile, or --userDataDir to specify custom location`);
470
+ console.error('');
471
+ console.error('💡 Troubleshooting:');
472
+ console.error(' 1. Close all Chrome windows and try again');
473
+ console.error(' 2. Use --isolated flag to use temporary profile');
474
+ console.error(' 3. Use --userDataDir to specify custom profile location');
476
475
  }
477
476
  throw error;
478
477
  }
@@ -110,63 +110,40 @@ function loadChromeBookmarks() {
110
110
  }
111
111
  }
112
112
  /**
113
- * Get all bookmarks: merged Chrome bookmarks with default fallback
113
+ * Get all bookmarks: returns only default development bookmarks for privacy
114
+ * User's personal Chrome bookmarks are not loaded to protect privacy
114
115
  */
115
116
  function getBookmarks() {
116
- const defaultBookmarks = getDefaultBookmarks();
117
- const chromeBookmarks = loadChromeBookmarks();
118
- // Merge bookmarks: Chrome bookmarks take precedence, but defaults are always included
119
- return {
120
- ...defaultBookmarks,
121
- ...chromeBookmarks
122
- };
117
+ return getDefaultBookmarks();
123
118
  }
124
119
  export const listBookmarks = defineTool({
125
120
  name: 'list_bookmarks',
126
- description: `List all available bookmarks including both Chrome bookmarks from the user's profile and default development bookmarks. Automatically loads bookmarks from Chrome's bookmark file and merges them with predefined development URLs.`,
121
+ description: `List all available bookmarks. Returns hardcoded development bookmarks only. User's personal Chrome bookmarks are not loaded to protect privacy.`,
127
122
  annotations: {
128
123
  category: ToolCategories.NAVIGATION_AUTOMATION,
129
124
  readOnlyHint: true,
130
125
  },
131
126
  schema: {},
132
127
  handler: async (_request, response, _context) => {
133
- const defaultBookmarks = getDefaultBookmarks();
134
- const chromeBookmarks = loadChromeBookmarks();
135
128
  const allBookmarks = getBookmarks();
136
129
  const bookmarkNames = Object.keys(allBookmarks);
137
130
  if (bookmarkNames.length === 0) {
138
131
  response.appendResponseLine('No bookmarks configured.');
139
- response.appendResponseLine('');
140
- response.appendResponseLine('💡 **Tip:** Chrome bookmarks could not be loaded. Check if Chrome is installed and has bookmarks.');
141
132
  return;
142
133
  }
143
- response.appendResponseLine('📚 **Available Bookmarks:**');
144
- response.appendResponseLine('');
145
- // Show loading status
146
- const chromeBookmarkCount = Object.keys(chromeBookmarks).length;
147
- const defaultBookmarkCount = Object.keys(defaultBookmarks).length;
148
- if (chromeBookmarkCount > 0) {
149
- response.appendResponseLine(`✅ Loaded ${chromeBookmarkCount} bookmarks from Chrome profile${chromeBookmarkCount >= 100 ? ' (limited to 100)' : ''}`);
150
- response.appendResponseLine(`📋 ${defaultBookmarkCount} default development bookmarks included`);
151
- }
152
- else {
153
- response.appendResponseLine(`⚠️ Could not load Chrome bookmarks, using ${defaultBookmarkCount} default bookmarks`);
154
- }
134
+ response.appendResponseLine('📚 **Available Development Bookmarks:**');
155
135
  response.appendResponseLine('');
156
136
  bookmarkNames.forEach(name => {
157
137
  const url = allBookmarks[name];
158
- const source = chromeBookmarks[name] ? '🌐' : '🔧';
159
- response.appendResponseLine(`${source} **${name}**: ${url}`);
138
+ response.appendResponseLine(`🔧 **${name}**: ${url}`);
160
139
  });
161
140
  response.appendResponseLine('');
162
- response.appendResponseLine('**Legend:** 🌐 = Chrome bookmark, 🔧 = Default bookmark');
163
- response.appendResponseLine('');
164
141
  response.appendResponseLine(`Use \`navigate_bookmark name="<bookmark_name>"\` to navigate to any of these URLs.`);
165
142
  },
166
143
  });
167
144
  export const navigateBookmark = defineTool({
168
145
  name: 'navigate_bookmark',
169
- description: `Navigate to a bookmark URL from Chrome bookmarks or default development bookmarks. Automatically includes bookmarks from the user's Chrome profile merged with predefined development resources.`,
146
+ description: `Navigate to a bookmark URL from default development bookmarks. User's personal Chrome bookmarks are not loaded to protect privacy.`,
170
147
  annotations: {
171
148
  category: ToolCategories.NAVIGATION_AUTOMATION,
172
149
  readOnlyHint: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-devtools-mcp-for-extension",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
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",
@@ -1,159 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright 2025 Google LLC
4
- * SPDX-License-Identifier: Apache-2.0
5
- */
6
- import fs from 'node:fs';
7
- import path from 'node:path';
8
- import z from 'zod';
9
- import { ToolCategories } from './categories.js';
10
- import { defineTool } from './ToolDefinition.js';
11
- // Tool for generating extension screenshots automatically
12
- export const generateExtensionScreenshots = defineTool({
13
- name: 'generate_extension_screenshots',
14
- description: `Automatically generate screenshots for Chrome Web Store submission`,
15
- annotations: {
16
- category: ToolCategories.EXTENSION_DEVELOPMENT,
17
- readOnlyHint: false,
18
- },
19
- schema: {
20
- extensionPath: z.string().describe('Path to the extension directory'),
21
- extensionId: z.string().optional().describe('Extension ID if already installed'),
22
- },
23
- handler: async (request, response, context) => {
24
- const { extensionPath, extensionId } = request.params;
25
- const screenshotsDir = path.join(path.dirname(extensionPath), 'screenshots');
26
- // Create screenshots directory
27
- if (!fs.existsSync(screenshotsDir)) {
28
- fs.mkdirSync(screenshotsDir, { recursive: true });
29
- }
30
- const page = context.getSelectedPage();
31
- response.appendResponseLine('📸 **Generating Extension Screenshots**');
32
- response.appendResponseLine('='.repeat(40));
33
- response.appendResponseLine('');
34
- try {
35
- // Set viewport to Chrome Web Store recommended size
36
- await page.setViewport({ width: 1280, height: 800 });
37
- // Screenshot 1: Extension in action on a popular website
38
- response.appendResponseLine('1️⃣ Taking screenshot of extension in action...');
39
- // Navigate to a demo page
40
- await page.goto('https://www.example.com', { waitUntil: 'networkidle0' });
41
- // If extension has a popup, try to capture it
42
- if (extensionId) {
43
- // Open extension popup if possible
44
- const extensionUrl = `chrome-extension://${extensionId}/popup.html`;
45
- // Try to open popup in new tab for screenshot
46
- const popupPage = await page.browser().newPage();
47
- await popupPage.setViewport({ width: 1280, height: 800 });
48
- try {
49
- await popupPage.goto(extensionUrl, { waitUntil: 'networkidle0' });
50
- // Take screenshot of popup
51
- const popupScreenshot = await popupPage.screenshot({
52
- type: 'png',
53
- fullPage: false,
54
- });
55
- const popupPath = path.join(screenshotsDir, 'screenshot-1-popup.png');
56
- fs.writeFileSync(popupPath, popupScreenshot);
57
- response.appendResponseLine(`✅ Popup screenshot saved: ${popupPath}`);
58
- await popupPage.close();
59
- }
60
- catch (error) {
61
- response.appendResponseLine(`⚠️ Could not capture popup: ${error}`);
62
- }
63
- }
64
- // Screenshot 2: Extension toolbar icon
65
- response.appendResponseLine('2️⃣ Taking screenshot with extension icon visible...');
66
- // Navigate to chrome://extensions to show the extension
67
- await page.goto('chrome://extensions/', { waitUntil: 'networkidle0' });
68
- await new Promise(resolve => setTimeout(resolve, 1000));
69
- const extensionsScreenshot = await page.screenshot({
70
- type: 'png',
71
- fullPage: false,
72
- });
73
- const extensionsPath = path.join(screenshotsDir, 'screenshot-2-extensions.png');
74
- fs.writeFileSync(extensionsPath, extensionsScreenshot);
75
- response.appendResponseLine(`✅ Extensions page screenshot saved: ${extensionsPath}`);
76
- // Screenshot 3: Options page (if exists)
77
- response.appendResponseLine('3️⃣ Taking screenshot of options page...');
78
- if (extensionId) {
79
- const optionsUrl = `chrome-extension://${extensionId}/options.html`;
80
- try {
81
- await page.goto(optionsUrl, { waitUntil: 'networkidle0' });
82
- const optionsScreenshot = await page.screenshot({
83
- type: 'png',
84
- fullPage: false,
85
- });
86
- const optionsPath = path.join(screenshotsDir, 'screenshot-3-options.png');
87
- fs.writeFileSync(optionsPath, optionsScreenshot);
88
- response.appendResponseLine(`✅ Options page screenshot saved: ${optionsPath}`);
89
- }
90
- catch (error) {
91
- response.appendResponseLine(`⚠️ No options page found`);
92
- }
93
- }
94
- // Screenshot 4: Extension working on a real website
95
- response.appendResponseLine('4️⃣ Taking screenshot on a real website...');
96
- // Navigate to a website where the extension might be useful
97
- await page.goto('https://www.google.com', { waitUntil: 'networkidle0' });
98
- await new Promise(resolve => setTimeout(resolve, 2000));
99
- const websiteScreenshot = await page.screenshot({
100
- type: 'png',
101
- fullPage: false,
102
- });
103
- const websitePath = path.join(screenshotsDir, 'screenshot-4-website.png');
104
- fs.writeFileSync(websitePath, websiteScreenshot);
105
- response.appendResponseLine(`✅ Website screenshot saved: ${websitePath}`);
106
- // Generate promotional images (smaller versions)
107
- response.appendResponseLine('');
108
- response.appendResponseLine('5️⃣ Generating promotional images...');
109
- // Small promo tile: 440x280
110
- await page.setViewport({ width: 440, height: 280 });
111
- await page.goto('chrome://extensions/', { waitUntil: 'networkidle0' });
112
- const smallPromo = await page.screenshot({
113
- type: 'png',
114
- fullPage: false,
115
- });
116
- const smallPromoPath = path.join(screenshotsDir, 'promo-small-440x280.png');
117
- fs.writeFileSync(smallPromoPath, smallPromo);
118
- response.appendResponseLine(`✅ Small promo tile saved: ${smallPromoPath}`);
119
- // Large promo tile: 920x680
120
- await page.setViewport({ width: 920, height: 680 });
121
- const largePromo = await page.screenshot({
122
- type: 'png',
123
- fullPage: false,
124
- });
125
- const largePromoPath = path.join(screenshotsDir, 'promo-large-920x680.png');
126
- fs.writeFileSync(largePromoPath, largePromo);
127
- response.appendResponseLine(`✅ Large promo tile saved: ${largePromoPath}`);
128
- // Marquee promo: 1400x560
129
- await page.setViewport({ width: 1400, height: 560 });
130
- const marqueePromo = await page.screenshot({
131
- type: 'png',
132
- fullPage: false,
133
- });
134
- const marqueePromoPath = path.join(screenshotsDir, 'promo-marquee-1400x560.png');
135
- fs.writeFileSync(marqueePromoPath, marqueePromo);
136
- response.appendResponseLine(`✅ Marquee promo saved: ${marqueePromoPath}`);
137
- response.appendResponseLine('');
138
- response.appendResponseLine('='.repeat(40));
139
- response.appendResponseLine('✅ **Screenshots Generated Successfully!**');
140
- response.appendResponseLine('');
141
- response.appendResponseLine(`📁 Screenshots saved in: ${screenshotsDir}`);
142
- response.appendResponseLine('');
143
- response.appendResponseLine('**Chrome Web Store Requirements:**');
144
- response.appendResponseLine('• Screenshots: 1280x800 or 640x400 (PNG or JPG)');
145
- response.appendResponseLine('• Small promo tile: 440x280');
146
- response.appendResponseLine('• Large promo tile: 920x680');
147
- response.appendResponseLine('• Marquee promo: 1400x560');
148
- response.appendResponseLine('');
149
- response.appendResponseLine('💡 **Tip:** Edit these screenshots to highlight your extension features!');
150
- }
151
- catch (error) {
152
- response.appendResponseLine(`❌ Error generating screenshots: ${error}`);
153
- }
154
- finally {
155
- // Reset viewport
156
- await page.setViewport({ width: 1280, height: 800 });
157
- }
158
- },
159
- });