chrome-devtools-mcp-for-extension 0.15.0 → 0.15.1

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.
@@ -444,6 +444,7 @@ export async function launch(options) {
444
444
  console.error(`[profiles] Using: ${userDataDir}`);
445
445
  console.error(` Reason: ${resolved.reason}`);
446
446
  console.error(` Project: ${resolved.projectName} (${resolved.hash})`);
447
+ console.error(` Client: ${resolved.clientId}`);
447
448
  if (resolved.reason === 'AUTO') {
448
449
  console.error(` Root: ${process.cwd()}`);
449
450
  }
@@ -546,15 +547,17 @@ export async function launch(options) {
546
547
  console.error(` Headless: ${headless}`);
547
548
  console.error(` Args: ${JSON.stringify(args, null, 2)}`);
548
549
  console.error(` Ignored Default Args: ["--disable-extensions", "--enable-automation"]`);
550
+ // IMPORTANT: Chrome extensions (especially MV3 content scripts and service workers)
551
+ // DO NOT work in headless mode. Always use headless:false when loading extensions.
552
+ // Reference: https://groups.google.com/a/chromium.org/g/headless-dev/c/nEoeUkoNI0o/m/9KZ4Os46AQAJ
553
+ const effectiveHeadless = extensionPaths.length > 0 ? false : headless;
554
+ if (extensionPaths.length > 0 && headless) {
555
+ console.warn('⚠️ WARNING: Extensions require headful mode. Forcing headless:false');
556
+ }
557
+ let browser;
558
+ let finalUserDataDir = userDataDir;
549
559
  try {
550
- // IMPORTANT: Chrome extensions (especially MV3 content scripts and service workers)
551
- // DO NOT work in headless mode. Always use headless:false when loading extensions.
552
- // Reference: https://groups.google.com/a/chromium.org/g/headless-dev/c/nEoeUkoNI0o/m/9KZ4Os46AQAJ
553
- const effectiveHeadless = extensionPaths.length > 0 ? false : headless;
554
- if (extensionPaths.length > 0 && headless) {
555
- console.warn('⚠️ WARNING: Extensions require headful mode. Forcing headless:false');
556
- }
557
- const browser = await puppeteer.launch({
560
+ browser = await puppeteer.launch({
558
561
  ...connectOptions,
559
562
  channel: puppeterChannel,
560
563
  executablePath: effectiveExecutablePath,
@@ -565,6 +568,50 @@ export async function launch(options) {
565
568
  args,
566
569
  ignoreDefaultArgs: ['--disable-extensions', '--enable-automation'],
567
570
  });
571
+ }
572
+ catch (e) {
573
+ // Profile lock collision fallback (v0.15.1)
574
+ const errorMsg = String(e.message || '').toLowerCase();
575
+ const isProfileLocked = errorMsg.includes('in use') ||
576
+ errorMsg.includes('lock') ||
577
+ errorMsg.includes('another chrome') ||
578
+ errorMsg.includes('profile appears to be');
579
+ if (isProfileLocked) {
580
+ // Fallback to ephemeral session
581
+ const sessionId = `${process.pid}-${Date.now()}`;
582
+ const tempPath = path.join(os.homedir(), '.cache', 'chrome-devtools-mcp', 'sessions', sessionId, channel || 'stable');
583
+ await fs.promises.mkdir(tempPath, { recursive: true });
584
+ console.error(`⚠️ Profile locked: ${userDataDir}`);
585
+ console.error(`📁 Falling back to ephemeral session: ${tempPath}`);
586
+ console.error(`💡 To avoid this, set MCP_CLIENT_ID (e.g., "claude-code", "codex")`);
587
+ // Clean up on exit
588
+ process.on('exit', () => {
589
+ try {
590
+ fs.rmSync(tempPath, { recursive: true, force: true });
591
+ }
592
+ catch {
593
+ /* ignore */
594
+ }
595
+ });
596
+ finalUserDataDir = tempPath;
597
+ // Retry with ephemeral profile
598
+ browser = await puppeteer.launch({
599
+ ...connectOptions,
600
+ channel: puppeterChannel,
601
+ executablePath: effectiveExecutablePath,
602
+ defaultViewport: null,
603
+ userDataDir: tempPath,
604
+ pipe: true,
605
+ headless: effectiveHeadless,
606
+ args,
607
+ ignoreDefaultArgs: ['--disable-extensions', '--enable-automation'],
608
+ });
609
+ }
610
+ else {
611
+ throw e;
612
+ }
613
+ }
614
+ try {
568
615
  // Log actual spawn args for debugging
569
616
  const spawnArgs = browser.process()?.spawnargs;
570
617
  if (spawnArgs) {
@@ -1,11 +1,12 @@
1
1
  // src/profile-resolver.ts
2
- // Phase 1 (v0.15.0)
2
+ // Phase 1 (v0.15.0) + v0.15.1 (MCP_CLIENT_ID support)
3
3
  // - Hybrid priority (CLI > MCP_USER_DATA_DIR > MCP_PROJECT_ID > AUTO > DEFAULT)
4
4
  // - Auto-detection (git root -> nearest package.json -> cwd)
5
5
  // - Realpath normalization
6
6
  // - Tilde (~) expansion
7
7
  // - Short SHA-256 hash (8 chars)
8
8
  // - CI detection => ephemeral session profile (unless MCP_PERSIST_PROFILES)
9
+ // - Client ID isolation (MCP_CLIENT_ID environment variable)
9
10
  // - Minimal console.error() logging of decision
10
11
  import fs from 'node:fs';
11
12
  import os from 'node:os';
@@ -16,6 +17,7 @@ const CACHE_ROOT = path.join(os.homedir(), '.cache', 'chrome-devtools-mcp');
16
17
  // --- Public API ---
17
18
  export function resolveUserDataDir(opts) {
18
19
  const channel = opts.channel || 'stable';
20
+ const clientId = sanitize(opts.env.MCP_CLIENT_ID || 'default');
19
21
  // 0) CI detection → ephemeral session directory (unless MCP_PERSIST_PROFILES)
20
22
  // - This happens before other priorities to keep CI clean by default.
21
23
  if (isCI(opts.env) && !opts.env.MCP_PERSIST_PROFILES) {
@@ -33,13 +35,14 @@ export function resolveUserDataDir(opts) {
33
35
  const result = {
34
36
  path: tempPath,
35
37
  reason: 'AUTO', // keep enum as specified (no EPHEMERAL type in Phase 1)
36
- projectKey: `session-${sessionId}`,
38
+ projectKey: `session-${sessionId}_${clientId}`,
37
39
  projectName: 'ci-session',
38
40
  hash: sessionId,
41
+ clientId,
39
42
  channel,
40
43
  };
41
44
  // concise decision log (resolver-side)
42
- console.error(`[profiles] resolved(AUTO, ci-ephemeral): ${result.path} (session=${sessionId})`);
45
+ console.error(`[profiles] resolved(AUTO, ci-ephemeral): ${result.path} (session=${sessionId}, client=${clientId})`);
43
46
  return result;
44
47
  }
45
48
  // 1) CLI explicit userDataDir
@@ -48,12 +51,13 @@ export function resolveUserDataDir(opts) {
48
51
  const result = {
49
52
  path: p,
50
53
  reason: 'CLI',
51
- projectKey: stripHomeForKey(p),
54
+ projectKey: `${stripHomeForKey(p)}_${clientId}`,
52
55
  projectName: 'cli',
53
56
  hash: shortHash(p),
57
+ clientId,
54
58
  channel,
55
59
  };
56
- console.error(`[profiles] resolved(CLI): ${result.path}`);
60
+ console.error(`[profiles] resolved(CLI): ${result.path} (client=${clientId})`);
57
61
  return result;
58
62
  }
59
63
  // 2) ENV: MCP_USER_DATA_DIR (full path)
@@ -63,27 +67,30 @@ export function resolveUserDataDir(opts) {
63
67
  const result = {
64
68
  path: p,
65
69
  reason: 'MCP_USER_DATA_DIR',
66
- projectKey: stripHomeForKey(p),
70
+ projectKey: `${stripHomeForKey(p)}_${clientId}`,
67
71
  projectName: 'env',
68
72
  hash: shortHash(p),
73
+ clientId,
69
74
  channel,
70
75
  };
71
- console.error(`[profiles] resolved(MCP_USER_DATA_DIR): ${result.path}`);
76
+ console.error(`[profiles] resolved(MCP_USER_DATA_DIR): ${result.path} (client=${clientId})`);
72
77
  return result;
73
78
  }
74
79
  // 3) ENV: MCP_PROJECT_ID (project-scoped persistent profile)
75
80
  const projectId = sanitize(opts.env.MCP_PROJECT_ID || '');
76
81
  if (projectId) {
77
- const p = projectProfilePath(projectId, channel);
82
+ const key = `${projectId}_${clientId}`;
83
+ const p = projectProfilePath(key, channel);
78
84
  const result = {
79
85
  path: p,
80
86
  reason: 'MCP_PROJECT_ID',
81
- projectKey: projectId,
87
+ projectKey: key,
82
88
  projectName: projectId,
83
89
  hash: shortHash(projectId),
90
+ clientId,
84
91
  channel,
85
92
  };
86
- console.error(`[profiles] resolved(MCP_PROJECT_ID): ${result.path} (projectId=${projectId})`);
93
+ console.error(`[profiles] resolved(MCP_PROJECT_ID): ${result.path} (projectId=${projectId}, client=${clientId})`);
87
94
  return result;
88
95
  }
89
96
  // 4) AUTO: detect by root -> name -> hash
@@ -92,7 +99,7 @@ export function resolveUserDataDir(opts) {
92
99
  const name = detectProjectName(root);
93
100
  const realRoot = realpathSafe(root);
94
101
  const hash = shortHash(realRoot);
95
- const key = `${sanitize(name)}_${hash}`;
102
+ const key = `${sanitize(name)}_${hash}_${clientId}`;
96
103
  const p = projectProfilePath(key, channel);
97
104
  const result = {
98
105
  path: p,
@@ -100,24 +107,26 @@ export function resolveUserDataDir(opts) {
100
107
  projectKey: key,
101
108
  projectName: sanitize(name),
102
109
  hash,
110
+ clientId,
103
111
  channel,
104
112
  };
105
- console.error(`[profiles] resolved(AUTO): ${result.path} (root=${root}, name=${name}, hash=${hash})`);
113
+ console.error(`[profiles] resolved(AUTO): ${result.path} (root=${root}, name=${name}, hash=${hash}, client=${clientId})`);
106
114
  return result;
107
115
  }
108
116
  catch (e) {
109
117
  // 5) DEFAULT fallback
110
- const key = 'project-default';
118
+ const key = `project-default_${clientId}`;
111
119
  const p = projectProfilePath(key, channel);
112
120
  const result = {
113
121
  path: p,
114
122
  reason: 'DEFAULT',
115
123
  projectKey: key,
116
- projectName: key,
124
+ projectName: 'project-default',
117
125
  hash: '00000000',
126
+ clientId,
118
127
  channel,
119
128
  };
120
- console.error(`[profiles] resolved(DEFAULT): ${result.path} (reason=${e?.message || 'fallback'})`);
129
+ console.error(`[profiles] resolved(DEFAULT): ${result.path} (reason=${e?.message || 'fallback'}, client=${clientId})`);
121
130
  return result;
122
131
  }
123
132
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-devtools-mcp-for-extension",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
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",