chrome-devtools-mcp-for-extension 0.11.0 → 0.12.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
@@ -15,10 +15,12 @@ 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 { runStartupCheck } from './startup-check.js';
18
19
  import * as bookmarkTools from './tools/bookmarks.js';
19
20
  import * as chatgptWebTools from './tools/chatgpt-web.js';
20
21
  import * as deepResearchChatGPTTools from './tools/deep_research_chatgpt.js';
21
22
  import * as consoleTools from './tools/console.js';
23
+ import * as diagnoseUiTools from './tools/diagnose-ui.js';
22
24
  import * as emulationTools from './tools/emulation.js';
23
25
  import * as extensionTools from './tools/extensions.js';
24
26
  import * as inputTools from './tools/input.js';
@@ -56,6 +58,7 @@ server.server.setRequestHandler(SetLevelRequestSchema, () => {
56
58
  return {};
57
59
  });
58
60
  let context;
61
+ let uiHealthCheckRun = false; // Track if UI health check has been run
59
62
  async function getContext() {
60
63
  const browser = await resolveBrowser({
61
64
  browserUrl: args.browserUrl,
@@ -74,6 +77,14 @@ async function getContext() {
74
77
  // Always recreate context if browser reference changed or context doesn't exist
75
78
  if (!context || context.browser !== browser) {
76
79
  context = await McpContext.from(browser, logger);
80
+ // Run UI health check only once per browser instance
81
+ if (!uiHealthCheckRun) {
82
+ uiHealthCheckRun = true;
83
+ // Run in background to avoid blocking startup
84
+ runStartupCheck(browser).catch(err => {
85
+ logger(`Startup check failed: ${err}`);
86
+ });
87
+ }
77
88
  }
78
89
  return context;
79
90
  }
@@ -138,6 +149,7 @@ const tools = [
138
149
  ...Object.values(chatgptWebTools),
139
150
  ...Object.values(deepResearchChatGPTTools),
140
151
  ...Object.values(consoleTools),
152
+ ...Object.values(diagnoseUiTools),
141
153
  ...Object.values(emulationTools),
142
154
  ...Object.values(extensionTools),
143
155
  ...Object.values(inputTools),
@@ -0,0 +1,85 @@
1
+ {
2
+ "version": "2025-10",
3
+ "lastUpdated": "2025-10-03",
4
+ "description": "ChatGPT Web UI selectors for browser automation",
5
+ "elements": {
6
+ "newChatLink": {
7
+ "css": "a[href='/']",
8
+ "description": "新しいチャット作成リンク"
9
+ },
10
+ "tempChatToggle": {
11
+ "ax": {
12
+ "role": "button",
13
+ "nameContains": "一時チャットをオフにする"
14
+ },
15
+ "description": "一時チャットON/OFFトグルボタン"
16
+ },
17
+ "addFileButton": {
18
+ "ax": {
19
+ "role": "button",
20
+ "ariaLabel": "ファイルの追加"
21
+ },
22
+ "description": "ファイル追加(ツールメニューを開く)ボタン"
23
+ },
24
+ "deepResearchMenuItem": {
25
+ "css": "[role='menuitemradio']",
26
+ "text": "Deep Research",
27
+ "description": "DeepResearchモード切り替えメニュー項目"
28
+ },
29
+ "deepResearchDeleteButton": {
30
+ "text": "リサーチ:クリックして削除",
31
+ "description": "DeepResearch削除ボタン(モード確認用)"
32
+ },
33
+ "deepResearchSourcesButton": {
34
+ "text": "情報源",
35
+ "description": "情報源ボタン(モード確認用)"
36
+ },
37
+ "composerTextarea": {
38
+ "css": "textarea[placeholder]",
39
+ "description": "メッセージ入力テキストエリア"
40
+ },
41
+ "prosemirrorEditor": {
42
+ "css": ".ProseMirror[contenteditable='true']",
43
+ "description": "ProseMirrorエディタ"
44
+ },
45
+ "sendButton": {
46
+ "css": "button[data-testid='send-button']",
47
+ "description": "送信ボタン"
48
+ },
49
+ "userMessage": {
50
+ "css": "[data-message-author-role='user']",
51
+ "description": "ユーザーメッセージ"
52
+ },
53
+ "assistantMessage": {
54
+ "css": "[data-message-author-role='assistant']",
55
+ "description": "アシスタントメッセージ"
56
+ },
57
+ "stopButton": {
58
+ "text": [
59
+ "停止",
60
+ "リサーチを停止",
61
+ "ストリーミングの停止"
62
+ ],
63
+ "ax": {
64
+ "ariaLabel": [
65
+ "停止",
66
+ "ストリーミングの停止"
67
+ ]
68
+ },
69
+ "description": "ストリーミング停止ボタン"
70
+ },
71
+ "thinkingTimeButton": {
72
+ "ax": {
73
+ "ariaLabelContains": "思考時間"
74
+ },
75
+ "description": "思考時間表示ボタン"
76
+ }
77
+ },
78
+ "placeholders": {
79
+ "normalMode": "Message ChatGPT",
80
+ "deepResearchMode": [
81
+ "詳細なレポート",
82
+ "リサーチ"
83
+ ]
84
+ }
85
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import * as fs from 'node:fs';
7
+ import * as path from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ let cachedSelectors = null;
10
+ /**
11
+ * Load ChatGPT selectors from JSON file
12
+ */
13
+ export function loadSelectors() {
14
+ if (cachedSelectors) {
15
+ return cachedSelectors;
16
+ }
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const __dirname = path.dirname(__filename);
19
+ const selectorsPath = path.join(__dirname, 'chatgpt.json');
20
+ try {
21
+ const data = fs.readFileSync(selectorsPath, 'utf-8');
22
+ cachedSelectors = JSON.parse(data);
23
+ return cachedSelectors;
24
+ }
25
+ catch (error) {
26
+ throw new Error(`Failed to load selectors from ${selectorsPath}: ${error}`);
27
+ }
28
+ }
29
+ /**
30
+ * Get a specific selector definition
31
+ */
32
+ export function getSelector(elementName) {
33
+ const selectors = loadSelectors();
34
+ const selector = selectors.elements[elementName];
35
+ if (!selector) {
36
+ throw new Error(`Selector not found: ${elementName}`);
37
+ }
38
+ return selector;
39
+ }
40
+ /**
41
+ * Generate page.evaluate() compatible selector function for text matching
42
+ */
43
+ export function generateTextMatcher(selector, tagName = 'button') {
44
+ const textPatterns = Array.isArray(selector.text)
45
+ ? selector.text
46
+ : [selector.text];
47
+ return `
48
+ Array.from(document.querySelectorAll('${tagName}')).find(el => {
49
+ const text = el.textContent || '';
50
+ return ${textPatterns.map((t) => `text.includes('${t}')`).join(' || ')};
51
+ })
52
+ `;
53
+ }
54
+ /**
55
+ * Generate page.evaluate() compatible selector function for accessibility matching
56
+ */
57
+ export function generateAxMatcher(selector, tagName = 'button') {
58
+ if (!selector.ax) {
59
+ throw new Error('Accessibility selector not defined');
60
+ }
61
+ const conditions = [];
62
+ if (selector.ax.ariaLabel) {
63
+ const labels = Array.isArray(selector.ax.ariaLabel)
64
+ ? selector.ax.ariaLabel
65
+ : [selector.ax.ariaLabel];
66
+ conditions.push(labels.map((label) => `aria.includes('${label}')`).join(' || '));
67
+ }
68
+ if (selector.ax.ariaLabelContains) {
69
+ conditions.push(`aria.includes('${selector.ax.ariaLabelContains}')`);
70
+ }
71
+ if (selector.ax.nameContains) {
72
+ conditions.push(`aria.includes('${selector.ax.nameContains}')`);
73
+ }
74
+ return `
75
+ Array.from(document.querySelectorAll('${tagName}')).find(el => {
76
+ const aria = el.getAttribute('aria-label') || '';
77
+ return ${conditions.join(' || ')};
78
+ })
79
+ `;
80
+ }
81
+ /**
82
+ * Check if placeholder matches expected patterns
83
+ */
84
+ export function checkPlaceholder(placeholder, mode) {
85
+ const selectors = loadSelectors();
86
+ if (mode === 'normal' && selectors.placeholders?.normalMode) {
87
+ return placeholder.includes(selectors.placeholders.normalMode);
88
+ }
89
+ if (mode === 'deepResearch' && selectors.placeholders?.deepResearchMode) {
90
+ const patterns = Array.isArray(selectors.placeholders.deepResearchMode)
91
+ ? selectors.placeholders.deepResearchMode
92
+ : [selectors.placeholders.deepResearchMode];
93
+ return patterns.some((pattern) => placeholder.includes(pattern));
94
+ }
95
+ return false;
96
+ }
97
+ /**
98
+ * Clear cached selectors (useful for testing or hot-reload)
99
+ */
100
+ export function clearCache() {
101
+ cachedSelectors = null;
102
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ /**
7
+ * UI elements to check during health verification
8
+ */
9
+ const UI_ELEMENTS = [
10
+ {
11
+ name: 'Deep Research Toggle',
12
+ selector: '[role="menuitemradio"]', // Deep Research menu item
13
+ },
14
+ {
15
+ name: 'Composer Textarea',
16
+ selector: 'textarea, .ProseMirror[contenteditable="true"]', // ChatGPT input area
17
+ },
18
+ {
19
+ name: 'Send Button',
20
+ selector: 'button[data-testid="send-button"]', // Send message button
21
+ },
22
+ ];
23
+ /**
24
+ * Check if UI element exists using accessibility tree
25
+ */
26
+ async function checkElementExists(page, element) {
27
+ try {
28
+ // First try direct DOM query
29
+ const domCheck = await page.evaluate((selector) => {
30
+ return document.querySelector(selector) !== null;
31
+ }, element.selector);
32
+ if (domCheck) {
33
+ return true;
34
+ }
35
+ // Fallback: Check accessibility tree
36
+ const axTree = await page.accessibility.snapshot();
37
+ if (!axTree) {
38
+ return false;
39
+ }
40
+ // Search for element by name in AX tree
41
+ function searchAxTree(node) {
42
+ if (node.name?.includes(element.name)) {
43
+ return true;
44
+ }
45
+ if (node.children) {
46
+ for (const child of node.children) {
47
+ if (searchAxTree(child)) {
48
+ return true;
49
+ }
50
+ }
51
+ }
52
+ return false;
53
+ }
54
+ return searchAxTree(axTree);
55
+ }
56
+ catch (error) {
57
+ console.error(`Error checking ${element.name}:`, error);
58
+ return false;
59
+ }
60
+ }
61
+ /**
62
+ * Verify ChatGPT UI health on startup
63
+ */
64
+ export async function verifyUIHealth(browser) {
65
+ console.error('\n🔍 UI Health Check: Starting...');
66
+ let page;
67
+ try {
68
+ // Get or create a page
69
+ const pages = await browser.pages();
70
+ page = pages[0] || (await browser.newPage());
71
+ // Navigate to ChatGPT with short timeout
72
+ console.error(' Navigating to ChatGPT...');
73
+ await page.goto('https://chatgpt.com/', {
74
+ waitUntil: 'networkidle2',
75
+ timeout: 30000,
76
+ });
77
+ // Wait for page to be ready
78
+ await new Promise((resolve) => setTimeout(resolve, 2000));
79
+ // Check login status
80
+ const currentUrl = page.url();
81
+ if (currentUrl.includes('auth') || currentUrl.includes('login')) {
82
+ console.error('⚠️ Not logged in to ChatGPT - skipping UI verification');
83
+ console.error(' Please log in manually for full functionality');
84
+ return;
85
+ }
86
+ // Check each UI element
87
+ const results = [];
88
+ for (const element of UI_ELEMENTS) {
89
+ const found = await checkElementExists(page, element);
90
+ results.push({ element, found });
91
+ if (found) {
92
+ console.error(`✅ ${element.name}: Found`);
93
+ }
94
+ else {
95
+ const status = element.optional ? '⚠️' : '❌';
96
+ console.error(`${status} ${element.name}: NOT FOUND`);
97
+ }
98
+ }
99
+ // Summary
100
+ const missingElements = results.filter((r) => !r.found && !r.element.optional);
101
+ if (missingElements.length > 0) {
102
+ console.error('\n⚠️ UI Health Check Warning:');
103
+ console.error(' Some UI elements were not found. ChatGPT UI may have changed.');
104
+ console.error(' Missing elements:');
105
+ missingElements.forEach((r) => {
106
+ console.error(` - ${r.element.name}`);
107
+ });
108
+ console.error('\n💡 Suggestion: Run diagnostics to investigate:');
109
+ console.error(' npm run diagnose:ui');
110
+ }
111
+ else {
112
+ console.error('\n✅ UI Health Check: All elements found');
113
+ }
114
+ }
115
+ catch (error) {
116
+ const errorMessage = error instanceof Error ? error.message : String(error);
117
+ console.error(`\n❌ UI Health Check Failed: ${errorMessage}`);
118
+ console.error(' This is a warning only - MCP server will continue to start');
119
+ console.error(' ChatGPT functionality may be limited');
120
+ }
121
+ finally {
122
+ // Don't close the page - let the server reuse it
123
+ console.error('');
124
+ }
125
+ }
126
+ /**
127
+ * Run UI health check with timeout protection
128
+ */
129
+ export async function runStartupCheck(browser) {
130
+ const timeoutMs = 35000; // 35 seconds total timeout
131
+ try {
132
+ await Promise.race([
133
+ verifyUIHealth(browser),
134
+ new Promise((_, reject) => setTimeout(() => reject(new Error('UI health check timeout')), timeoutMs)),
135
+ ]);
136
+ }
137
+ catch (error) {
138
+ const errorMessage = error instanceof Error ? error.message : String(error);
139
+ console.error(`⚠️ Startup check timeout or error: ${errorMessage}`);
140
+ console.error(' Continuing with server startup...');
141
+ }
142
+ }
@@ -8,6 +8,7 @@ import path from 'node:path';
8
8
  import z from 'zod';
9
9
  import { ToolCategories } from './categories.js';
10
10
  import { defineTool } from './ToolDefinition.js';
11
+ import { loadSelectors, getSelector } from '../selectors/loader.js';
11
12
  /**
12
13
  * Path to store chat session data
13
14
  */
@@ -280,29 +281,32 @@ export const askChatGPTWeb = defineTool({
280
281
  if (deepResearchEnabled) {
281
282
  response.appendResponseLine('✅ DeepResearchモード有効化完了');
282
283
  await new Promise((resolve) => setTimeout(resolve, 1000));
283
- // Verify mode was actually enabled (check for composer-pill button)
284
- const verified = await page.evaluate(() => {
285
- const DEEP_RESEARCH_PATTERN = /リサーチ|deep\s*research|ディープ\s*リサーチ|深度研究|深入研究/i;
286
- const promptTextarea = document.querySelector('#prompt-textarea');
287
- if (promptTextarea) {
288
- const form = promptTextarea.closest('form');
289
- if (form) {
290
- const pillButton = Array.from(form.querySelectorAll('button')).find((btn) => {
291
- const text = btn.textContent?.trim() || '';
292
- const ariaLabel = btn.getAttribute('aria-label') || '';
293
- return (btn.className.includes('composer-pill') &&
294
- DEEP_RESEARCH_PATTERN.test(text + ' ' + ariaLabel));
295
- });
296
- return !!pillButton;
297
- }
298
- }
299
- return false;
300
- });
301
- if (verified) {
302
- response.appendResponseLine('✅ モード確認完了: DeepResearch有効(composer-pill検出)');
284
+ // Verify mode was actually enabled (check for new UI indicators)
285
+ const selectors = loadSelectors();
286
+ const verification = await page.evaluate((deleteText, sourcesText, deepResearchPlaceholders) => {
287
+ // Check placeholder text
288
+ const textarea = document.querySelector('textarea');
289
+ const placeholder = textarea?.getAttribute('placeholder') || '';
290
+ // Check for delete button (using JSON selector)
291
+ const deleteButton = Array.from(document.querySelectorAll('button')).find(btn => btn.textContent?.includes(deleteText));
292
+ // Check for sources button (using JSON selector)
293
+ const sourcesButton = Array.from(document.querySelectorAll('button')).find(btn => btn.textContent?.includes(sourcesText));
294
+ // Check placeholder against expected patterns
295
+ const hasCorrectPlaceholder = deepResearchPlaceholders.some((pattern) => placeholder.includes(pattern));
296
+ return {
297
+ hasCorrectPlaceholder,
298
+ hasDeleteButton: !!deleteButton,
299
+ hasSourcesButton: !!sourcesButton,
300
+ placeholder: placeholder
301
+ };
302
+ }, getSelector('deepResearchDeleteButton').text, getSelector('deepResearchSourcesButton').text, Array.isArray(selectors.placeholders?.deepResearchMode)
303
+ ? selectors.placeholders.deepResearchMode
304
+ : [selectors.placeholders?.deepResearchMode || '']);
305
+ if (verification.hasCorrectPlaceholder || verification.hasDeleteButton) {
306
+ response.appendResponseLine('✅ モード確認完了: DeepResearch有効');
303
307
  }
304
308
  else {
305
- response.appendResponseLine('⚠️ DeepResearchモードの確認に失敗しました');
309
+ response.appendResponseLine(`⚠️ DeepResearchモードの確認に失敗しました(placeholder: ${verification.placeholder})`);
306
310
  }
307
311
  }
308
312
  else {
@@ -312,28 +316,27 @@ export const askChatGPTWeb = defineTool({
312
316
  }
313
317
  // Step 4: Send question (with final mode verification)
314
318
  if (useDeepResearch) {
315
- const finalCheck = await page.evaluate(() => {
316
- const DEEP_RESEARCH_PATTERN = /リサーチ|deep\s*research|ディープ\s*リサーチ|深度研究|深入研究/i;
317
- const promptTextarea = document.querySelector('#prompt-textarea');
318
- if (promptTextarea) {
319
- const form = promptTextarea.closest('form');
320
- if (form) {
321
- const pillButton = Array.from(form.querySelectorAll('button')).find((btn) => {
322
- const text = btn.textContent?.trim() || '';
323
- const ariaLabel = btn.getAttribute('aria-label') || '';
324
- return (btn.className.includes('composer-pill') &&
325
- DEEP_RESEARCH_PATTERN.test(text + ' ' + ariaLabel));
326
- });
327
- return !!pillButton;
328
- }
329
- }
330
- return false;
331
- });
332
- if (!finalCheck) {
333
- response.appendResponseLine('❌ エラー: DeepResearchモードが無効です(composer-pill未検出)。送信を中止します。');
319
+ const selectorsForCheck = loadSelectors();
320
+ const finalCheck = await page.evaluate((deleteText, deepResearchPlaceholders) => {
321
+ // Check placeholder text
322
+ const textarea = document.querySelector('textarea');
323
+ const placeholder = textarea?.getAttribute('placeholder') || '';
324
+ // Check for delete button (using JSON selector)
325
+ const deleteButton = Array.from(document.querySelectorAll('button')).find(btn => btn.textContent?.includes(deleteText));
326
+ // Check if placeholder matches DeepResearch patterns
327
+ const placeholderMatches = deepResearchPlaceholders.some((pattern) => placeholder.includes(pattern));
328
+ return {
329
+ isEnabled: placeholderMatches || !!deleteButton,
330
+ placeholder: placeholder
331
+ };
332
+ }, getSelector('deepResearchDeleteButton').text, Array.isArray(selectorsForCheck.placeholders?.deepResearchMode)
333
+ ? selectorsForCheck.placeholders.deepResearchMode
334
+ : [selectorsForCheck.placeholders?.deepResearchMode || '']);
335
+ if (!finalCheck.isEnabled) {
336
+ response.appendResponseLine(`❌ エラー: DeepResearchモードが無効です。送信を中止します。(placeholder: ${finalCheck.placeholder})`);
334
337
  return;
335
338
  }
336
- response.appendResponseLine('✅ 送信前確認: DeepResearchモード有効(composer-pill検出)');
339
+ response.appendResponseLine('✅ 送信前確認: DeepResearchモード有効');
337
340
  }
338
341
  response.appendResponseLine('質問を送信中...');
339
342
  const questionSent = await page.evaluate((questionText) => {
@@ -0,0 +1,310 @@
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
+ /**
12
+ * Known important elements in ChatGPT UI
13
+ */
14
+ const KNOWN_ELEMENTS = [
15
+ {
16
+ name: 'Deep Research Toggle',
17
+ checks: [
18
+ { type: 'css', selector: '[role="menuitemradio"][aria-label*="Deep"]' },
19
+ { type: 'xpath', selector: '//div[@role="menuitemradio" and contains(text(), "Deep research")]' },
20
+ { type: 'text', contains: 'Deep research' },
21
+ ],
22
+ },
23
+ {
24
+ name: 'Composer Textarea',
25
+ checks: [
26
+ { type: 'css', selector: 'textarea[placeholder*="Message"]' },
27
+ { type: 'css', selector: '[contenteditable="true"]' },
28
+ { type: 'css', selector: '#prompt-textarea' },
29
+ ],
30
+ },
31
+ {
32
+ name: 'Send Button',
33
+ checks: [
34
+ { type: 'css', selector: 'button[data-testid="send-button"]' },
35
+ { type: 'css', selector: 'button[aria-label*="Send"]' },
36
+ { type: 'xpath', selector: '//button[contains(@aria-label, "Send")]' },
37
+ ],
38
+ },
39
+ {
40
+ name: 'Model Selector',
41
+ checks: [
42
+ { type: 'css', selector: 'button[aria-label*="model"]' },
43
+ { type: 'css', selector: '[role="combobox"]' },
44
+ { type: 'text', contains: 'ChatGPT' },
45
+ ],
46
+ },
47
+ ];
48
+ /**
49
+ * Ensure snapshot directory exists
50
+ */
51
+ async function ensureSnapshotDir() {
52
+ const snapshotDir = path.join(process.cwd(), 'docs/ui-snapshots');
53
+ await fs.promises.mkdir(snapshotDir, { recursive: true });
54
+ return snapshotDir;
55
+ }
56
+ /**
57
+ * Generate timestamp-based filename
58
+ */
59
+ function generateFilename(extension) {
60
+ const now = new Date();
61
+ const timestamp = now.toISOString()
62
+ .replace(/:/g, '')
63
+ .replace(/\..+/, '')
64
+ .replace('T', '-')
65
+ .slice(2); // Remove first 2 digits of year (20YY -> YY)
66
+ return `chatgpt-${timestamp}.${extension}`;
67
+ }
68
+ /**
69
+ * Detect element using multiple strategies
70
+ */
71
+ async function detectElement(page, elementDef) {
72
+ const result = {
73
+ name: elementDef.name,
74
+ status: 'not_found',
75
+ selectors: {},
76
+ };
77
+ try {
78
+ // Try CSS selectors
79
+ for (const check of elementDef.checks.filter(c => c.type === 'css')) {
80
+ try {
81
+ const element = await page.$(check.selector);
82
+ if (element) {
83
+ result.status = 'found';
84
+ result.selectors.css = check.selector;
85
+ break;
86
+ }
87
+ }
88
+ catch {
89
+ // Continue to next selector
90
+ }
91
+ }
92
+ // Try XPath selectors
93
+ for (const check of elementDef.checks.filter(c => c.type === 'xpath')) {
94
+ try {
95
+ const elements = await page.$x(check.selector);
96
+ if (elements.length > 0) {
97
+ if (result.status !== 'found') {
98
+ result.status = 'found';
99
+ }
100
+ result.selectors.xpath = check.selector;
101
+ break;
102
+ }
103
+ }
104
+ catch {
105
+ // Continue to next selector
106
+ }
107
+ }
108
+ // Try text-based detection
109
+ for (const check of elementDef.checks.filter(c => c.type === 'text')) {
110
+ try {
111
+ const hasText = await page.evaluate((text) => {
112
+ return document.body.innerText.includes(text);
113
+ }, check.contains);
114
+ if (hasText) {
115
+ if (result.status !== 'found') {
116
+ result.status = 'found';
117
+ }
118
+ result.selectors.accessibility = `Text contains: "${check.contains}"`;
119
+ break;
120
+ }
121
+ }
122
+ catch {
123
+ // Continue to next check
124
+ }
125
+ }
126
+ // If we found some selectors but not all expected ones, mark as structure_changed
127
+ if (result.status === 'found' && Object.keys(result.selectors).length < elementDef.checks.length) {
128
+ result.status = 'structure_changed';
129
+ result.details = 'Some selectors work, but structure may have changed';
130
+ }
131
+ }
132
+ catch (error) {
133
+ result.details = `Error during detection: ${error instanceof Error ? error.message : String(error)}`;
134
+ }
135
+ return result;
136
+ }
137
+ /**
138
+ * Generate diagnostic report in Markdown format
139
+ */
140
+ function generateReport(result) {
141
+ const lines = [];
142
+ lines.push('# ChatGPT UI Diagnosis Report');
143
+ lines.push('');
144
+ lines.push(`**Date**: ${result.timestamp}`);
145
+ lines.push(`**URL**: ${result.url}`);
146
+ lines.push('');
147
+ lines.push('## Element Detection Results');
148
+ lines.push('');
149
+ for (const element of result.elements) {
150
+ const statusEmoji = element.status === 'found' ? '✅' :
151
+ element.status === 'structure_changed' ? '⚠️' :
152
+ '❌';
153
+ lines.push(`### ${statusEmoji} ${element.name}`);
154
+ lines.push(`- **Status**: ${element.status.replace(/_/g, ' ')}`);
155
+ if (element.selectors && Object.keys(element.selectors).length > 0) {
156
+ lines.push('- **Current Selectors**:');
157
+ if (element.selectors.css) {
158
+ lines.push(` - CSS: \`${element.selectors.css}\``);
159
+ }
160
+ if (element.selectors.xpath) {
161
+ lines.push(` - XPath: \`${element.selectors.xpath}\``);
162
+ }
163
+ if (element.selectors.accessibility) {
164
+ lines.push(` - Accessibility: \`${element.selectors.accessibility}\``);
165
+ }
166
+ }
167
+ if (element.details) {
168
+ lines.push(`- **Details**: ${element.details}`);
169
+ }
170
+ if (element.suggestion) {
171
+ lines.push(`- **Suggestion**: ${element.suggestion}`);
172
+ }
173
+ lines.push('');
174
+ }
175
+ lines.push('## Files Generated');
176
+ lines.push('');
177
+ lines.push(`- **HTML**: ${result.filesGenerated.html}`);
178
+ lines.push(`- **Screenshot**: ${result.filesGenerated.screenshot}`);
179
+ lines.push(`- **AX Tree**: ${result.filesGenerated.axTree}`);
180
+ lines.push(`- **Report**: ${result.filesGenerated.report}`);
181
+ lines.push('');
182
+ lines.push('## Next Steps');
183
+ lines.push('');
184
+ lines.push('1. Review the HTML snapshot to understand current DOM structure');
185
+ lines.push('2. Check the screenshot to see visual layout changes');
186
+ lines.push('3. Analyze the AX tree JSON for accessibility structure changes');
187
+ lines.push('4. Update selectors in relevant tool files based on findings');
188
+ lines.push('');
189
+ return lines.join('\n');
190
+ }
191
+ export const diagnoseChatgptUi = defineTool({
192
+ name: 'diagnose_chatgpt_ui',
193
+ description: 'Diagnose ChatGPT UI structure for debugging. ' +
194
+ 'Captures full AX tree, HTML snapshot, screenshot, and generates a diagnostic report. ' +
195
+ 'Use this when ChatGPT UI detection is failing or after ChatGPT updates.',
196
+ annotations: {
197
+ title: 'Diagnose ChatGPT UI',
198
+ category: ToolCategories.DEBUGGING,
199
+ readOnlyHint: true,
200
+ },
201
+ schema: {
202
+ url: z.string()
203
+ .default('https://chatgpt.com/')
204
+ .describe('ChatGPT URL to diagnose (default: https://chatgpt.com/)'),
205
+ waitForLoad: z.number()
206
+ .default(5000)
207
+ .describe('Time to wait for page to load in milliseconds (default: 5000)'),
208
+ },
209
+ handler: async (request, response, context) => {
210
+ const { url, waitForLoad } = request.params;
211
+ const page = context.getSelectedPage();
212
+ try {
213
+ response.appendResponseLine('🔍 Starting ChatGPT UI diagnosis...');
214
+ response.appendResponseLine('');
215
+ // Navigate to ChatGPT
216
+ response.appendResponseLine(`📡 Navigating to ${url}...`);
217
+ await page.goto(url, { waitUntil: 'networkidle0', timeout: 30000 });
218
+ // Wait for page to stabilize
219
+ response.appendResponseLine(`⏳ Waiting ${waitForLoad}ms for page to stabilize...`);
220
+ await new Promise(resolve => setTimeout(resolve, waitForLoad));
221
+ // Ensure snapshot directory exists
222
+ const snapshotDir = await ensureSnapshotDir();
223
+ const timestamp = new Date().toISOString();
224
+ // Generate filenames
225
+ const htmlFilename = generateFilename('html');
226
+ const screenshotFilename = generateFilename('png');
227
+ const axFilename = generateFilename('ax.json');
228
+ const reportFilename = generateFilename('report.md');
229
+ // Capture HTML
230
+ response.appendResponseLine('📄 Capturing HTML snapshot...');
231
+ const htmlContent = await page.content();
232
+ const htmlPath = path.join(snapshotDir, htmlFilename);
233
+ await fs.promises.writeFile(htmlPath, htmlContent, 'utf-8');
234
+ response.appendResponseLine(`✅ HTML saved: ${htmlFilename}`);
235
+ // Take screenshot
236
+ response.appendResponseLine('📸 Taking screenshot...');
237
+ const screenshotBuffer = await page.screenshot({
238
+ fullPage: true,
239
+ type: 'png',
240
+ });
241
+ const screenshotPath = path.join(snapshotDir, screenshotFilename);
242
+ await fs.promises.writeFile(screenshotPath, screenshotBuffer);
243
+ response.appendResponseLine(`✅ Screenshot saved: ${screenshotFilename}`);
244
+ // Capture full AX tree
245
+ response.appendResponseLine('🌳 Capturing accessibility tree...');
246
+ const snapshot = await page.accessibility.snapshot({ interestingOnly: false });
247
+ const axPath = path.join(snapshotDir, axFilename);
248
+ await fs.promises.writeFile(axPath, JSON.stringify(snapshot, null, 2), 'utf-8');
249
+ response.appendResponseLine(`✅ AX tree saved: ${axFilename}`);
250
+ // Detect known elements
251
+ response.appendResponseLine('');
252
+ response.appendResponseLine('🔎 Detecting important UI elements...');
253
+ const elementResults = [];
254
+ for (const elementDef of KNOWN_ELEMENTS) {
255
+ const result = await detectElement(page, elementDef);
256
+ elementResults.push(result);
257
+ const emoji = result.status === 'found' ? '✅' :
258
+ result.status === 'structure_changed' ? '⚠️' :
259
+ '❌';
260
+ response.appendResponseLine(`${emoji} ${result.name}: ${result.status}`);
261
+ }
262
+ // Generate diagnostic result
263
+ const diagnosticResult = {
264
+ timestamp,
265
+ url: page.url(),
266
+ elements: elementResults,
267
+ filesGenerated: {
268
+ html: htmlFilename,
269
+ screenshot: screenshotFilename,
270
+ axTree: axFilename,
271
+ report: reportFilename,
272
+ },
273
+ };
274
+ // Generate and save report
275
+ response.appendResponseLine('');
276
+ response.appendResponseLine('📝 Generating diagnostic report...');
277
+ const reportContent = generateReport(diagnosticResult);
278
+ const reportPath = path.join(snapshotDir, reportFilename);
279
+ await fs.promises.writeFile(reportPath, reportContent, 'utf-8');
280
+ response.appendResponseLine(`✅ Report saved: ${reportFilename}`);
281
+ // Output summary
282
+ response.appendResponseLine('');
283
+ response.appendResponseLine('✅ Diagnosis complete!');
284
+ response.appendResponseLine('');
285
+ response.appendResponseLine('📁 All files saved to: docs/ui-snapshots/');
286
+ response.appendResponseLine('');
287
+ response.appendResponseLine('Summary:');
288
+ const foundCount = elementResults.filter(e => e.status === 'found').length;
289
+ const changedCount = elementResults.filter(e => e.status === 'structure_changed').length;
290
+ const notFoundCount = elementResults.filter(e => e.status === 'not_found').length;
291
+ response.appendResponseLine(`- ✅ Found: ${foundCount}`);
292
+ response.appendResponseLine(`- ⚠️ Changed: ${changedCount}`);
293
+ response.appendResponseLine(`- ❌ Not Found: ${notFoundCount}`);
294
+ // Attach screenshot to response
295
+ response.attachImage({
296
+ data: Buffer.from(screenshotBuffer).toString('base64'),
297
+ mimeType: 'image/png',
298
+ });
299
+ }
300
+ catch (error) {
301
+ response.appendResponseLine('❌ Diagnosis failed:');
302
+ response.appendResponseLine(error instanceof Error ? error.message : String(error));
303
+ if (error instanceof Error && error.stack) {
304
+ response.appendResponseLine('');
305
+ response.appendResponseLine('Stack trace:');
306
+ response.appendResponseLine(error.stack);
307
+ }
308
+ }
309
+ },
310
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-devtools-mcp-for-extension",
3
- "version": "0.11.0",
3
+ "version": "0.12.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",
@@ -14,6 +14,7 @@
14
14
  "docs:generate": "node --experimental-strip-types scripts/generate-docs.ts",
15
15
  "start": "npm run build && node build/src/index.js",
16
16
  "start-debug": "DEBUG=mcp:* DEBUG_COLORS=false npm run build && node build/src/index.js",
17
+ "diagnose:ui": "echo 'Use diagnose_chatgpt_ui tool via MCP client (Claude Code)'",
17
18
  "test": "npm run build && node --require ./build/tests/setup.js --no-warnings=ExperimentalWarning --test-reporter spec --test-force-exit --test \"build/tests/**/*.test.js\"",
18
19
  "test:only": "npm run build && node --require ./build/tests/setup.js --no-warnings=ExperimentalWarning --test-reporter spec --test-force-exit --test --test-only \"build/tests/**/*.test.js\"",
19
20
  "test:only:no-build": "node --require ./build/tests/setup.js --no-warnings=ExperimentalWarning --test-reporter spec --test-force-exit --test --test-only \"build/tests/**/*.test.js\"",