chrome-devtools-mcp-for-extension 0.25.7 → 0.26.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/main.js CHANGED
@@ -46,17 +46,9 @@ import { McpResponse } from './McpResponse.js';
46
46
  import { Mutex } from './Mutex.js';
47
47
  import { setProjectRoot } from './project-root-state.js';
48
48
  import { resolveRoots } from './roots-manager.js';
49
- import * as chatgptWebTools from './tools/chatgpt-web.js';
50
- import * as consoleTools from './tools/console.js';
51
- import * as emulationTools from './tools/emulation.js';
52
- import * as geminiWebTools from './tools/gemini-web.js';
53
- import { click, fill, fillForm } from './tools/input.js';
54
- import * as networkTools from './tools/network.js';
55
- import { pages, navigate } from './tools/pages.js';
56
- import * as performanceTools from './tools/performance.js';
57
- import * as screenshotTools from './tools/screenshot.js';
58
- import * as scriptTools from './tools/script.js';
59
- import * as snapshotTools from './tools/snapshot.js';
49
+ import { ToolRegistry, PluginLoader } from './plugin-api.js';
50
+ import { registerCoreTools, getCoreToolCount } from './tools/core-tools.js';
51
+ import { registerOptionalTools, WEB_LLM_TOOLS_INFO } from './tools/optional-tools.js';
60
52
  function readPackageJson() {
61
53
  const currentDir = import.meta.dirname;
62
54
  const packageJsonPath = path.join(currentDir, '..', '..', 'package.json');
@@ -237,25 +229,33 @@ function registerTool(tool) {
237
229
  }
238
230
  });
239
231
  }
240
- const tools = [
241
- ...Object.values(chatgptWebTools),
242
- ...Object.values(geminiWebTools),
243
- ...Object.values(consoleTools),
244
- ...Object.values(emulationTools),
245
- click,
246
- fill,
247
- fillForm,
248
- ...Object.values(networkTools),
249
- pages,
250
- navigate,
251
- ...Object.values(performanceTools),
252
- ...Object.values(screenshotTools),
253
- ...Object.values(scriptTools),
254
- ...Object.values(snapshotTools),
255
- ];
256
- for (const tool of tools) {
232
+ // v0.26.0: Use ToolRegistry for plugin architecture
233
+ const toolRegistry = new ToolRegistry();
234
+ // Register core tools (stable, site-independent)
235
+ registerCoreTools(toolRegistry);
236
+ logger(`[tools] Registered ${getCoreToolCount()} core tools`);
237
+ // Register optional tools (web-llm, site-dependent)
238
+ const optionalCount = registerOptionalTools(toolRegistry);
239
+ if (optionalCount > 0) {
240
+ logger(`[tools] ${WEB_LLM_TOOLS_INFO.disclaimer}`);
241
+ }
242
+ // Load external plugins from MCP_PLUGINS environment variable
243
+ const pluginList = process.env.MCP_PLUGINS;
244
+ if (pluginList) {
245
+ const pluginLoader = new PluginLoader(toolRegistry, logger);
246
+ const { loaded, failed } = await pluginLoader.loadFromList(pluginList);
247
+ if (loaded.length > 0) {
248
+ logger(`[plugins] Successfully loaded: ${loaded.join(', ')}`);
249
+ }
250
+ if (failed.length > 0) {
251
+ logger(`[plugins] Failed to load: ${failed.join(', ')}`);
252
+ }
253
+ }
254
+ // Register all tools with MCP server
255
+ for (const tool of toolRegistry.getAll()) {
257
256
  registerTool(tool);
258
257
  }
258
+ logger(`[tools] Total registered: ${toolRegistry.size} tools`);
259
259
  // Set initialization callback
260
260
  server.server.oninitialized = () => {
261
261
  initializationComplete = true;
@@ -0,0 +1,189 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ /**
7
+ * Registry for managing MCP tools.
8
+ * Allows dynamic registration and querying of tools.
9
+ */
10
+ export class ToolRegistry {
11
+ tools = new Map();
12
+ categories = new Map();
13
+ /**
14
+ * Register a single tool.
15
+ * @throws Error if a tool with the same name already exists
16
+ */
17
+ register(tool) {
18
+ if (this.tools.has(tool.name)) {
19
+ throw new Error(`Tool "${tool.name}" is already registered`);
20
+ }
21
+ this.tools.set(tool.name, tool);
22
+ // Track by category
23
+ const category = tool.annotations.category;
24
+ if (!this.categories.has(category)) {
25
+ this.categories.set(category, new Set());
26
+ }
27
+ this.categories.get(category).add(tool.name);
28
+ }
29
+ /**
30
+ * Register multiple tools at once.
31
+ */
32
+ registerBatch(tools) {
33
+ for (const tool of tools) {
34
+ this.register(tool);
35
+ }
36
+ }
37
+ /**
38
+ * Get all registered tools.
39
+ */
40
+ getAll() {
41
+ return Array.from(this.tools.values());
42
+ }
43
+ /**
44
+ * Get tools by category.
45
+ */
46
+ getByCategory(category) {
47
+ const names = this.categories.get(category);
48
+ if (!names)
49
+ return [];
50
+ return Array.from(names)
51
+ .map((name) => this.tools.get(name))
52
+ .filter(Boolean);
53
+ }
54
+ /**
55
+ * Get a specific tool by name.
56
+ */
57
+ get(name) {
58
+ return this.tools.get(name);
59
+ }
60
+ /**
61
+ * Check if a tool is registered.
62
+ */
63
+ has(name) {
64
+ return this.tools.has(name);
65
+ }
66
+ /**
67
+ * Get count of registered tools.
68
+ */
69
+ get size() {
70
+ return this.tools.size;
71
+ }
72
+ /**
73
+ * Unregister a tool by name.
74
+ * Returns true if the tool was removed, false if it didn't exist.
75
+ */
76
+ unregister(name) {
77
+ const tool = this.tools.get(name);
78
+ if (!tool)
79
+ return false;
80
+ this.tools.delete(name);
81
+ const category = tool.annotations.category;
82
+ this.categories.get(category)?.delete(name);
83
+ return true;
84
+ }
85
+ /**
86
+ * Clear all registered tools.
87
+ */
88
+ clear() {
89
+ this.tools.clear();
90
+ this.categories.clear();
91
+ }
92
+ }
93
+ /**
94
+ * Plugin loader for dynamically loading plugins.
95
+ */
96
+ export class PluginLoader {
97
+ plugins = new Map();
98
+ registry;
99
+ log;
100
+ config;
101
+ constructor(registry, log = console.error, config = {}) {
102
+ this.registry = registry;
103
+ this.log = log;
104
+ this.config = config;
105
+ }
106
+ /**
107
+ * Load a plugin from a module path or package name.
108
+ * @param moduleId - Path to module or npm package name
109
+ */
110
+ async load(moduleId) {
111
+ try {
112
+ this.log(`[plugins] Loading plugin: ${moduleId}`);
113
+ // Dynamic import
114
+ const module = await import(moduleId);
115
+ const plugin = module.default || module.plugin || module;
116
+ if (!plugin.id || !plugin.register) {
117
+ this.log(`[plugins] Invalid plugin (missing id or register): ${moduleId}`);
118
+ return false;
119
+ }
120
+ if (this.plugins.has(plugin.id)) {
121
+ this.log(`[plugins] Plugin already loaded: ${plugin.id}`);
122
+ return false;
123
+ }
124
+ // Create plugin context
125
+ const ctx = {
126
+ registry: this.registry,
127
+ log: (msg) => this.log(`[${plugin.id}] ${msg}`),
128
+ config: this.config,
129
+ };
130
+ // Register the plugin
131
+ await plugin.register(ctx);
132
+ this.plugins.set(plugin.id, plugin);
133
+ this.log(`[plugins] Loaded: ${plugin.name} v${plugin.version} (${plugin.id})`);
134
+ return true;
135
+ }
136
+ catch (error) {
137
+ this.log(`[plugins] Failed to load ${moduleId}: ${error instanceof Error ? error.message : String(error)}`);
138
+ return false;
139
+ }
140
+ }
141
+ /**
142
+ * Load multiple plugins from environment variable or config.
143
+ * @param pluginIds - Comma-separated list of plugin module IDs
144
+ */
145
+ async loadFromList(pluginIds) {
146
+ const ids = pluginIds
147
+ .split(',')
148
+ .map((id) => id.trim())
149
+ .filter(Boolean);
150
+ const loaded = [];
151
+ const failed = [];
152
+ for (const id of ids) {
153
+ const success = await this.load(id);
154
+ if (success) {
155
+ loaded.push(id);
156
+ }
157
+ else {
158
+ failed.push(id);
159
+ }
160
+ }
161
+ return { loaded, failed };
162
+ }
163
+ /**
164
+ * Unload a plugin by ID.
165
+ */
166
+ async unload(pluginId) {
167
+ const plugin = this.plugins.get(pluginId);
168
+ if (!plugin)
169
+ return false;
170
+ try {
171
+ if (plugin.unload) {
172
+ await plugin.unload();
173
+ }
174
+ this.plugins.delete(pluginId);
175
+ this.log(`[plugins] Unloaded: ${pluginId}`);
176
+ return true;
177
+ }
178
+ catch (error) {
179
+ this.log(`[plugins] Failed to unload ${pluginId}: ${error instanceof Error ? error.message : String(error)}`);
180
+ return false;
181
+ }
182
+ }
183
+ /**
184
+ * Get list of loaded plugins.
185
+ */
186
+ getLoaded() {
187
+ return Array.from(this.plugins.values());
188
+ }
189
+ }
@@ -18,6 +18,7 @@ import crypto from 'node:crypto';
18
18
  import fs from 'node:fs';
19
19
  import os from 'node:os';
20
20
  import path from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
21
22
  import { detectClientType } from './client-detector.js';
22
23
  import { detectProjectName, detectProjectRoot } from './project-detector.js';
23
24
  import { getProjectRoot } from './project-root-state.js';
@@ -30,49 +31,84 @@ export function resolveUserDataDir(opts) {
30
31
  if (opts.rootsInfo) {
31
32
  const stableProfilePath = path.join(CACHE_ROOT, 'profiles', opts.rootsInfo.profileKey, opts.rootsInfo.clientName, channel);
32
33
  const normalized = pathNormalize(stableProfilePath);
33
- // v0.25.6: Migration from legacy profile formats to stable identity profile
34
- // Supports: (1) v0.25.4以前のURI+clientベースハッシュ, (2) ディレクトリパスベースハッシュ
34
+ // v0.25.8: Comprehensive migration from legacy profile formats to stable identity profile
35
+ // Handles:
36
+ // - v0.18.x: URI+client+version hash, directory name as project name
37
+ // - v0.19.0-v0.25.4: URI+client hash (no version), directory name as project name
38
+ // - v0.25.5+: stable identity hash, package.json name as project name
35
39
  if (!fs.existsSync(stableProfilePath) && opts.rootsInfo.rootsUris.length > 0) {
36
40
  try {
37
41
  const firstUri = opts.rootsInfo.rootsUris[0];
38
42
  const url = new URL(firstUri);
39
43
  if (url.protocol === 'file:') {
40
- const rootPath = url.pathname;
44
+ // Extract directory name for OLD project name (v0.25.4以前)
45
+ const rootPath = fileURLToPath(url);
41
46
  const realRoot = realpathSafe(rootPath);
42
- // Try multiple legacy hash formats
47
+ const dirName = path.basename(realRoot);
48
+ const oldProjectName = sanitize(dirName); // e.g., "adlogger" from "adLogger"
49
+ const newProjectName = opts.rootsInfo.projectName; // e.g., "adblocker" from package.json
50
+ // Try both old and new project names
51
+ const projectNamesToTry = [oldProjectName];
52
+ if (newProjectName !== oldProjectName) {
53
+ projectNamesToTry.push(newProjectName);
54
+ }
55
+ // Legacy hash formats to try
56
+ const sortedUris = [...opts.rootsInfo.rootsUris].sort();
57
+ const clientName = opts.rootsInfo.clientName;
58
+ const clientVersion = opts.rootsInfo.clientVersion;
43
59
  const legacyHashFormats = [
44
- // Format 1: v0.25.4以前 roots-manager.ts (URI + client JSON hash)
60
+ // Format 1: v0.19.0-v0.25.4 (URI+client, no version)
45
61
  {
46
62
  name: 'uri+client',
47
- hash: (() => {
48
- const sortedUris = [firstUri].sort();
49
- const keyMaterial = JSON.stringify({
50
- roots: sortedUris,
51
- client: opts.rootsInfo.clientName,
52
- });
53
- return crypto.createHash('sha256').update(keyMaterial).digest('hex').slice(0, 8);
54
- })(),
63
+ hash: crypto
64
+ .createHash('sha256')
65
+ .update(JSON.stringify({
66
+ roots: sortedUris,
67
+ client: clientName,
68
+ }))
69
+ .digest('hex')
70
+ .slice(0, 8),
55
71
  },
56
- // Format 2: Directory path hash (shortHash)
72
+ // Format 2: v0.18.x (URI+client+version) - try common version strings
73
+ ...['1.0.0', '0.1.0', clientVersion].map((version) => ({
74
+ name: `uri+client+version(${version})`,
75
+ hash: crypto
76
+ .createHash('sha256')
77
+ .update(JSON.stringify({
78
+ roots: sortedUris,
79
+ client: clientName,
80
+ version,
81
+ }))
82
+ .digest('hex')
83
+ .slice(0, 8),
84
+ })),
85
+ // Format 3: Directory path hash
57
86
  {
58
87
  name: 'directory-path',
59
88
  hash: shortHash(realRoot),
60
89
  },
61
90
  ];
62
- for (const format of legacyHashFormats) {
63
- const legacyKey = `${opts.rootsInfo.projectName}_${format.hash}`;
64
- const legacyPath = path.join(CACHE_ROOT, 'profiles', legacyKey, opts.rootsInfo.clientName, channel);
65
- if (fs.existsSync(legacyPath)) {
66
- console.error(`[profiles] Migration: Found legacy profile (${format.name}): ${legacyPath}`);
67
- console.error(`[profiles] Migration: Creating symlink to stable profile: ${stableProfilePath}`);
68
- try {
69
- fs.mkdirSync(path.dirname(stableProfilePath), { recursive: true });
70
- fs.symlinkSync(legacyPath, stableProfilePath, 'dir');
71
- console.error(`[profiles] Migration: Symlink created successfully`);
72
- break; // Stop after first successful migration
73
- }
74
- catch (e) {
75
- console.error(`[profiles] Migration: ⚠️ Failed to create symlink: ${e}`);
91
+ // Try all combinations of project name × hash format
92
+ let migrated = false;
93
+ for (const projName of projectNamesToTry) {
94
+ if (migrated)
95
+ break;
96
+ for (const format of legacyHashFormats) {
97
+ const legacyKey = `${projName}_${format.hash}`;
98
+ const legacyPath = path.join(CACHE_ROOT, 'profiles', legacyKey, opts.rootsInfo.clientName, channel);
99
+ if (fs.existsSync(legacyPath)) {
100
+ console.error(`[profiles] Migration: Found legacy profile (${format.name}, project=${projName}): ${legacyPath}`);
101
+ console.error(`[profiles] Migration: Creating symlink to stable profile: ${stableProfilePath}`);
102
+ try {
103
+ fs.mkdirSync(path.dirname(stableProfilePath), { recursive: true });
104
+ fs.symlinkSync(legacyPath, stableProfilePath, 'dir');
105
+ console.error(`[profiles] Migration: ✅ Symlink created successfully`);
106
+ migrated = true;
107
+ break;
108
+ }
109
+ catch (e) {
110
+ console.error(`[profiles] Migration: ⚠️ Failed to create symlink: ${e}`);
111
+ }
76
112
  }
77
113
  }
78
114
  }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ // Input tools
7
+ import { click, hover, fill, drag, fillForm, uploadFile } from './input.js';
8
+ // Navigation tools
9
+ import { pages, navigate, resizePage, handleDialog } from './pages.js';
10
+ // Console tools
11
+ import * as consoleTools from './console.js';
12
+ // Emulation tools
13
+ import * as emulationTools from './emulation.js';
14
+ // Network tools
15
+ import * as networkTools from './network.js';
16
+ // Performance tools
17
+ import * as performanceTools from './performance.js';
18
+ // Screenshot tools
19
+ import * as screenshotTools from './screenshot.js';
20
+ // Script tools
21
+ import * as scriptTools from './script.js';
22
+ // Snapshot tools
23
+ import * as snapshotTools from './snapshot.js';
24
+ /**
25
+ * All core tools as an array.
26
+ */
27
+ export const coreTools = [
28
+ // Input automation
29
+ click,
30
+ hover,
31
+ fill,
32
+ drag,
33
+ fillForm,
34
+ uploadFile,
35
+ // Navigation
36
+ pages,
37
+ navigate,
38
+ resizePage,
39
+ handleDialog,
40
+ // Console
41
+ ...Object.values(consoleTools),
42
+ // Emulation
43
+ ...Object.values(emulationTools),
44
+ // Network
45
+ ...Object.values(networkTools),
46
+ // Performance
47
+ ...Object.values(performanceTools),
48
+ // Screenshot
49
+ ...Object.values(screenshotTools),
50
+ // Script
51
+ ...Object.values(scriptTools),
52
+ // Snapshot
53
+ ...Object.values(snapshotTools),
54
+ ];
55
+ /**
56
+ * Register all core tools with a ToolRegistry.
57
+ */
58
+ export function registerCoreTools(registry) {
59
+ for (const tool of coreTools) {
60
+ // Skip non-tool exports (like constants)
61
+ if (tool && typeof tool === 'object' && 'name' in tool && 'handler' in tool) {
62
+ registry.register(tool);
63
+ }
64
+ }
65
+ }
66
+ /**
67
+ * Get count of core tools.
68
+ */
69
+ export function getCoreToolCount() {
70
+ return coreTools.filter((tool) => tool && typeof tool === 'object' && 'name' in tool && 'handler' in tool).length;
71
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ // ChatGPT web tools
7
+ import * as chatgptWebTools from './chatgpt-web.js';
8
+ // Gemini web tools
9
+ import * as geminiWebTools from './gemini-web.js';
10
+ /**
11
+ * All optional (web-llm) tools as an array.
12
+ */
13
+ export const optionalTools = [
14
+ ...Object.values(chatgptWebTools),
15
+ ...Object.values(geminiWebTools),
16
+ ];
17
+ /**
18
+ * Check if web-llm tools should be loaded.
19
+ * Returns false if MCP_DISABLE_WEB_LLM is set to 'true'.
20
+ */
21
+ export function shouldLoadWebLlmTools() {
22
+ const disable = process.env.MCP_DISABLE_WEB_LLM;
23
+ return disable !== 'true' && disable !== '1';
24
+ }
25
+ /**
26
+ * Register optional tools with a ToolRegistry.
27
+ * Respects MCP_DISABLE_WEB_LLM environment variable.
28
+ */
29
+ export function registerOptionalTools(registry) {
30
+ if (!shouldLoadWebLlmTools()) {
31
+ console.error('[tools] Web-LLM tools disabled via MCP_DISABLE_WEB_LLM');
32
+ return 0;
33
+ }
34
+ let count = 0;
35
+ for (const tool of optionalTools) {
36
+ // Skip non-tool exports (like constants)
37
+ if (tool && typeof tool === 'object' && 'name' in tool && 'handler' in tool) {
38
+ try {
39
+ registry.register(tool);
40
+ count++;
41
+ }
42
+ catch (error) {
43
+ // Log but don't fail - optional tools should not block startup
44
+ console.error(`[tools] Failed to register optional tool: ${error instanceof Error ? error.message : String(error)}`);
45
+ }
46
+ }
47
+ }
48
+ if (count > 0) {
49
+ console.error(`[tools] Loaded ${count} optional web-llm tools (experimental, may break)`);
50
+ }
51
+ return count;
52
+ }
53
+ /**
54
+ * Get count of optional tools.
55
+ */
56
+ export function getOptionalToolCount() {
57
+ return optionalTools.filter((tool) => tool && typeof tool === 'object' && 'name' in tool && 'handler' in tool).length;
58
+ }
59
+ /**
60
+ * Metadata about optional tools for documentation.
61
+ */
62
+ export const WEB_LLM_TOOLS_INFO = {
63
+ disclaimer: 'Web-LLM tools (ask_chatgpt_web, ask_gemini_web) are experimental and best-effort. ' +
64
+ 'They depend on specific website UIs and may break when those UIs change. ' +
65
+ 'For production use, consider using official APIs instead.',
66
+ disableEnvVar: 'MCP_DISABLE_WEB_LLM',
67
+ tools: ['ask_chatgpt_web', 'ask_gemini_web'],
68
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-devtools-mcp-for-extension",
3
- "version": "0.25.7",
3
+ "version": "0.26.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": "./scripts/cli.mjs",