chrome-devtools-mcp-for-extension 0.6.3 → 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.
package/README.md CHANGED
@@ -25,19 +25,6 @@ AI-powered Chrome extension development with automated testing, debugging, and W
25
25
 
26
26
  ---
27
27
 
28
- ## 📊 How It Compares
29
-
30
- | Feature | This Tool | Puppeteer/Playwright | Original chrome-devtools-mcp |
31
- |---------|-----------|----------------------|------------------------------|
32
- | Extension Support | ✅ Always enabled | ❌ Disabled by default | ⚠️ Manual config required |
33
- | Setup Required | ❌ None | ✅ Complex config files | ✅ Multiple flags needed |
34
- | Real User Profile | ✅ Direct access | ❌ Temporary profiles | ⚠️ Optional |
35
- | Profile Copying | ❌ No copying needed | ⚠️ Manual setup | ⚠️ Manual setup |
36
- | Web Store Automation | ✅ Built-in | ❌ None | ❌ None |
37
- | Extension Debugging | ✅ Service worker + console | ⚠️ Limited | ❌ None |
38
-
39
- ---
40
-
41
28
  ## 🚀 Quick Start
42
29
 
43
30
  ### 1. Install (30 seconds)
@@ -47,14 +34,10 @@ AI-powered Chrome extension development with automated testing, debugging, and W
47
34
  claude mcp add --scope user chrome-devtools-extension npx chrome-devtools-mcp-for-extension@latest
48
35
  ```
49
36
 
50
- <details>
51
37
  <summary>Other MCP clients (Cursor, VS Code Copilot, Cline)</summary>
52
38
 
53
39
  Add to your MCP configuration file:
54
40
 
55
- **Cursor**: `~/.cursor/extensions_config.json`
56
- **VS Code Copilot**: `.vscode/settings.json`
57
-
58
41
  ```json
59
42
  {
60
43
  "mcpServers": {
@@ -66,7 +49,6 @@ Add to your MCP configuration file:
66
49
  }
67
50
  ```
68
51
 
69
- </details>
70
52
 
71
53
  ### 2. Restart your AI client
72
54
 
@@ -79,39 +61,6 @@ See [Common Workflows](#-common-workflows) below for typical use cases
79
61
 
80
62
  ---
81
63
 
82
- ## 💡 See The Difference
83
-
84
- ### Traditional Approach (Puppeteer)
85
- ```javascript
86
- // ❌ Complex 15+ line setup
87
- const browser = await puppeteer.launch({
88
- headless: false,
89
- args: [
90
- '--disable-extensions-except=/path/to/your/extension',
91
- '--load-extension=/path/to/your/extension',
92
- '--user-data-dir=/tmp/test-profile',
93
- // ... 10+ more flags
94
- ],
95
- ignoreDefaultArgs: ['--disable-extensions'],
96
- });
97
- // Still doesn't use your real Chrome environment!
98
- ```
99
-
100
- ### Zero-Config Approach (This Tool)
101
- ```bash
102
- # ✅ One command
103
- claude mcp add chrome-devtools-extension npx chrome-devtools-mcp-for-extension@latest
104
-
105
- # Then just ask:
106
- "Test my extension on youtube.com"
107
- "Debug why my content script isn't working"
108
- "Submit my extension to Chrome Web Store"
109
-
110
- # That's it! Uses your actual Chrome with all your extensions
111
- ```
112
-
113
- ---
114
-
115
64
  ## ✨ Core Capabilities
116
65
 
117
66
  - 🧩 **Extension Development**: Load, debug, and reload Chrome extensions during development
@@ -166,6 +115,21 @@ Quick reference for the 3 core extension tools:
166
115
  | `reload_extension` | Hot-reload during development | "Reload my-extension" |
167
116
  | `inspect_service_worker` | Debug background scripts | "Debug service worker for my-extension" |
168
117
 
118
+
119
+ ---
120
+
121
+ ## 📊 How It Compares
122
+
123
+ | Feature | This Tool | Puppeteer/Playwright | Original chrome-devtools-mcp |
124
+ |---------|-----------|----------------------|------------------------------|
125
+ | Extension Support | ✅ Always enabled | ❌ Disabled by default | ⚠️ Manual config required |
126
+ | Setup Required | ❌ None | ✅ Complex config files | ✅ Multiple flags needed |
127
+ | Real User Profile | ✅ Direct access | ❌ Temporary profiles | ⚠️ Optional |
128
+ | Profile Copying | ❌ No copying needed | ⚠️ Manual setup | ⚠️ Manual setup |
129
+ | Web Store Automation | ✅ Built-in | ❌ None | ❌ None |
130
+ | Extension Debugging | ✅ Service worker + console | ⚠️ Limited | ❌ None |
131
+
132
+ ---
169
133
  <details>
170
134
  <summary>📖 Detailed Tool Documentation</summary>
171
135
 
@@ -481,9 +445,27 @@ interface ManifestValidation {
481
445
 
482
446
  ## Profile Lock Conflicts
483
447
 
484
- **Error: "User Data Directory is already in use"**
485
- - Close regular Chrome before starting MCP server
486
- - Or use `--isolated` flag for separate profile
448
+ **Error: "Chrome is already using this profile"**
449
+
450
+ This error occurs when Chrome is already running with the system profile. The MCP server detects profile lock files and fails immediately to prevent conflicts.
451
+
452
+ **Solution:**
453
+ 1. **Close all Chrome windows** - The simplest solution is to completely quit Chrome before starting the MCP server
454
+ 2. **Use isolated profile** - Run with `--isolated` flag to use a temporary profile:
455
+ ```bash
456
+ npx chrome-devtools-mcp-for-extension@latest --isolated
457
+ ```
458
+ 3. **Use custom profile** - Specify a custom profile directory:
459
+ ```bash
460
+ npx chrome-devtools-mcp-for-extension@latest --userDataDir=/path/to/custom/profile
461
+ ```
462
+
463
+ **Why This Happens:**
464
+ - Chrome creates lock files (`SingletonLock`, `SingletonCookie`, `SingletonSocket`) when a profile is in use
465
+ - Multiple Chrome instances cannot share the same profile simultaneously
466
+ - The MCP server now detects these locks early and fails fast with a clear error message
467
+
468
+ **Note:** Previous versions silently fell back to a temporary profile, which caused confusion. Version 0.6.4+ fails immediately with a clear error instead.
487
469
 
488
470
  </details>
489
471
 
@@ -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.3",
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
- });
@@ -1,332 +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 archiver from 'archiver';
9
- import z from 'zod';
10
- import { ToolCategories } from './categories.js';
11
- import { defineTool } from './ToolDefinition.js';
12
- // Main submission tool - now with browser automation!
13
- export const submitToWebStore = defineTool({
14
- name: 'submit_to_webstore',
15
- description: `Automatically submit a Chrome extension to the Web Store using browser automation`,
16
- annotations: {
17
- category: ToolCategories.EXTENSION_DEVELOPMENT,
18
- readOnlyHint: false,
19
- },
20
- schema: {
21
- extensionPath: z.string().describe('Path to the extension directory'),
22
- autoSubmit: z.boolean().optional().default(false).describe('Automatically submit via browser (requires login)'),
23
- },
24
- handler: async (request, response, context) => {
25
- const { extensionPath } = request.params;
26
- const outputPath = path.join(path.dirname(extensionPath), `${path.basename(extensionPath)}-submission.zip`);
27
- response.appendResponseLine('🚀 **Chrome Web Store Submission Process**');
28
- response.appendResponseLine('='.repeat(40));
29
- response.appendResponseLine('');
30
- // Step 1: Validate manifest
31
- response.appendResponseLine('**Step 1: Validating manifest.json...**');
32
- const manifestPath = path.join(extensionPath, 'manifest.json');
33
- let manifest;
34
- let manifestValid = true;
35
- try {
36
- if (!fs.existsSync(manifestPath)) {
37
- response.appendResponseLine('❌ manifest.json not found');
38
- return;
39
- }
40
- const manifestContent = fs.readFileSync(manifestPath, 'utf-8');
41
- manifest = JSON.parse(manifestContent);
42
- // Validation checks
43
- const errors = [];
44
- const warnings = [];
45
- const suggestions = [];
46
- // Required fields
47
- if (manifest.manifest_version !== 3) {
48
- errors.push('Must use Manifest V3 (manifest_version: 3)');
49
- manifestValid = false;
50
- }
51
- if (!manifest.name || manifest.name.length > 45) {
52
- errors.push('Name is required and must be <= 45 characters');
53
- manifestValid = false;
54
- }
55
- if (!manifest.version || !/^\d+\.\d+\.\d+(\.\d+)?$/.test(manifest.version)) {
56
- errors.push('Version must follow format: 1.0.0 or 1.0.0.0');
57
- manifestValid = false;
58
- }
59
- if (!manifest.description || manifest.description.length > 132) {
60
- warnings.push('Description should be provided and <= 132 characters');
61
- }
62
- // Icons
63
- if (!manifest.icons || !manifest.icons['128']) {
64
- warnings.push('Should include 128x128 icon');
65
- }
66
- // Permissions check
67
- const dangerousPermissions = [
68
- 'debugger',
69
- 'devtools',
70
- 'management',
71
- 'privacy',
72
- 'proxy',
73
- 'system.cpu',
74
- 'system.memory',
75
- 'vpnProvider',
76
- ];
77
- const usedDangerousPerms = manifest.permissions?.filter(p => dangerousPermissions.includes(p)) || [];
78
- if (usedDangerousPerms.length > 0) {
79
- warnings.push(`Uses sensitive permissions: ${usedDangerousPerms.join(', ')}`);
80
- }
81
- // Host permissions
82
- if (manifest.host_permissions?.includes('<all_urls>')) {
83
- warnings.push('Using <all_urls> requires strong justification');
84
- }
85
- // Service worker check
86
- if (manifest.background?.service_worker) {
87
- const swPath = path.join(extensionPath, manifest.background.service_worker);
88
- if (!fs.existsSync(swPath)) {
89
- errors.push(`Service worker file not found: ${manifest.background.service_worker}`);
90
- manifestValid = false;
91
- }
92
- }
93
- // Suggestions
94
- if (!manifest.icons?.['16'] || !manifest.icons?.['48']) {
95
- suggestions.push('Consider adding 16x16 and 48x48 icons');
96
- }
97
- // Display results
98
- if (manifestValid) {
99
- response.appendResponseLine('✅ Manifest is valid');
100
- }
101
- else {
102
- response.appendResponseLine('❌ Manifest has errors');
103
- }
104
- if (errors.length > 0) {
105
- response.appendResponseLine('\n**Errors:**');
106
- errors.forEach(err => response.appendResponseLine(`- ❌ ${err}`));
107
- }
108
- if (warnings.length > 0) {
109
- response.appendResponseLine('\n**Warnings:**');
110
- warnings.forEach(warn => response.appendResponseLine(`- ⚠️ ${warn}`));
111
- }
112
- if (suggestions.length > 0) {
113
- response.appendResponseLine('\n**Suggestions:**');
114
- suggestions.forEach(sug => response.appendResponseLine(`- 💡 ${sug}`));
115
- }
116
- }
117
- catch (error) {
118
- response.appendResponseLine(`❌ Failed to parse manifest: ${error}`);
119
- return;
120
- }
121
- if (!manifestValid) {
122
- response.appendResponseLine('');
123
- response.appendResponseLine('❌ **Submission blocked:** Fix manifest errors first');
124
- return;
125
- }
126
- response.appendResponseLine('');
127
- // Step 2: Generate store listing
128
- response.appendResponseLine('**Step 2: Generating store listing...**');
129
- let description = `${manifest.name} is a Chrome extension that ${manifest.description || 'enhances your browsing experience'}.\n\n`;
130
- description += '## Features\n\n';
131
- // Infer features from permissions
132
- if (manifest.permissions?.includes('tabs')) {
133
- description += '• Manage and organize your browser tabs\n';
134
- }
135
- if (manifest.permissions?.includes('storage')) {
136
- description += '• Save your preferences and settings\n';
137
- }
138
- if (manifest.permissions?.includes('notifications')) {
139
- description += '• Receive helpful notifications\n';
140
- }
141
- if (manifest.content_scripts) {
142
- description += '• Enhance website functionality\n';
143
- }
144
- if (manifest.action?.default_popup) {
145
- description += '• Quick access from toolbar\n';
146
- }
147
- // Guess category
148
- const { permissions = [], host_permissions = [] } = manifest;
149
- let category = 'Productivity'; // Default
150
- if (permissions.includes('tabs') || permissions.includes('bookmarks')) {
151
- category = 'Productivity';
152
- }
153
- else if (permissions.includes('downloads')) {
154
- category = 'Tools';
155
- }
156
- else if (host_permissions.some(h => h.includes('youtube') || h.includes('video'))) {
157
- category = 'Entertainment';
158
- }
159
- else if (host_permissions.some(h => h.includes('facebook') || h.includes('twitter'))) {
160
- category = 'Social & Communication';
161
- }
162
- response.appendResponseLine(`**Name:** ${manifest.name}`);
163
- response.appendResponseLine(`**Summary:** ${manifest.description || `${manifest.name} - Chrome Extension`}`);
164
- response.appendResponseLine(`**Category:** ${category}`);
165
- response.appendResponseLine('');
166
- response.appendResponseLine('**Generated Description Preview:**');
167
- response.appendResponseLine(description.substring(0, 200) + '...');
168
- response.appendResponseLine('');
169
- // Step 3: Create submission package
170
- response.appendResponseLine('**Step 3: Creating submission package...**');
171
- try {
172
- await new Promise((resolve, reject) => {
173
- const output = fs.createWriteStream(outputPath);
174
- const archive = archiver('zip', {
175
- zlib: { level: 9 } // Maximum compression
176
- });
177
- output.on('close', () => {
178
- const sizeKB = (archive.pointer() / 1024).toFixed(2);
179
- response.appendResponseLine(`📦 Package created: ${outputPath}`);
180
- response.appendResponseLine(` Size: ${sizeKB} KB`);
181
- resolve();
182
- });
183
- archive.on('error', (err) => {
184
- response.appendResponseLine(`❌ Failed to create package: ${err}`);
185
- reject(err);
186
- });
187
- archive.pipe(output);
188
- // Add extension files, excluding unnecessary ones
189
- archive.glob('**/*', {
190
- cwd: extensionPath,
191
- ignore: [
192
- 'node_modules/**',
193
- '.git/**',
194
- '.gitignore',
195
- '*.map',
196
- '.DS_Store',
197
- 'Thumbs.db',
198
- '*.log',
199
- 'test/**',
200
- 'tests/**',
201
- 'docs/**',
202
- ],
203
- });
204
- archive.finalize();
205
- });
206
- response.appendResponseLine('✅ Package created successfully!');
207
- }
208
- catch (error) {
209
- response.appendResponseLine(`❌ Failed to create package: ${error}`);
210
- return;
211
- }
212
- response.appendResponseLine('');
213
- response.appendResponseLine('='.repeat(40));
214
- response.appendResponseLine('**📋 Final Checklist:**');
215
- response.appendResponseLine('');
216
- response.appendResponseLine('Before submitting to Chrome Web Store:');
217
- response.appendResponseLine('1. ✅ Manifest validated');
218
- response.appendResponseLine('2. ✅ ZIP package created');
219
- response.appendResponseLine('3. ⬜ Add screenshots (1280x800 recommended)');
220
- response.appendResponseLine('4. ⬜ Add promotional images');
221
- response.appendResponseLine('5. ⬜ Write privacy policy (if needed)');
222
- response.appendResponseLine('6. ⬜ Pay $5 developer registration fee (first time only)');
223
- response.appendResponseLine('');
224
- response.appendResponseLine('**Submit at:** https://chrome.google.com/webstore/devconsole');
225
- // Step 4: Auto-submit via browser automation
226
- if (request.params.autoSubmit) {
227
- response.appendResponseLine('');
228
- response.appendResponseLine('='.repeat(40));
229
- response.appendResponseLine('**🤖 Step 4: Automated Browser Submission**');
230
- response.appendResponseLine('');
231
- const page = context.getSelectedPage();
232
- try {
233
- // Navigate to Chrome Web Store Developer Dashboard
234
- response.appendResponseLine('Navigating to Developer Dashboard...');
235
- await page.goto('https://chrome.google.com/webstore/devconsole', {
236
- waitUntil: 'networkidle0',
237
- });
238
- // Check if user is logged in
239
- await new Promise(resolve => setTimeout(resolve, 2000));
240
- const currentUrl = page.url();
241
- if (currentUrl.includes('accounts.google.com')) {
242
- response.appendResponseLine('⚠️ Login required. Please log in manually.');
243
- response.appendResponseLine('After logging in, run this command again.');
244
- return;
245
- }
246
- // Check if this is a new submission or update
247
- response.appendResponseLine('Checking for existing extensions...');
248
- // Look for "Add new item" button
249
- const addNewButton = await page.$('button[aria-label="Add new item"], a[href*="register"]');
250
- if (addNewButton) {
251
- response.appendResponseLine('Creating new extension submission...');
252
- await addNewButton.click();
253
- await page.waitForNavigation({ waitUntil: 'networkidle0' });
254
- }
255
- else {
256
- response.appendResponseLine('❌ Could not find "Add new item" button');
257
- response.appendResponseLine('Please ensure you are on the Developer Dashboard');
258
- return;
259
- }
260
- // Upload the ZIP file
261
- response.appendResponseLine('Uploading extension package...');
262
- const fileInput = await page.$('input[type="file"]');
263
- if (fileInput) {
264
- await fileInput.uploadFile(outputPath);
265
- response.appendResponseLine('✅ Package uploaded');
266
- // Wait for processing
267
- await new Promise(resolve => setTimeout(resolve, 3000));
268
- // Look for any errors
269
- const errorElements = await page.$$('.error-message, [role="alert"]');
270
- if (errorElements.length > 0) {
271
- const errorText = await page.evaluate(() => {
272
- const errors = document.querySelectorAll('.error-message, [role="alert"]');
273
- return Array.from(errors).map(e => e.textContent).join('\n');
274
- });
275
- response.appendResponseLine(`⚠️ Upload errors detected: ${errorText}`);
276
- }
277
- }
278
- else {
279
- response.appendResponseLine('❌ Could not find file upload input');
280
- return;
281
- }
282
- // Fill in store listing information
283
- response.appendResponseLine('Filling in store listing...');
284
- // Title field (usually pre-filled from manifest)
285
- const titleInput = await page.$('input[name="title"], input[aria-label*="title"]');
286
- if (titleInput) {
287
- const currentTitle = await titleInput.evaluate(el => el.value);
288
- if (!currentTitle) {
289
- await titleInput.type(manifest.name);
290
- }
291
- }
292
- // Summary/Short description
293
- const summaryInput = await page.$('textarea[name="summary"], textarea[aria-label*="summary"]');
294
- if (summaryInput) {
295
- await summaryInput.click({ clickCount: 3 }); // Select all
296
- await summaryInput.type(manifest.description || `${manifest.name} - Chrome Extension`);
297
- }
298
- // Detailed description
299
- const descriptionInput = await page.$('textarea[name="description"], textarea[aria-label*="description"]');
300
- if (descriptionInput) {
301
- await descriptionInput.click({ clickCount: 3 });
302
- await descriptionInput.type(description);
303
- }
304
- // Category selection
305
- const categorySelect = await page.$('select[name="category"], select[aria-label*="category"]');
306
- if (categorySelect) {
307
- await categorySelect.select(category.toLowerCase().replace(/\s+/g, '_'));
308
- }
309
- // Language
310
- const languageSelect = await page.$('select[name="language"], select[aria-label*="language"]');
311
- if (languageSelect) {
312
- await languageSelect.select('en');
313
- }
314
- response.appendResponseLine('✅ Store listing filled');
315
- // Screenshots reminder
316
- response.appendResponseLine('');
317
- response.appendResponseLine('⚠️ **Manual steps required:**');
318
- response.appendResponseLine('1. Add at least 1 screenshot (1280x800 or 640x400)');
319
- response.appendResponseLine('2. Add promotional images if needed');
320
- response.appendResponseLine('3. Review all information');
321
- response.appendResponseLine('4. Click "Save draft" or "Submit for review"');
322
- response.appendResponseLine('');
323
- response.appendResponseLine('The browser is now on the submission page.');
324
- response.appendResponseLine('Complete the remaining steps manually.');
325
- }
326
- catch (error) {
327
- response.appendResponseLine(`❌ Automation error: ${error}`);
328
- response.appendResponseLine('You may need to complete the submission manually.');
329
- }
330
- }
331
- },
332
- });