chrome-devtools-mcp-for-extension 0.17.0 → 0.18.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.
@@ -425,6 +425,7 @@ export async function launch(options) {
425
425
  env: process.env,
426
426
  cwd: process.cwd(),
427
427
  channel: channel || 'stable',
428
+ rootsInfo: options.rootsInfo, // v0.18.0: Pass Roots info
428
429
  });
429
430
  const userDataDir = resolved.path;
430
431
  await fs.promises.mkdir(userDataDir, { recursive: true });
package/build/src/cli.js CHANGED
@@ -74,6 +74,11 @@ export const cliOptions = {
74
74
  type: 'string',
75
75
  describe: 'Path to a file to write debug logs to. Set the env variable `DEBUG` to `*` to enable verbose logs. Useful for submitting bug reports.',
76
76
  },
77
+ projectRoot: {
78
+ type: 'string',
79
+ description: 'Explicitly specify the project root directory for profile isolation. Overrides MCP roots/list. Useful when roots/list is not available.',
80
+ conflicts: 'browserUrl',
81
+ },
77
82
  };
78
83
  export function parseArguments(version, argv = process.argv) {
79
84
  const yargsInstance = yargs(hideBin(argv))
package/build/src/main.js CHANGED
@@ -8,13 +8,14 @@ import fs from 'node:fs';
8
8
  import path from 'node:path';
9
9
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10
10
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
11
- import { SetLevelRequestSchema } from '@modelcontextprotocol/sdk/types.js';
11
+ import { SetLevelRequestSchema, RootsListChangedNotificationSchema } from '@modelcontextprotocol/sdk/types.js';
12
12
  import { resolveBrowser } from './browser.js';
13
13
  import { parseArguments } from './cli.js';
14
14
  import { logger, saveLogsToFile } from './logger.js';
15
15
  import { McpContext } from './McpContext.js';
16
16
  import { McpResponse } from './McpResponse.js';
17
17
  import { Mutex } from './Mutex.js';
18
+ import { resolveRoots } from './roots-manager.js';
18
19
  import { runStartupCheck } from './startup-check.js';
19
20
  import * as bookmarkTools from './tools/bookmarks.js';
20
21
  import * as chatgptWebTools from './tools/chatgpt-web.js';
@@ -57,9 +58,34 @@ const server = new McpServer({
57
58
  server.server.setRequestHandler(SetLevelRequestSchema, () => {
58
59
  return {};
59
60
  });
61
+ // Handle roots/list_changed notification
62
+ server.server.setNotificationHandler(RootsListChangedNotificationSchema, async () => {
63
+ logger('[roots] Received roots/list_changed notification - roots have changed');
64
+ // Invalidate cached roots info
65
+ cachedRootsInfo = null;
66
+ logger('[roots] Cached roots cleared - will re-fetch on next browser launch');
67
+ });
60
68
  let context;
61
69
  let uiHealthCheckRun = false; // Track if UI health check has been run
70
+ let cachedRootsInfo = null; // Cache roots info
71
+ let initializationComplete = false; // Track if MCP initialization is complete
62
72
  async function getContext() {
73
+ // Wait for initialization to complete before resolving roots
74
+ if (!initializationComplete) {
75
+ logger('[roots] Waiting for MCP initialization to complete...');
76
+ await new Promise(resolve => setTimeout(resolve, 100)); // Brief wait
77
+ if (!initializationComplete) {
78
+ logger('[roots] WARNING: MCP not yet initialized, proceeding without roots');
79
+ }
80
+ }
81
+ // Resolve roots info (cached or fresh)
82
+ if (!cachedRootsInfo) {
83
+ cachedRootsInfo = await resolveRoots(server.server, {
84
+ cliProjectRoot: args.projectRoot,
85
+ envProjectRoot: process.env.MCP_PROJECT_ROOT,
86
+ autoCwd: process.cwd(),
87
+ });
88
+ }
63
89
  const browserOptions = {
64
90
  browserUrl: args.browserUrl,
65
91
  headless: args.headless,
@@ -73,6 +99,7 @@ async function getContext() {
73
99
  chromeProfile: args.chromeProfile,
74
100
  userDataDir: args.userDataDir,
75
101
  logFile,
102
+ rootsInfo: cachedRootsInfo, // Pass roots info to browser
76
103
  };
77
104
  const browser = await resolveBrowser(browserOptions);
78
105
  // Browser factory function for reconnection
@@ -176,6 +203,11 @@ const tools = [
176
203
  for (const tool of tools) {
177
204
  registerTool(tool);
178
205
  }
206
+ // Set initialization callback
207
+ server.server.oninitialized = () => {
208
+ initializationComplete = true;
209
+ logger('[roots] MCP initialization complete');
210
+ };
179
211
  const transport = new StdioServerTransport();
180
212
  await server.connect(transport);
181
213
  logger('Chrome DevTools MCP Server connected');
@@ -19,6 +19,23 @@ const CACHE_ROOT = path.join(os.homedir(), '.cache', 'chrome-devtools-mcp');
19
19
  // --- Public API ---
20
20
  export function resolveUserDataDir(opts) {
21
21
  const channel = opts.channel || 'stable';
22
+ // v0.18.0: PRIORITY 0 - Use Roots info if available
23
+ if (opts.rootsInfo) {
24
+ const profilePath = path.join(CACHE_ROOT, 'profiles', opts.rootsInfo.profileKey, opts.rootsInfo.clientName, channel);
25
+ const normalized = pathNormalize(profilePath);
26
+ const result = {
27
+ path: normalized,
28
+ reason: opts.rootsInfo.source,
29
+ projectKey: opts.rootsInfo.profileKey,
30
+ projectName: opts.rootsInfo.projectName,
31
+ hash: opts.rootsInfo.profileKey.slice(0, 8), // Use first 8 chars as hash
32
+ clientId: opts.rootsInfo.clientName,
33
+ channel,
34
+ };
35
+ console.error(`[profiles] Resolved via Roots (${opts.rootsInfo.source}): ${result.path}`);
36
+ console.error(`[profiles] project=${opts.rootsInfo.projectName}, client=${opts.rootsInfo.clientName}, key=${opts.rootsInfo.profileKey}`);
37
+ return result;
38
+ }
22
39
  // Auto-detect client type from parent process if MCP_CLIENT_ID not set
23
40
  let clientId;
24
41
  if (opts.env.MCP_CLIENT_ID) {
@@ -0,0 +1,171 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ /**
7
+ * Roots Manager
8
+ *
9
+ * Manages MCP roots (project directories) and generates stable profile keys.
10
+ * Implements the MCP Roots protocol for project-scoped Chrome profiles.
11
+ */
12
+ import { createHash } from 'node:crypto';
13
+ /**
14
+ * Fetch roots from MCP client using roots/list protocol
15
+ */
16
+ export async function fetchRootsFromClient(server) {
17
+ const clientCaps = server.getClientCapabilities();
18
+ if (!clientCaps?.roots) {
19
+ console.error('[roots] Client does not support roots capability');
20
+ return null;
21
+ }
22
+ try {
23
+ console.error('[roots] Requesting roots/list from client...');
24
+ const result = await server.listRoots({}, { timeout: 5000 });
25
+ console.error(`[roots] Received ${result.roots.length} roots from client`);
26
+ return result;
27
+ }
28
+ catch (error) {
29
+ console.error(`[roots] Failed to fetch roots from client: ${error instanceof Error ? error.message : String(error)}`);
30
+ return null;
31
+ }
32
+ }
33
+ /**
34
+ * Generate stable profile key from roots and client info
35
+ */
36
+ export function generateProfileKey(rootsUris, clientName, clientVersion) {
37
+ // Sort URIs for consistent hashing across multi-root workspaces
38
+ const sortedUris = [...rootsUris].sort();
39
+ const keyMaterial = JSON.stringify({
40
+ roots: sortedUris,
41
+ client: clientName,
42
+ version: clientVersion,
43
+ });
44
+ // Use first 12 chars of SHA-256 for stable, collision-resistant key
45
+ const hash = createHash('sha256').update(keyMaterial).digest('hex').slice(0, 12);
46
+ return hash;
47
+ }
48
+ /**
49
+ * Extract project name from roots URIs
50
+ */
51
+ export function extractProjectName(roots) {
52
+ if (roots.length === 0) {
53
+ return 'unknown';
54
+ }
55
+ // Prefer explicit name if provided
56
+ const firstRoot = roots[0];
57
+ if (firstRoot.name) {
58
+ return sanitizeProjectName(firstRoot.name);
59
+ }
60
+ // Extract from file:// URI
61
+ try {
62
+ const url = new URL(firstRoot.uri);
63
+ if (url.protocol === 'file:') {
64
+ const pathParts = url.pathname.split('/').filter(Boolean);
65
+ const dirName = pathParts[pathParts.length - 1] || 'root';
66
+ return sanitizeProjectName(dirName);
67
+ }
68
+ }
69
+ catch {
70
+ // Fall through to default
71
+ }
72
+ return 'unknown';
73
+ }
74
+ /**
75
+ * Sanitize project name for use in file paths
76
+ */
77
+ function sanitizeProjectName(name) {
78
+ return name.toLowerCase().replace(/[^a-z0-9-_]/g, '-');
79
+ }
80
+ /**
81
+ * Resolve roots information from MCP client or fallbacks
82
+ */
83
+ export async function resolveRoots(server, fallbackOptions) {
84
+ const clientInfo = server.getClientVersion();
85
+ const clientName = clientInfo?.name || 'unknown-client';
86
+ const clientVersion = clientInfo?.version || '0.0.0';
87
+ // 1) Try roots/list from client (preferred)
88
+ const rootsResult = await fetchRootsFromClient(server);
89
+ if (rootsResult && rootsResult.roots.length > 0) {
90
+ const rootsUris = rootsResult.roots.map(r => r.uri);
91
+ const profileKey = generateProfileKey(rootsUris, clientName, clientVersion);
92
+ const projectName = extractProjectName(rootsResult.roots);
93
+ console.error(`[roots] Resolved via roots/list: key=${profileKey}, project=${projectName}, client=${clientName}`);
94
+ return {
95
+ profileKey,
96
+ projectName,
97
+ rootsUris,
98
+ clientName,
99
+ clientVersion,
100
+ source: 'roots/list',
101
+ };
102
+ }
103
+ // 2) Fallback: CLI argument --project-root
104
+ if (fallbackOptions.cliProjectRoot) {
105
+ const uri = pathToFileUri(fallbackOptions.cliProjectRoot);
106
+ const profileKey = generateProfileKey([uri], clientName, clientVersion);
107
+ const projectName = extractProjectName([{ uri }]);
108
+ console.error(`[roots] Resolved via --project-root: key=${profileKey}, project=${projectName}`);
109
+ return {
110
+ profileKey,
111
+ projectName,
112
+ rootsUris: [uri],
113
+ clientName,
114
+ clientVersion,
115
+ source: '--project-root',
116
+ };
117
+ }
118
+ // 3) Fallback: Environment variable MCP_PROJECT_ROOT
119
+ if (fallbackOptions.envProjectRoot) {
120
+ const uri = pathToFileUri(fallbackOptions.envProjectRoot);
121
+ const profileKey = generateProfileKey([uri], clientName, clientVersion);
122
+ const projectName = extractProjectName([{ uri }]);
123
+ console.error(`[roots] Resolved via MCP_PROJECT_ROOT: key=${profileKey}, project=${projectName}`);
124
+ return {
125
+ profileKey,
126
+ projectName,
127
+ rootsUris: [uri],
128
+ clientName,
129
+ clientVersion,
130
+ source: 'MCP_PROJECT_ROOT',
131
+ };
132
+ }
133
+ // 4) Fallback: AUTO (cwd-based, last resort)
134
+ if (fallbackOptions.autoCwd) {
135
+ const uri = pathToFileUri(fallbackOptions.autoCwd);
136
+ const profileKey = generateProfileKey([uri], clientName, clientVersion);
137
+ const projectName = extractProjectName([{ uri }]);
138
+ console.error(`[roots] Resolved via AUTO (cwd): key=${profileKey}, project=${projectName}`);
139
+ return {
140
+ profileKey,
141
+ projectName,
142
+ rootsUris: [uri],
143
+ clientName,
144
+ clientVersion,
145
+ source: 'AUTO',
146
+ };
147
+ }
148
+ // Absolute fallback
149
+ const fallbackUri = 'file:///unknown';
150
+ const profileKey = generateProfileKey([fallbackUri], clientName, clientVersion);
151
+ console.error('[roots] WARNING: No roots available, using fallback');
152
+ return {
153
+ profileKey,
154
+ projectName: 'unknown',
155
+ rootsUris: [fallbackUri],
156
+ clientName,
157
+ clientVersion,
158
+ source: 'AUTO',
159
+ };
160
+ }
161
+ /**
162
+ * Convert absolute file path to file:// URI
163
+ */
164
+ function pathToFileUri(absPath) {
165
+ // Normalize path and convert to file:// URI
166
+ const normalized = absPath.replace(/\\/g, '/');
167
+ const withoutLeadingSlash = normalized.startsWith('/')
168
+ ? normalized
169
+ : '/' + normalized;
170
+ return `file://${withoutLeadingSlash}`;
171
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-devtools-mcp-for-extension",
3
- "version": "0.17.0",
3
+ "version": "0.18.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",