chrome-devtools-mcp-for-extension 0.9.10 → 0.9.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/src/browser.js +178 -49
- package/build/src/cli.js +5 -0
- package/build/src/main.js +1 -0
- package/package.json +1 -1
package/build/src/browser.js
CHANGED
|
@@ -98,6 +98,140 @@ function scanExtensionsDirectory(extensionsDir) {
|
|
|
98
98
|
}
|
|
99
99
|
return extensionPaths;
|
|
100
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Get System Chrome User Data directory (not Chrome for Testing)
|
|
103
|
+
*/
|
|
104
|
+
function getSystemChromeUserDataDir(channel) {
|
|
105
|
+
const homeDir = os.homedir();
|
|
106
|
+
const platform = os.platform();
|
|
107
|
+
if (platform === 'darwin') {
|
|
108
|
+
// macOS
|
|
109
|
+
let chromeDataPath = path.join(homeDir, 'Library', 'Application Support', 'Google', 'Chrome');
|
|
110
|
+
if (channel === 'canary') {
|
|
111
|
+
chromeDataPath = path.join(homeDir, 'Library', 'Application Support', 'Google', 'Chrome Canary');
|
|
112
|
+
}
|
|
113
|
+
else if (channel === 'beta') {
|
|
114
|
+
chromeDataPath = path.join(homeDir, 'Library', 'Application Support', 'Google', 'Chrome Beta');
|
|
115
|
+
}
|
|
116
|
+
else if (channel === 'dev') {
|
|
117
|
+
chromeDataPath = path.join(homeDir, 'Library', 'Application Support', 'Google', 'Chrome Dev');
|
|
118
|
+
}
|
|
119
|
+
return chromeDataPath;
|
|
120
|
+
}
|
|
121
|
+
else if (platform === 'win32') {
|
|
122
|
+
// Windows
|
|
123
|
+
let chromeDataPath = path.join(homeDir, 'AppData', 'Local', 'Google', 'Chrome', 'User Data');
|
|
124
|
+
if (channel === 'canary') {
|
|
125
|
+
chromeDataPath = path.join(homeDir, 'AppData', 'Local', 'Google', 'Chrome SxS', 'User Data');
|
|
126
|
+
}
|
|
127
|
+
else if (channel === 'beta') {
|
|
128
|
+
chromeDataPath = path.join(homeDir, 'AppData', 'Local', 'Google', 'Chrome Beta', 'User Data');
|
|
129
|
+
}
|
|
130
|
+
else if (channel === 'dev') {
|
|
131
|
+
chromeDataPath = path.join(homeDir, 'AppData', 'Local', 'Google', 'Chrome Dev', 'User Data');
|
|
132
|
+
}
|
|
133
|
+
return chromeDataPath;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
// Linux
|
|
137
|
+
let chromeDataPath = path.join(homeDir, '.config', 'google-chrome');
|
|
138
|
+
if (channel === 'canary') {
|
|
139
|
+
chromeDataPath = path.join(homeDir, '.config', 'google-chrome-unstable');
|
|
140
|
+
}
|
|
141
|
+
else if (channel === 'beta') {
|
|
142
|
+
chromeDataPath = path.join(homeDir, '.config', 'google-chrome-beta');
|
|
143
|
+
}
|
|
144
|
+
else if (channel === 'dev') {
|
|
145
|
+
chromeDataPath = path.join(homeDir, '.config', 'google-chrome-unstable');
|
|
146
|
+
}
|
|
147
|
+
// Check if google-chrome exists, fallback to chromium
|
|
148
|
+
if (!fs.existsSync(chromeDataPath)) {
|
|
149
|
+
const chromiumPath = path.join(homeDir, '.config', 'chromium');
|
|
150
|
+
if (fs.existsSync(chromiumPath)) {
|
|
151
|
+
return chromiumPath;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return chromeDataPath;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Read Local State file to get last used profile
|
|
159
|
+
*/
|
|
160
|
+
function readLocalState(userDataDir) {
|
|
161
|
+
const localStatePath = path.join(userDataDir, 'Local State');
|
|
162
|
+
try {
|
|
163
|
+
if (!fs.existsSync(localStatePath)) {
|
|
164
|
+
return {};
|
|
165
|
+
}
|
|
166
|
+
const content = fs.readFileSync(localStatePath, 'utf-8');
|
|
167
|
+
const json = JSON.parse(content);
|
|
168
|
+
const lastUsed = json?.profile?.last_used;
|
|
169
|
+
if (typeof lastUsed === 'string') {
|
|
170
|
+
return { lastUsed };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
console.warn(`Failed to read Local State: ${error instanceof Error ? error.message : String(error)}`);
|
|
175
|
+
}
|
|
176
|
+
return {};
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Compare version strings (e.g., "2.3.2_0" vs "2.3.1_0")
|
|
180
|
+
*/
|
|
181
|
+
function compareVersion(a, b) {
|
|
182
|
+
// Normalize: "2.3.2_0" → [2, 3, 2]
|
|
183
|
+
const normalize = (v) => v.split('_')[0].split('.').map(x => parseInt(x, 10) || 0);
|
|
184
|
+
const aParts = normalize(a);
|
|
185
|
+
const bParts = normalize(b);
|
|
186
|
+
const maxLen = Math.max(aParts.length, bParts.length);
|
|
187
|
+
for (let i = 0; i < maxLen; i++) {
|
|
188
|
+
const diff = (aParts[i] || 0) - (bParts[i] || 0);
|
|
189
|
+
if (diff !== 0)
|
|
190
|
+
return diff;
|
|
191
|
+
}
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Scan one profile's Extensions directory and return extension paths
|
|
196
|
+
*/
|
|
197
|
+
function scanExtensionsInProfile(profileDir) {
|
|
198
|
+
const extensionPaths = [];
|
|
199
|
+
const extensionsDir = path.join(profileDir, 'Extensions');
|
|
200
|
+
if (!fs.existsSync(extensionsDir)) {
|
|
201
|
+
return extensionPaths;
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
const extensionIds = fs.readdirSync(extensionsDir, { withFileTypes: true });
|
|
205
|
+
for (const extensionEntry of extensionIds) {
|
|
206
|
+
if (!extensionEntry.isDirectory())
|
|
207
|
+
continue;
|
|
208
|
+
const extensionIdPath = path.join(extensionsDir, extensionEntry.name);
|
|
209
|
+
try {
|
|
210
|
+
const versions = fs.readdirSync(extensionIdPath, { withFileTypes: true })
|
|
211
|
+
.filter(e => e.isDirectory())
|
|
212
|
+
.map(e => e.name);
|
|
213
|
+
if (versions.length === 0)
|
|
214
|
+
continue;
|
|
215
|
+
// Find the latest version
|
|
216
|
+
const latestVersion = versions.sort(compareVersion).pop();
|
|
217
|
+
const versionPath = path.join(extensionIdPath, latestVersion);
|
|
218
|
+
const manifestPath = path.join(versionPath, 'manifest.json');
|
|
219
|
+
const manifest = validateExtensionManifest(manifestPath);
|
|
220
|
+
if (manifest) {
|
|
221
|
+
extensionPaths.push(versionPath);
|
|
222
|
+
console.error(` ✅ ${manifest.name} v${manifest.version} (MV${manifest.manifest_version})`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
console.warn(`Error processing extension ${extensionEntry.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
console.error(`Error scanning extensions in ${extensionsDir}: ${error instanceof Error ? error.message : String(error)}`);
|
|
232
|
+
}
|
|
233
|
+
return extensionPaths;
|
|
234
|
+
}
|
|
101
235
|
/**
|
|
102
236
|
* Get the Chrome extensions directory path for the current platform
|
|
103
237
|
*/
|
|
@@ -171,58 +305,53 @@ function validateExtensionManifest(manifestPath) {
|
|
|
171
305
|
}
|
|
172
306
|
/**
|
|
173
307
|
* Discover Chrome extensions installed in the system
|
|
308
|
+
* Uses Local State to determine the active profile, or uses specified profile
|
|
174
309
|
*/
|
|
175
|
-
function discoverSystemExtensions(channel) {
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
console.error(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
310
|
+
function discoverSystemExtensions(channel, chromeProfile) {
|
|
311
|
+
console.error(`🔍 Discovering system Chrome extensions...`);
|
|
312
|
+
const userDataDir = getSystemChromeUserDataDir(channel);
|
|
313
|
+
console.error(`📁 Chrome User Data: ${userDataDir}`);
|
|
314
|
+
// Determine target profile
|
|
315
|
+
let targetProfile;
|
|
316
|
+
if (chromeProfile) {
|
|
317
|
+
// CLI-specified profile takes priority
|
|
318
|
+
targetProfile = chromeProfile;
|
|
319
|
+
console.error(`🎯 Using CLI-specified profile: ${targetProfile}`);
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
// Read Local State to get last used profile
|
|
323
|
+
const { lastUsed } = readLocalState(userDataDir);
|
|
324
|
+
targetProfile = lastUsed || 'Default';
|
|
325
|
+
if (lastUsed) {
|
|
326
|
+
console.error(`🎯 Using last-used profile from Local State: ${targetProfile}`);
|
|
183
327
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if (!extensionEntry.isDirectory())
|
|
187
|
-
continue;
|
|
188
|
-
const extensionIdPath = path.join(extensionsDir, extensionEntry.name);
|
|
189
|
-
try {
|
|
190
|
-
// Each extension ID directory contains version subdirectories
|
|
191
|
-
const versions = fs.readdirSync(extensionIdPath, { withFileTypes: true });
|
|
192
|
-
// Find the latest/most recent version
|
|
193
|
-
let latestVersion = '';
|
|
194
|
-
let latestPath = '';
|
|
195
|
-
for (const versionEntry of versions) {
|
|
196
|
-
if (!versionEntry.isDirectory())
|
|
197
|
-
continue;
|
|
198
|
-
const versionPath = path.join(extensionIdPath, versionEntry.name);
|
|
199
|
-
const manifestPath = path.join(versionPath, 'manifest.json');
|
|
200
|
-
const manifest = validateExtensionManifest(manifestPath);
|
|
201
|
-
if (manifest) {
|
|
202
|
-
// Use the first valid version found (Chrome keeps the latest active)
|
|
203
|
-
if (!latestVersion || versionEntry.name > latestVersion) {
|
|
204
|
-
latestVersion = versionEntry.name;
|
|
205
|
-
latestPath = versionPath;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
if (latestPath && latestVersion) {
|
|
210
|
-
const manifest = validateExtensionManifest(path.join(latestPath, 'manifest.json'));
|
|
211
|
-
if (manifest) {
|
|
212
|
-
extensionPaths.push(latestPath);
|
|
213
|
-
console.error(` ✅ Found: ${manifest.name} v${manifest.version} (Manifest v${manifest.manifest_version})`);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
catch (error) {
|
|
218
|
-
console.warn(`Error processing extension ${extensionEntry.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
219
|
-
}
|
|
328
|
+
else {
|
|
329
|
+
console.error(`🎯 Local State not found, using Default profile`);
|
|
220
330
|
}
|
|
221
|
-
console.error(`📦 System extension discovery complete: ${extensionPaths.length} valid extensions found`);
|
|
222
331
|
}
|
|
223
|
-
|
|
224
|
-
|
|
332
|
+
// Check if target profile exists
|
|
333
|
+
const profileDir = path.join(userDataDir, targetProfile);
|
|
334
|
+
if (!fs.existsSync(profileDir)) {
|
|
335
|
+
console.warn(`⚠️ Profile directory not found: ${targetProfile}`);
|
|
336
|
+
// Fallback to Default
|
|
337
|
+
if (targetProfile !== 'Default') {
|
|
338
|
+
console.error(`📁 Falling back to Default profile`);
|
|
339
|
+
targetProfile = 'Default';
|
|
340
|
+
const defaultProfileDir = path.join(userDataDir, targetProfile);
|
|
341
|
+
if (!fs.existsSync(defaultProfileDir)) {
|
|
342
|
+
console.error(`❌ Default profile also not found. No extensions will be loaded.`);
|
|
343
|
+
return [];
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
console.error(`❌ Default profile not found. No extensions will be loaded.`);
|
|
348
|
+
return [];
|
|
349
|
+
}
|
|
225
350
|
}
|
|
351
|
+
// Scan the target profile
|
|
352
|
+
console.error(`📂 Scanning profile: ${targetProfile}`);
|
|
353
|
+
const extensionPaths = scanExtensionsInProfile(path.join(userDataDir, targetProfile));
|
|
354
|
+
console.error(`📦 Total: ${extensionPaths.length} extension(s) found in profile "${targetProfile}"`);
|
|
226
355
|
return extensionPaths;
|
|
227
356
|
}
|
|
228
357
|
// Store development extension paths globally for later retrieval
|
|
@@ -231,7 +360,7 @@ export function getDevelopmentExtensionPaths() {
|
|
|
231
360
|
return developmentExtensionPaths;
|
|
232
361
|
}
|
|
233
362
|
export async function launch(options) {
|
|
234
|
-
const { channel, executablePath, customDevTools, headless, isolated, loadExtension, loadExtensionsDir, loadSystemExtensions, } = options;
|
|
363
|
+
const { channel, executablePath, customDevTools, headless, isolated, loadExtension, loadExtensionsDir, loadSystemExtensions, chromeProfile, } = options;
|
|
235
364
|
// Reset development extension paths
|
|
236
365
|
developmentExtensionPaths = [];
|
|
237
366
|
const profileDirName = channel && channel !== 'stable'
|
|
@@ -294,7 +423,7 @@ export async function launch(options) {
|
|
|
294
423
|
// System extension discovery (default: true unless isolated flag is set)
|
|
295
424
|
const shouldLoadSystemExtensions = loadSystemExtensions ?? !isolated;
|
|
296
425
|
if (shouldLoadSystemExtensions) {
|
|
297
|
-
const systemExtensions = discoverSystemExtensions(channel);
|
|
426
|
+
const systemExtensions = discoverSystemExtensions(channel, chromeProfile);
|
|
298
427
|
if (systemExtensions.length > 0) {
|
|
299
428
|
extensionPaths.push(...systemExtensions);
|
|
300
429
|
console.error(`✅ Loaded ${systemExtensions.length} system Chrome extension(s)`);
|
package/build/src/cli.js
CHANGED
|
@@ -60,6 +60,11 @@ export const cliOptions = {
|
|
|
60
60
|
default: false,
|
|
61
61
|
conflicts: 'browserUrl',
|
|
62
62
|
},
|
|
63
|
+
chromeProfile: {
|
|
64
|
+
type: 'string',
|
|
65
|
+
description: 'Specify Chrome profile name (e.g., "Default", "Profile 1", "Profile 2"). If not specified, uses last_used from Local State. Only effective when --loadSystemExtensions is true.',
|
|
66
|
+
conflicts: 'browserUrl',
|
|
67
|
+
},
|
|
63
68
|
userDataDir: {
|
|
64
69
|
type: 'string',
|
|
65
70
|
description: 'Specify a custom user data directory for Chrome to use instead of the default. Auto-detected if not specified.',
|
package/build/src/main.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-devtools-mcp-for-extension",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.11",
|
|
4
4
|
"description": "MCP server for Chrome extension development with Web Store automation. Fork of chrome-devtools-mcp with extension-specific tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": "./build/src/index.js",
|