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.
- package/build/src/browser.js +1 -0
- package/build/src/cli.js +5 -0
- package/build/src/main.js +33 -1
- package/build/src/profile-resolver.js +17 -0
- package/build/src/roots-manager.js +171 -0
- package/package.json +1 -1
package/build/src/browser.js
CHANGED
|
@@ -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.
|
|
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",
|