chrome-devtools-mcp-for-extension 0.18.0 → 0.18.2

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.
@@ -32,8 +32,9 @@ export async function fetchRootsFromClient(server) {
32
32
  }
33
33
  /**
34
34
  * Generate stable profile key from roots and client info
35
+ * Format: projectName_hash (e.g., "my-app_2ca5dbf5")
35
36
  */
36
- export function generateProfileKey(rootsUris, clientName, clientVersion) {
37
+ export function generateProfileKey(rootsUris, clientName, clientVersion, projectName) {
37
38
  // Sort URIs for consistent hashing across multi-root workspaces
38
39
  const sortedUris = [...rootsUris].sort();
39
40
  const keyMaterial = JSON.stringify({
@@ -41,8 +42,13 @@ export function generateProfileKey(rootsUris, clientName, clientVersion) {
41
42
  client: clientName,
42
43
  version: clientVersion,
43
44
  });
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);
45
+ // Use first 8 chars of SHA-256 for stable, collision-resistant key
46
+ const hash = createHash('sha256').update(keyMaterial).digest('hex').slice(0, 8);
47
+ // Include project name for clarity (if available)
48
+ if (projectName) {
49
+ const sanitized = projectName.replace(/[^a-z0-9_-]/gi, '-').toLowerCase();
50
+ return `${sanitized}_${hash}`;
51
+ }
46
52
  return hash;
47
53
  }
48
54
  /**
@@ -88,8 +94,8 @@ export async function resolveRoots(server, fallbackOptions) {
88
94
  const rootsResult = await fetchRootsFromClient(server);
89
95
  if (rootsResult && rootsResult.roots.length > 0) {
90
96
  const rootsUris = rootsResult.roots.map(r => r.uri);
91
- const profileKey = generateProfileKey(rootsUris, clientName, clientVersion);
92
97
  const projectName = extractProjectName(rootsResult.roots);
98
+ const profileKey = generateProfileKey(rootsUris, clientName, clientVersion, projectName);
93
99
  console.error(`[roots] Resolved via roots/list: key=${profileKey}, project=${projectName}, client=${clientName}`);
94
100
  return {
95
101
  profileKey,
@@ -103,8 +109,8 @@ export async function resolveRoots(server, fallbackOptions) {
103
109
  // 2) Fallback: CLI argument --project-root
104
110
  if (fallbackOptions.cliProjectRoot) {
105
111
  const uri = pathToFileUri(fallbackOptions.cliProjectRoot);
106
- const profileKey = generateProfileKey([uri], clientName, clientVersion);
107
112
  const projectName = extractProjectName([{ uri }]);
113
+ const profileKey = generateProfileKey([uri], clientName, clientVersion, projectName);
108
114
  console.error(`[roots] Resolved via --project-root: key=${profileKey}, project=${projectName}`);
109
115
  return {
110
116
  profileKey,
@@ -118,8 +124,8 @@ export async function resolveRoots(server, fallbackOptions) {
118
124
  // 3) Fallback: Environment variable MCP_PROJECT_ROOT
119
125
  if (fallbackOptions.envProjectRoot) {
120
126
  const uri = pathToFileUri(fallbackOptions.envProjectRoot);
121
- const profileKey = generateProfileKey([uri], clientName, clientVersion);
122
127
  const projectName = extractProjectName([{ uri }]);
128
+ const profileKey = generateProfileKey([uri], clientName, clientVersion, projectName);
123
129
  console.error(`[roots] Resolved via MCP_PROJECT_ROOT: key=${profileKey}, project=${projectName}`);
124
130
  return {
125
131
  profileKey,
@@ -133,8 +139,8 @@ export async function resolveRoots(server, fallbackOptions) {
133
139
  // 4) Fallback: AUTO (cwd-based, last resort)
134
140
  if (fallbackOptions.autoCwd) {
135
141
  const uri = pathToFileUri(fallbackOptions.autoCwd);
136
- const profileKey = generateProfileKey([uri], clientName, clientVersion);
137
142
  const projectName = extractProjectName([{ uri }]);
143
+ const profileKey = generateProfileKey([uri], clientName, clientVersion, projectName);
138
144
  console.error(`[roots] Resolved via AUTO (cwd): key=${profileKey}, project=${projectName}`);
139
145
  return {
140
146
  profileKey,
@@ -147,11 +153,12 @@ export async function resolveRoots(server, fallbackOptions) {
147
153
  }
148
154
  // Absolute fallback
149
155
  const fallbackUri = 'file:///unknown';
150
- const profileKey = generateProfileKey([fallbackUri], clientName, clientVersion);
156
+ const fallbackProjectName = 'unknown';
157
+ const profileKey = generateProfileKey([fallbackUri], clientName, clientVersion, fallbackProjectName);
151
158
  console.error('[roots] WARNING: No roots available, using fallback');
152
159
  return {
153
160
  profileKey,
154
- projectName: 'unknown',
161
+ projectName: fallbackProjectName,
155
162
  rootsUris: [fallbackUri],
156
163
  clientName,
157
164
  clientVersion,
@@ -0,0 +1,24 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "lastUpdated": "2025-11-27",
4
+ "description": "Selectors for Gemini web interface",
5
+ "elements": {
6
+ "editor": {
7
+ "css": "div[contenteditable='true']",
8
+ "description": "Main input editor"
9
+ },
10
+ "sendButton": {
11
+ "ax": {
12
+ "ariaLabel": "送信"
13
+ },
14
+ "description": "Send message button"
15
+ },
16
+ "response": {
17
+ "css": "model-response",
18
+ "description": "Response container"
19
+ }
20
+ },
21
+ "placeholders": {
22
+ "normalMode": "プロンプトを入力"
23
+ }
24
+ }
@@ -99,4 +99,36 @@ export function checkPlaceholder(placeholder, mode) {
99
99
  */
100
100
  export function clearCache() {
101
101
  cachedSelectors = null;
102
+ cachedGeminiSelectors = null;
103
+ }
104
+ let cachedGeminiSelectors = null;
105
+ /**
106
+ * Load Gemini selectors from JSON file
107
+ */
108
+ export function loadGeminiSelectors() {
109
+ if (cachedGeminiSelectors) {
110
+ return cachedGeminiSelectors;
111
+ }
112
+ const __filename = fileURLToPath(import.meta.url);
113
+ const __dirname = path.dirname(__filename);
114
+ const selectorsPath = path.join(__dirname, 'gemini.json');
115
+ try {
116
+ const data = fs.readFileSync(selectorsPath, 'utf-8');
117
+ cachedGeminiSelectors = JSON.parse(data);
118
+ return cachedGeminiSelectors;
119
+ }
120
+ catch (error) {
121
+ throw new Error(`Failed to load selectors from ${selectorsPath}: ${error}`);
122
+ }
123
+ }
124
+ /**
125
+ * Get a specific Gemini selector definition
126
+ */
127
+ export function getGeminiSelector(elementName) {
128
+ const selectors = loadGeminiSelectors();
129
+ const selector = selectors.elements[elementName];
130
+ if (!selector) {
131
+ throw new Error(`Selector not found: ${elementName}`);
132
+ }
133
+ return selector;
102
134
  }
@@ -9,6 +9,7 @@ import z from 'zod';
9
9
  import { ToolCategories } from './categories.js';
10
10
  import { defineTool } from './ToolDefinition.js';
11
11
  import { CHATGPT_CONFIG } from '../config.js';
12
+ import { isLoginRequired } from '../login-helper.js';
12
13
  /**
13
14
  * Known important elements in ChatGPT UI
14
15
  */
@@ -216,6 +217,14 @@ export const diagnoseChatgptUi = defineTool({
216
217
  // Navigate to ChatGPT
217
218
  response.appendResponseLine(`📡 Navigating to ${url}...`);
218
219
  await page.goto(url, { waitUntil: 'networkidle0', timeout: 30000 });
220
+ // Check if login is required
221
+ const needsLogin = await isLoginRequired(page);
222
+ if (needsLogin) {
223
+ response.appendResponseLine('\n❌ ChatGPTへのログインが必要です');
224
+ response.appendResponseLine('📱 ブラウザウィンドウでログインしてから再実行してください');
225
+ return;
226
+ }
227
+ response.appendResponseLine('✅ Login check passed');
219
228
  // Wait for page to stabilize
220
229
  response.appendResponseLine(`⏳ Waiting ${waitForLoad}ms for page to stabilize...`);
221
230
  await new Promise(resolve => setTimeout(resolve, waitForLoad));
@@ -0,0 +1,357 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import z from 'zod';
9
+ import { ToolCategories } from './categories.js';
10
+ import { defineTool } from './ToolDefinition.js';
11
+ import { GEMINI_CONFIG } from '../config.js';
12
+ import { isLoginRequired } from '../login-helper.js';
13
+ /**
14
+ * Path to store chat session data
15
+ */
16
+ const CHAT_SESSIONS_FILE = path.join(process.cwd(), 'docs/ask/gemini/.chat-sessions.json');
17
+ /**
18
+ * Load chat sessions from JSON file
19
+ */
20
+ async function loadChatSessions() {
21
+ try {
22
+ const data = await fs.promises.readFile(CHAT_SESSIONS_FILE, 'utf-8');
23
+ return JSON.parse(data);
24
+ }
25
+ catch {
26
+ return {};
27
+ }
28
+ }
29
+ /**
30
+ * Save a chat session for a project
31
+ */
32
+ async function saveChatSession(projectName, session) {
33
+ const sessions = await loadChatSessions();
34
+ if (!sessions[projectName]) {
35
+ sessions[projectName] = [];
36
+ }
37
+ const existingIndex = sessions[projectName].findIndex(s => s.chatId === session.chatId);
38
+ if (existingIndex >= 0) {
39
+ sessions[projectName][existingIndex] = {
40
+ ...sessions[projectName][existingIndex],
41
+ ...session,
42
+ lastUsed: new Date().toISOString(),
43
+ };
44
+ }
45
+ else {
46
+ sessions[projectName].push({
47
+ ...session,
48
+ createdAt: session.createdAt || new Date().toISOString(),
49
+ });
50
+ }
51
+ const dir = path.dirname(CHAT_SESSIONS_FILE);
52
+ await fs.promises.mkdir(dir, { recursive: true });
53
+ await fs.promises.writeFile(CHAT_SESSIONS_FILE, JSON.stringify(sessions, null, 2), 'utf-8');
54
+ }
55
+ /**
56
+ * Sanitize question
57
+ */
58
+ function sanitizeQuestion(text) {
59
+ const passwordPatterns = [
60
+ /password\s*[:=]\s*\S+/gi,
61
+ /パスワード\s*[::=]\s*\S+/gi,
62
+ /pwd\s*[:=]\s*\S+/gi,
63
+ /secret\s*[:=]\s*\S+/gi,
64
+ ];
65
+ let sanitized = text;
66
+ for (const pattern of passwordPatterns) {
67
+ sanitized = sanitized.replace(pattern, '[パスワードは除外されました]');
68
+ }
69
+ return sanitized;
70
+ }
71
+ /**
72
+ * Save conversation log
73
+ */
74
+ async function saveConversationLog(projectName, question, response, metadata) {
75
+ const now = new Date();
76
+ const timestamp = [
77
+ String(now.getFullYear()).slice(2).padStart(2, '0'),
78
+ String(now.getMonth() + 1).padStart(2, '0'),
79
+ String(now.getDate()).padStart(2, '0'),
80
+ '_',
81
+ String(now.getHours()).padStart(2, '0'),
82
+ String(now.getMinutes()).padStart(2, '0'),
83
+ String(now.getSeconds()).padStart(2, '0'),
84
+ ].join('');
85
+ const topicSlug = question
86
+ .substring(0, 50)
87
+ .replace(/[^a-z0-9\u3040-\u309f\u30a0-\u30ff\u4e00-\u9faf]+/gi, '-')
88
+ .toLowerCase()
89
+ .slice(0, 30);
90
+ let logPath;
91
+ if (metadata.chatId) {
92
+ const conversationNum = String(metadata.conversationNumber || 1).padStart(3, '0');
93
+ const filename = `${conversationNum}-${timestamp}-${topicSlug}.md`;
94
+ const logDir = path.join('docs/ask/gemini', metadata.chatId);
95
+ logPath = path.join(process.cwd(), logDir, filename);
96
+ await fs.promises.mkdir(path.join(process.cwd(), logDir), { recursive: true });
97
+ }
98
+ else {
99
+ const filename = `${timestamp}-${projectName}-${topicSlug}.md`;
100
+ const logDir = 'docs/ask/gemini';
101
+ logPath = path.join(process.cwd(), logDir, filename);
102
+ await fs.promises.mkdir(path.dirname(logPath), { recursive: true });
103
+ }
104
+ const content = `# ${topicSlug}
105
+
106
+ ## 📅 メタ情報
107
+ - **日時**: ${now.toLocaleString('ja-JP')}
108
+ - **プロジェクト**: ${projectName}
109
+ - **AIモデル**: ${metadata.model || 'Gemini'}
110
+ ${metadata.chatId ? `- **チャットID**: ${metadata.chatId}\n` : ''}${metadata.conversationNumber ? `- **会話番号**: ${metadata.conversationNumber}\n` : ''}${metadata.chatUrl ? `- **チャットURL**: ${metadata.chatUrl}\n` : ''}
111
+ ## ❓ 質問
112
+
113
+ ${question}
114
+
115
+ ## 💬 回答
116
+
117
+ ${response}
118
+ `;
119
+ await fs.promises.writeFile(logPath, content, 'utf-8');
120
+ return path.relative(process.cwd(), logPath);
121
+ }
122
+ export const askGeminiWeb = defineTool({
123
+ name: 'ask_gemini_web',
124
+ description: `Ask Gemini a question via web browser automation. Conversations are organized by project name and logged to docs/ask/gemini/.`,
125
+ annotations: {
126
+ category: ToolCategories.NAVIGATION_AUTOMATION,
127
+ readOnlyHint: false,
128
+ },
129
+ schema: {
130
+ question: z
131
+ .string()
132
+ .describe('The question to ask Gemini.'),
133
+ projectName: z
134
+ .string()
135
+ .optional()
136
+ .describe('Project name for organizing conversations. Defaults to current working directory name.'),
137
+ createNewChat: z
138
+ .boolean()
139
+ .optional()
140
+ .describe('Force creation of a new chat. Default: false'),
141
+ },
142
+ handler: async (request, response, context) => {
143
+ const { question, projectName, createNewChat = false } = request.params;
144
+ const sanitizedQuestion = sanitizeQuestion(question);
145
+ const project = projectName || path.basename(process.cwd()) || 'unknown-project';
146
+ const page = context.getSelectedPage();
147
+ try {
148
+ response.appendResponseLine('Geminiに接続中...');
149
+ await page.goto(GEMINI_CONFIG.DEFAULT_URL, { waitUntil: 'networkidle2' });
150
+ const needsLogin = await isLoginRequired(page);
151
+ if (needsLogin) {
152
+ response.appendResponseLine('\n❌ Geminiへのログインが必要です');
153
+ response.appendResponseLine('ブラウザでログインしてください。');
154
+ return;
155
+ }
156
+ response.appendResponseLine('✅ ログイン確認完了');
157
+ let isNewChat = false;
158
+ let sessionChatId;
159
+ if (!createNewChat) {
160
+ const sessions = await loadChatSessions();
161
+ const projectSessions = sessions[project] || [];
162
+ if (projectSessions.length > 0) {
163
+ const sortedSessions = [...projectSessions].sort((a, b) => new Date(b.lastUsed).getTime() - new Date(a.lastUsed).getTime());
164
+ const latestSession = sortedSessions[0];
165
+ response.appendResponseLine(`既存のチャットを使用: ${latestSession.url}`);
166
+ await page.goto(latestSession.url, { waitUntil: 'networkidle2' });
167
+ sessionChatId = latestSession.chatId;
168
+ await new Promise((resolve) => setTimeout(resolve, 1000));
169
+ }
170
+ else {
171
+ isNewChat = true;
172
+ }
173
+ }
174
+ else {
175
+ isNewChat = true;
176
+ }
177
+ if (isNewChat) {
178
+ response.appendResponseLine('新規チャットを作成中...');
179
+ await page.goto(GEMINI_CONFIG.BASE_URL + 'app', { waitUntil: 'networkidle2' });
180
+ }
181
+ response.appendResponseLine('質問を送信中...');
182
+ // Input text using the textbox element
183
+ // Gemini uses a textbox with role="textbox" or a div with contenteditable
184
+ const questionSent = await page.evaluate((questionText) => {
185
+ // Try textbox first (Gemini's current implementation)
186
+ const textbox = document.querySelector('[role="textbox"]');
187
+ if (textbox) {
188
+ textbox.focus();
189
+ // Clear existing content
190
+ textbox.innerHTML = '';
191
+ // Insert text
192
+ const p = document.createElement('p');
193
+ p.textContent = questionText;
194
+ textbox.appendChild(p);
195
+ textbox.dispatchEvent(new Event('input', { bubbles: true }));
196
+ return true;
197
+ }
198
+ // Fallback to contenteditable
199
+ const editor = document.querySelector('div[contenteditable="true"]');
200
+ if (editor) {
201
+ editor.innerHTML = '';
202
+ const p = document.createElement('p');
203
+ p.textContent = questionText;
204
+ editor.appendChild(p);
205
+ editor.dispatchEvent(new Event('input', { bubbles: true }));
206
+ return true;
207
+ }
208
+ return false;
209
+ }, sanitizedQuestion);
210
+ if (!questionSent) {
211
+ response.appendResponseLine('❌ 入力欄が見つかりません');
212
+ return;
213
+ }
214
+ await new Promise((resolve) => setTimeout(resolve, 500));
215
+ // Click send button - look for "プロンプトを送信" or similar
216
+ const sent = await page.evaluate(() => {
217
+ const buttons = Array.from(document.querySelectorAll('button'));
218
+ // Primary: look for "プロンプトを送信" button
219
+ let sendButton = buttons.find(b => b.textContent?.includes('プロンプトを送信') ||
220
+ b.textContent?.includes('送信') ||
221
+ b.getAttribute('aria-label')?.includes('送信') ||
222
+ b.getAttribute('aria-label')?.includes('Send'));
223
+ // Fallback: look for send icon
224
+ if (!sendButton) {
225
+ sendButton = buttons.find(b => b.querySelector('mat-icon[data-mat-icon-name="send"]') ||
226
+ b.querySelector('[data-icon="send"]'));
227
+ }
228
+ if (sendButton && !sendButton.disabled) {
229
+ sendButton.click();
230
+ return true;
231
+ }
232
+ return false;
233
+ });
234
+ if (!sent) {
235
+ // Fallback: try Enter key
236
+ await page.keyboard.press('Enter');
237
+ response.appendResponseLine('⚠️ 送信ボタンが見つかりません (Enterキーを試行)');
238
+ }
239
+ response.appendResponseLine('回答を待機中...');
240
+ // Wait for response using actual Gemini UI indicators:
241
+ // - Generating: "回答を停止" button appears, "Gemini が入力中です" text
242
+ // - Complete: "Gemini が回答しました" text appears
243
+ const startTime = Date.now();
244
+ let stableCount = 0;
245
+ let lastResponseText = '';
246
+ while (true) {
247
+ await new Promise((resolve) => setTimeout(resolve, 1500));
248
+ const status = await page.evaluate(() => {
249
+ // Check for generating indicators
250
+ const buttons = Array.from(document.querySelectorAll('button'));
251
+ const stopButton = buttons.find(b => b.textContent?.includes('回答を停止') ||
252
+ b.textContent?.includes('Stop') ||
253
+ b.getAttribute('aria-label')?.includes('Stop'));
254
+ // Check for status text
255
+ const bodyText = document.body.innerText;
256
+ const isTyping = bodyText.includes('Gemini が入力中です') ||
257
+ bodyText.includes('Gemini is typing');
258
+ const isComplete = bodyText.includes('Gemini が回答しました') ||
259
+ bodyText.includes('Gemini has responded');
260
+ const isGenerating = !!stopButton || isTyping;
261
+ // Get the response content from model-response elements
262
+ const modelResponses = Array.from(document.querySelectorAll('model-response'));
263
+ let responseContent = '';
264
+ if (modelResponses.length > 0) {
265
+ // Get the last model response
266
+ const lastResponse = modelResponses[modelResponses.length - 1];
267
+ responseContent = lastResponse.textContent || '';
268
+ }
269
+ // Fallback: get text from main area
270
+ if (!responseContent) {
271
+ const main = document.querySelector('main');
272
+ responseContent = main?.innerText || '';
273
+ }
274
+ return {
275
+ isGenerating,
276
+ isComplete,
277
+ responseContent
278
+ };
279
+ });
280
+ // If explicitly marked as complete, we're done
281
+ if (status.isComplete && !status.isGenerating) {
282
+ break;
283
+ }
284
+ // If not generating and response text is stable for 2 iterations, we're done
285
+ if (!status.isGenerating && status.responseContent === lastResponseText && status.responseContent.length > 0) {
286
+ stableCount++;
287
+ if (stableCount >= 2) {
288
+ break;
289
+ }
290
+ }
291
+ else {
292
+ stableCount = 0;
293
+ }
294
+ lastResponseText = status.responseContent;
295
+ if (Date.now() - startTime > 180000) { // 3 mins timeout
296
+ response.appendResponseLine('⚠️ タイムアウト(3分)');
297
+ break;
298
+ }
299
+ }
300
+ // Get the final response content
301
+ const responseText = await page.evaluate(() => {
302
+ // Get content from model-response elements
303
+ const modelResponses = Array.from(document.querySelectorAll('model-response'));
304
+ if (modelResponses.length > 0) {
305
+ // Get the last model response
306
+ const lastResponse = modelResponses[modelResponses.length - 1];
307
+ return lastResponse.textContent?.trim() || '';
308
+ }
309
+ // Fallback: get text from main area
310
+ const main = document.querySelector('main');
311
+ return main?.innerText.slice(-5000) || '';
312
+ });
313
+ response.appendResponseLine('✅ 回答完了');
314
+ // Save session
315
+ if (isNewChat) {
316
+ const chatUrl = page.url();
317
+ const chatIdMatch = chatUrl.match(/\/app\/([a-f0-9]+)/);
318
+ const chatId = chatIdMatch ? chatIdMatch[1] : 'unknown-' + Date.now();
319
+ await saveChatSession(project, {
320
+ chatId,
321
+ url: chatUrl,
322
+ lastUsed: new Date().toISOString(),
323
+ createdAt: new Date().toISOString(),
324
+ title: `[Project: ${project}]`,
325
+ conversationCount: 1,
326
+ });
327
+ sessionChatId = chatId;
328
+ }
329
+ // Save log
330
+ const logPath = await saveConversationLog(project, sanitizedQuestion, responseText, {
331
+ chatUrl: page.url(),
332
+ chatId: sessionChatId,
333
+ });
334
+ response.appendResponseLine(`📝 会話ログ保存: ${logPath}`);
335
+ }
336
+ catch (error) {
337
+ const errorMessage = error instanceof Error ? error.message : String(error);
338
+ response.appendResponseLine(`❌ エラー: ${errorMessage}`);
339
+ // Error snapshot
340
+ try {
341
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
342
+ const debugDir = path.join(process.cwd(), 'docs/ask/gemini/debug');
343
+ await fs.promises.mkdir(debugDir, { recursive: true });
344
+ const screenshotPath = path.join(debugDir, `error-${timestamp}.png`);
345
+ await page.screenshot({ path: screenshotPath });
346
+ response.appendResponseLine(`📸 エラー時のスクリーンショット: ${screenshotPath}`);
347
+ const htmlPath = path.join(debugDir, `error-${timestamp}.html`);
348
+ const html = await page.content();
349
+ await fs.promises.writeFile(htmlPath, html, 'utf-8');
350
+ response.appendResponseLine(`📄 エラー時のHTML: ${htmlPath}`);
351
+ }
352
+ catch (snapshotError) {
353
+ console.error('Failed to capture error snapshot:', snapshotError);
354
+ }
355
+ }
356
+ },
357
+ });
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "chrome-devtools-mcp-for-extension",
3
- "version": "0.18.0",
3
+ "version": "0.18.2",
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
- "bin": "./build/src/index.js",
6
+ "bin": "./scripts/cli.mjs",
7
7
  "main": "index.js",
8
8
  "scripts": {
9
9
  "build": "tsc && node --experimental-strip-types --no-warnings=ExperimentalWarning scripts/post-build.ts",
10
+ "dev": "MCP_ENV=development node scripts/mcp-wrapper.mjs",
10
11
  "typecheck": "tsc --noEmit",
11
12
  "format": "eslint --cache --fix . && prettier --write --cache .",
12
13
  "check-format": "eslint --cache . && prettier --check --cache .;",
@@ -68,15 +69,16 @@
68
69
  "@types/yargs": "^17.0.33",
69
70
  "@typescript-eslint/eslint-plugin": "^8.43.0",
70
71
  "@typescript-eslint/parser": "^8.43.0",
72
+ "chokidar": "^4.0.3",
71
73
  "chrome-devtools-frontend": "1.0.1520535",
72
74
  "eslint": "^9.35.0",
73
- "eslint-plugin-import": "^2.32.0",
74
75
  "eslint-import-resolver-typescript": "^4.4.4",
76
+ "eslint-plugin-import": "^2.32.0",
75
77
  "globals": "^16.4.0",
76
78
  "prettier": "^3.6.2",
77
79
  "sinon": "^21.0.0",
78
- "typescript-eslint": "^8.43.0",
79
- "typescript": "^5.9.2"
80
+ "typescript": "^5.9.2",
81
+ "typescript-eslint": "^8.43.0"
80
82
  },
81
83
  "engines": {
82
84
  "node": ">=22.12.0"
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI Entry Point for chrome-devtools-mcp-for-extension
4
+ *
5
+ * This is the entry point when users run:
6
+ * npx chrome-devtools-mcp-for-extension
7
+ * chrome-devtools-mcp-for-extension (if globally installed)
8
+ *
9
+ * Launches the MCP server with browser globals mock:
10
+ * - Loads browser-globals-mock.mjs BEFORE main.js
11
+ * - Ensures chrome-devtools-frontend modules work in Node.js
12
+ * - Simple execution: no wrapper, no hot-reload
13
+ */
14
+
15
+ import { spawn } from "node:child_process";
16
+ import process from "node:process";
17
+ import { fileURLToPath } from "node:url";
18
+ import path from "node:path";
19
+
20
+ // Resolve paths
21
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
22
+ const mockPath = path.join(__dirname, "browser-globals-mock.mjs");
23
+ const mainPath = path.join(__dirname, "..", "build", "src", "main.js");
24
+
25
+ // Launch MCP server with --import flag
26
+ const child = spawn(process.execPath, [
27
+ "--import", mockPath,
28
+ mainPath,
29
+ ...process.argv.slice(2) // Forward CLI arguments
30
+ ], {
31
+ stdio: "inherit",
32
+ env: process.env,
33
+ });
34
+
35
+ child.on("exit", (code, signal) => {
36
+ if (signal) {
37
+ process.exit(1);
38
+ }
39
+ process.exit(code ?? 0);
40
+ });
41
+
42
+ // Forward signals
43
+ process.on("SIGTERM", () => child?.kill("SIGTERM"));
44
+ process.on("SIGINT", () => child?.kill("SIGINT"));