chrome-devtools-mcp-for-extension 0.6.3 → 0.6.5

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
 
@@ -479,11 +443,16 @@ interface ManifestValidation {
479
443
  - `--load-extension` may be restricted in newer Chrome versions
480
444
  - **Solution**: Use system profile (default) instead of `--loadExtension` flag
481
445
 
482
- ## Profile Lock Conflicts
446
+ ## Concurrent Chrome Usage
447
+
448
+ **Can I use Chrome while the MCP server is running?**
449
+
450
+ Yes! The MCP server can run alongside your regular Chrome browser. Chrome is robust enough to handle concurrent access to the same profile.
483
451
 
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
452
+ **Note:** If you experience any issues with concurrent usage, you can use the `--isolated` flag to run with a separate profile:
453
+ ```bash
454
+ npx chrome-devtools-mcp-for-extension@latest --isolated
455
+ ```
487
456
 
488
457
  </details>
489
458
 
@@ -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,21 @@ 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
+ }
46
60
  function scanExtensionsDirectory(extensionsDir) {
47
61
  const extensionPaths = [];
48
62
  try {
@@ -234,6 +248,7 @@ export async function launch(options) {
234
248
  : 'chrome-profile';
235
249
  let userDataDir = options.userDataDir;
236
250
  let usingSystemProfile = false;
251
+ let profileDirectory = 'Default';
237
252
  if (!isolated && !userDataDir) {
238
253
  // Try to use system Chrome profile directly
239
254
  const systemProfile = detectSystemChromeProfile(channel) || detectAnySystemChromeProfile();
@@ -241,9 +256,11 @@ export async function launch(options) {
241
256
  // Use system profile directly for better user experience
242
257
  userDataDir = systemProfile.path;
243
258
  usingSystemProfile = true;
259
+ // Detect last used profile directory
260
+ profileDirectory = getLastUsedProfile(userDataDir);
244
261
  console.error(`✅ Using system Chrome profile: ${systemProfile.channel}`);
245
262
  console.error(` Path: ${userDataDir}`);
246
- console.error(` ⚠️ Note: Close regular Chrome if you encounter issues`);
263
+ console.error(` Profile Directory: ${profileDirectory}`);
247
264
  }
248
265
  else {
249
266
  // Fallback to isolated profile if no system Chrome found
@@ -256,7 +273,7 @@ export async function launch(options) {
256
273
  }
257
274
  const args = [
258
275
  '--hide-crash-restore-bubble',
259
- '--profile-directory=Default', // Ensure we use the Default profile
276
+ `--profile-directory=${profileDirectory}`,
260
277
  ];
261
278
  if (customDevTools) {
262
279
  args.push(`--custom-devtools-frontend=file://${customDevTools}`);
@@ -333,6 +350,7 @@ export async function launch(options) {
333
350
  console.error(` Channel: ${puppeterChannel || 'default'}`);
334
351
  console.error(` Executable: ${executablePath || 'auto-detected'}`);
335
352
  console.error(` User Data Dir: ${userDataDir || 'temporary'}`);
353
+ console.error(` Profile Directory: ${profileDirectory}`);
336
354
  console.error(` Profile Type: ${usingSystemProfile ? 'System Profile (auto-detected)' : 'Custom Profile'}`);
337
355
  console.error(` Headless: ${headless}`);
338
356
  console.error(` Args: ${JSON.stringify(args, null, 2)}`);
@@ -349,6 +367,11 @@ export async function launch(options) {
349
367
  args,
350
368
  ignoreDefaultArgs: ['--disable-extensions', '--enable-automation'],
351
369
  });
370
+ // Log actual spawn args for debugging
371
+ const spawnArgs = browser.process()?.spawnargs;
372
+ if (spawnArgs) {
373
+ console.error(`Actual spawn args: ${spawnArgs.join(' ')}`);
374
+ }
352
375
  if (options.logFile) {
353
376
  // FIXME: we are probably subscribing too late to catch startup logs. We
354
377
  // should expose the process earlier or expose the getRecentLogs() getter.
@@ -424,55 +447,17 @@ export async function launch(options) {
424
447
  return browser;
425
448
  }
426
449
  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
450
+ // Fail fast with clear error message - no silent fallback
451
+ console.error(`❌ Failed to launch Chrome`);
452
+ console.error(` User Data Dir: ${userDataDir}`);
453
+ console.error(` Profile Directory: ${profileDirectory}`);
454
+ console.error(` Error: ${error instanceof Error ? error.message : String(error)}`);
473
455
  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`);
456
+ console.error('');
457
+ console.error('💡 Troubleshooting:');
458
+ console.error(' 1. Close all Chrome windows and try again');
459
+ console.error(' 2. Use --isolated flag to use temporary profile');
460
+ console.error(' 3. Use --userDataDir to specify custom profile location');
476
461
  }
477
462
  throw error;
478
463
  }
@@ -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.5",
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
- });