chrome-devtools-mcp-for-extension 0.6.4 → 0.7.0
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 +35 -20
- package/build/src/browser.js +11 -44
- package/build/src/profile-manager.js +150 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -443,29 +443,44 @@ interface ManifestValidation {
|
|
|
443
443
|
- `--load-extension` may be restricted in newer Chrome versions
|
|
444
444
|
- **Solution**: Use system profile (default) instead of `--loadExtension` flag
|
|
445
445
|
|
|
446
|
-
## Profile
|
|
446
|
+
## Dedicated Profile Architecture
|
|
447
447
|
|
|
448
|
-
**
|
|
448
|
+
**How does the MCP server handle Chrome profiles?**
|
|
449
449
|
|
|
450
|
-
|
|
450
|
+
The MCP server uses a **dedicated profile with symlinks** to provide the best of both worlds:
|
|
451
451
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
452
|
+
### What Gets Shared (via Symlinks)
|
|
453
|
+
- ✅ **Extensions**: Your installed Chrome extensions are accessible via symlink
|
|
454
|
+
- ✅ **Bookmarks**: Your bookmarks are shared (read-only)
|
|
455
|
+
|
|
456
|
+
### What Stays Private (Dedicated Profile)
|
|
457
|
+
- 🔒 **Cookies & Login State**: Separate login state for security
|
|
458
|
+
- 🔒 **Browsing History**: Independent history
|
|
459
|
+
- 🔒 **Preferences**: MCP-specific settings
|
|
460
|
+
|
|
461
|
+
### Profile Location
|
|
462
|
+
```
|
|
463
|
+
~/.cache/chrome-devtools-mcp/chrome-profile-dedicated/
|
|
464
|
+
└── Default/
|
|
465
|
+
├── Extensions/ → (symlink to system Chrome)
|
|
466
|
+
├── Bookmarks → (symlink to system Chrome)
|
|
467
|
+
├── Cookies (dedicated)
|
|
468
|
+
└── ...
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### First Launch
|
|
472
|
+
- **Initial setup**: Extensions and bookmarks are automatically linked
|
|
473
|
+
- **Google Login required**: You'll need to log in once (login state is not shared from your main Chrome)
|
|
474
|
+
- **Subsequent launches**: Login state is preserved in the dedicated profile
|
|
475
|
+
|
|
476
|
+
### Concurrent Usage
|
|
477
|
+
✅ **Yes!** The MCP server runs alongside your regular Chrome browser without conflicts. Each uses its own profile directory.
|
|
478
|
+
|
|
479
|
+
### Isolated Mode
|
|
480
|
+
For testing or if you prefer a completely empty profile:
|
|
481
|
+
```bash
|
|
482
|
+
npx chrome-devtools-mcp-for-extension@latest --isolated
|
|
483
|
+
```
|
|
469
484
|
|
|
470
485
|
</details>
|
|
471
486
|
|
package/build/src/browser.js
CHANGED
|
@@ -7,7 +7,7 @@ import fs from 'node:fs';
|
|
|
7
7
|
import os from 'node:os';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import puppeteer from 'puppeteer-core';
|
|
10
|
-
import {
|
|
10
|
+
import { setupDedicatedProfile } from './profile-manager.js';
|
|
11
11
|
let browser;
|
|
12
12
|
const ignoredPrefixes = new Set([
|
|
13
13
|
'chrome://',
|
|
@@ -42,33 +42,6 @@ async function ensureBrowserConnected(browserURL) {
|
|
|
42
42
|
});
|
|
43
43
|
return browser;
|
|
44
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
|
-
}
|
|
72
45
|
function scanExtensionsDirectory(extensionsDir) {
|
|
73
46
|
const extensionPaths = [];
|
|
74
47
|
try {
|
|
@@ -262,27 +235,21 @@ export async function launch(options) {
|
|
|
262
235
|
let usingSystemProfile = false;
|
|
263
236
|
let profileDirectory = 'Default';
|
|
264
237
|
if (!isolated && !userDataDir) {
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
usingSystemProfile =
|
|
271
|
-
// Check if profile is already in use
|
|
272
|
-
assertProfileNotInUse(userDataDir);
|
|
273
|
-
// Detect last used profile directory
|
|
274
|
-
profileDirectory = getLastUsedProfile(userDataDir);
|
|
275
|
-
console.error(`✅ Using system Chrome profile: ${systemProfile.channel}`);
|
|
276
|
-
console.error(` Path: ${userDataDir}`);
|
|
277
|
-
console.error(` Profile Directory: ${profileDirectory}`);
|
|
238
|
+
// Use dedicated profile with symlinks to system extensions and bookmarks
|
|
239
|
+
try {
|
|
240
|
+
const dedicatedProfile = await setupDedicatedProfile(channel);
|
|
241
|
+
userDataDir = dedicatedProfile.userDataDir;
|
|
242
|
+
profileDirectory = dedicatedProfile.profileDirectory;
|
|
243
|
+
usingSystemProfile = false; // Using dedicated profile, not system profile
|
|
278
244
|
}
|
|
279
|
-
|
|
280
|
-
// Fallback to isolated profile if
|
|
245
|
+
catch (error) {
|
|
246
|
+
// Fallback to isolated profile if setup fails
|
|
247
|
+
console.error(`⚠️ Failed to setup dedicated profile: ${error instanceof Error ? error.message : String(error)}`);
|
|
281
248
|
userDataDir = path.join(os.homedir(), '.cache', 'chrome-devtools-mcp', profileDirName);
|
|
282
249
|
await fs.promises.mkdir(userDataDir, {
|
|
283
250
|
recursive: true,
|
|
284
251
|
});
|
|
285
|
-
console.error(`📁 Using isolated profile (
|
|
252
|
+
console.error(`📁 Using isolated profile (fallback)`);
|
|
286
253
|
}
|
|
287
254
|
}
|
|
288
255
|
const args = [
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile Manager for Chrome DevTools MCP
|
|
3
|
+
*
|
|
4
|
+
* This module manages dedicated Chrome profiles that use symlinks to share
|
|
5
|
+
* Extensions and Bookmarks from the system Chrome profile while maintaining
|
|
6
|
+
* isolated Cookies, Login Data, and Preferences.
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import os from 'os';
|
|
11
|
+
/**
|
|
12
|
+
* Detect system Chrome profile for a specific channel
|
|
13
|
+
*/
|
|
14
|
+
function detectSystemChromeProfile(channel) {
|
|
15
|
+
const home = os.homedir();
|
|
16
|
+
const chromePaths = {
|
|
17
|
+
stable: path.join(home, 'Library/Application Support/Google/Chrome'),
|
|
18
|
+
beta: path.join(home, 'Library/Application Support/Google/Chrome Beta'),
|
|
19
|
+
canary: path.join(home, 'Library/Application Support/Google/Chrome Canary'),
|
|
20
|
+
dev: path.join(home, 'Library/Application Support/Google/Chrome Dev'),
|
|
21
|
+
};
|
|
22
|
+
const targetChannel = channel || 'stable';
|
|
23
|
+
const chromePath = chromePaths[targetChannel];
|
|
24
|
+
if (chromePath && fs.existsSync(chromePath)) {
|
|
25
|
+
return { path: chromePath, channel: targetChannel };
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Detect any available system Chrome profile
|
|
31
|
+
*/
|
|
32
|
+
function detectAnySystemChromeProfile() {
|
|
33
|
+
const channels = ['stable', 'beta', 'dev', 'canary'];
|
|
34
|
+
for (const channel of channels) {
|
|
35
|
+
const profile = detectSystemChromeProfile(channel);
|
|
36
|
+
if (profile) {
|
|
37
|
+
return profile;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Get the last used profile directory name from Local State
|
|
44
|
+
*/
|
|
45
|
+
function getLastUsedProfile(userDataDir) {
|
|
46
|
+
const localStatePath = path.join(userDataDir, 'Local State');
|
|
47
|
+
try {
|
|
48
|
+
const localStateContent = fs.readFileSync(localStatePath, 'utf-8');
|
|
49
|
+
const localState = JSON.parse(localStateContent);
|
|
50
|
+
const lastUsed = localState?.profile?.last_used;
|
|
51
|
+
if (lastUsed && typeof lastUsed === 'string') {
|
|
52
|
+
return lastUsed;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
// Ignore errors, will use default
|
|
57
|
+
}
|
|
58
|
+
return 'Default';
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Create or update a symlink safely
|
|
62
|
+
*
|
|
63
|
+
* @param target - The target path that the symlink should point to
|
|
64
|
+
* @param linkPath - The path where the symlink should be created
|
|
65
|
+
*/
|
|
66
|
+
function createSymlinkSafe(target, linkPath) {
|
|
67
|
+
// Check if target exists
|
|
68
|
+
if (!fs.existsSync(target)) {
|
|
69
|
+
console.error(`⚠️ Symlink target does not exist: ${target}`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// If link already exists and points to the correct target, skip
|
|
73
|
+
if (fs.existsSync(linkPath)) {
|
|
74
|
+
try {
|
|
75
|
+
const currentTarget = fs.readlinkSync(linkPath);
|
|
76
|
+
if (currentTarget === target) {
|
|
77
|
+
// Already correctly linked
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// Remove old symlink
|
|
81
|
+
fs.unlinkSync(linkPath);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
// Not a symlink, remove the file/directory
|
|
85
|
+
if (fs.lstatSync(linkPath).isDirectory()) {
|
|
86
|
+
fs.rmSync(linkPath, { recursive: true, force: true });
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
fs.unlinkSync(linkPath);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Create new symlink
|
|
94
|
+
fs.symlinkSync(target, linkPath, 'dir');
|
|
95
|
+
console.error(`🔗 Created symlink: ${path.basename(linkPath)} -> ${target}`);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Setup a dedicated Chrome profile with symlinks to system profile
|
|
99
|
+
*
|
|
100
|
+
* This function:
|
|
101
|
+
* 1. Detects the system Chrome profile
|
|
102
|
+
* 2. Creates a dedicated profile directory
|
|
103
|
+
* 3. Creates symlinks for Extensions and Bookmarks
|
|
104
|
+
* 4. Returns the dedicated profile information
|
|
105
|
+
*
|
|
106
|
+
* @param channel - Chrome channel to use (stable, beta, canary, dev)
|
|
107
|
+
* @returns Dedicated profile information
|
|
108
|
+
*/
|
|
109
|
+
export async function setupDedicatedProfile(channel) {
|
|
110
|
+
// Detect system Chrome profile
|
|
111
|
+
const systemProfile = detectSystemChromeProfile(channel) || detectAnySystemChromeProfile();
|
|
112
|
+
if (!systemProfile) {
|
|
113
|
+
throw new Error('No system Chrome profile found. Please install Chrome and run it at least once.');
|
|
114
|
+
}
|
|
115
|
+
// Get the last used profile directory
|
|
116
|
+
const profileDirectory = getLastUsedProfile(systemProfile.path);
|
|
117
|
+
const systemProfileDir = path.join(systemProfile.path, profileDirectory);
|
|
118
|
+
// Create dedicated profile directory
|
|
119
|
+
const dedicatedUserDataDir = path.join(os.homedir(), '.cache', 'chrome-devtools-mcp', 'chrome-profile-dedicated');
|
|
120
|
+
await fs.promises.mkdir(dedicatedUserDataDir, { recursive: true });
|
|
121
|
+
const dedicatedProfileDir = path.join(dedicatedUserDataDir, profileDirectory);
|
|
122
|
+
await fs.promises.mkdir(dedicatedProfileDir, { recursive: true });
|
|
123
|
+
console.error('📁 Dedicated Profile Setup:');
|
|
124
|
+
console.error(` System Profile: ${systemProfileDir}`);
|
|
125
|
+
console.error(` Dedicated Profile: ${dedicatedProfileDir}`);
|
|
126
|
+
console.error(` Profile Directory: ${profileDirectory}`);
|
|
127
|
+
// Create symlinks for shared resources
|
|
128
|
+
const symlinkTargets = [
|
|
129
|
+
{ name: 'Extensions', required: false },
|
|
130
|
+
{ name: 'Bookmarks', required: false },
|
|
131
|
+
];
|
|
132
|
+
for (const { name, required } of symlinkTargets) {
|
|
133
|
+
const targetPath = path.join(systemProfileDir, name);
|
|
134
|
+
const linkPath = path.join(dedicatedProfileDir, name);
|
|
135
|
+
if (fs.existsSync(targetPath)) {
|
|
136
|
+
createSymlinkSafe(targetPath, linkPath);
|
|
137
|
+
}
|
|
138
|
+
else if (required) {
|
|
139
|
+
console.error(`⚠️ Required item not found: ${name}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
console.error('✅ Dedicated profile setup complete');
|
|
143
|
+
console.error(' First launch will require Google login (login state is not shared)');
|
|
144
|
+
return {
|
|
145
|
+
userDataDir: dedicatedUserDataDir,
|
|
146
|
+
profileDirectory,
|
|
147
|
+
systemProfilePath: systemProfileDir,
|
|
148
|
+
channel: systemProfile.channel,
|
|
149
|
+
};
|
|
150
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-devtools-mcp-for-extension",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
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",
|