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.
- package/README.md +363 -188
- package/build/src/browser.js +50 -51
- package/build/src/tools/bookmarks.js +7 -30
- package/package.json +1 -1
- package/build/src/tools/webstore-auto-screenshot.js +0 -159
- package/build/src/tools/webstore-submission.js +0 -332
|
@@ -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
|
-
});
|