chrome-devtools-mcp-for-extension 0.11.1 → 0.13.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.
@@ -10,6 +10,7 @@ import { NetworkCollector, PageCollector } from './PageCollector.js';
10
10
  import { listPages } from './tools/pages.js';
11
11
  import { CLOSE_PAGE_ERROR } from './tools/ToolDefinition.js';
12
12
  import { WaitForHelper } from './WaitForHelper.js';
13
+ import { BrowserConnectionManager, } from './browser-connection-manager.js';
13
14
  const DEFAULT_TIMEOUT = 5_000;
14
15
  const NAVIGATION_TIMEOUT = 10_000;
15
16
  function getNetworkMultiplierFromString(condition) {
@@ -29,6 +30,7 @@ function getNetworkMultiplierFromString(condition) {
29
30
  export class McpContext {
30
31
  browser;
31
32
  logger;
33
+ connectionManager;
32
34
  // The most recent page state.
33
35
  #pages = [];
34
36
  #selectedPageIdx = 0;
@@ -42,9 +44,14 @@ export class McpContext {
42
44
  #dialog;
43
45
  #nextSnapshotId = 1;
44
46
  #traceResults = [];
45
- constructor(browser, logger) {
47
+ constructor(browser, logger, browserFactory, connectionOptions) {
46
48
  this.browser = browser;
47
49
  this.logger = logger;
50
+ this.connectionManager = new BrowserConnectionManager(connectionOptions);
51
+ // Set up browser instance and factory for reconnection
52
+ if (browserFactory) {
53
+ this.connectionManager.setBrowser(browser, browserFactory);
54
+ }
48
55
  this.#networkCollector = new NetworkCollector(this.browser, (page, collect) => {
49
56
  page.on('request', request => {
50
57
  collect(request);
@@ -65,11 +72,25 @@ export class McpContext {
65
72
  await this.#networkCollector.init();
66
73
  await this.#consoleCollector.init();
67
74
  }
68
- static async from(browser, logger) {
69
- const context = new McpContext(browser, logger);
75
+ static async from(browser, logger, browserFactory, connectionOptions) {
76
+ const context = new McpContext(browser, logger, browserFactory, connectionOptions);
70
77
  await context.#init();
71
78
  return context;
72
79
  }
80
+ /**
81
+ * Update browser instance after reconnection
82
+ */
83
+ async updateBrowser(newBrowser) {
84
+ this.browser = newBrowser;
85
+ this.connectionManager.setBrowser(newBrowser, async () => newBrowser);
86
+ // Reinitialize collectors for new browser
87
+ await this.#networkCollector.init();
88
+ await this.#consoleCollector.init();
89
+ // Recreate pages snapshot
90
+ await this.createPagesSnapshot();
91
+ this.setSelectedPageIdx(0);
92
+ this.logger('Browser instance updated after reconnection');
93
+ }
73
94
  getNetworkRequests() {
74
95
  const page = this.getSelectedPage();
75
96
  return this.#networkCollector.getData(page);
@@ -0,0 +1,198 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ /**
7
+ * Default connection manager options
8
+ */
9
+ const DEFAULT_OPTIONS = {
10
+ maxReconnectAttempts: 3,
11
+ initialRetryDelay: 1000, // 1 second
12
+ maxRetryDelay: 10000, // 10 seconds
13
+ enableLogging: true,
14
+ onReconnect: undefined,
15
+ };
16
+ /**
17
+ * Browser Connection Manager
18
+ *
19
+ * Provides automatic reconnection for CDP operations
20
+ */
21
+ export class BrowserConnectionManager {
22
+ reconnectAttempts = 0;
23
+ options;
24
+ browser = null;
25
+ browserFactory = null;
26
+ constructor(options = {}) {
27
+ this.options = { ...DEFAULT_OPTIONS, ...options };
28
+ }
29
+ /**
30
+ * Set browser instance and factory for reconnection
31
+ */
32
+ setBrowser(browser, factory) {
33
+ this.browser = browser;
34
+ this.browserFactory = factory;
35
+ }
36
+ /**
37
+ * Execute an operation with automatic retry on CDP connection errors
38
+ */
39
+ async executeWithRetry(operation, operationName) {
40
+ try {
41
+ return await operation();
42
+ }
43
+ catch (error) {
44
+ if (this.isCDPConnectionError(error)) {
45
+ this.log(`CDP connection error in ${operationName}, attempting reconnect...`);
46
+ return await this.retryWithReconnect(operation, operationName);
47
+ }
48
+ throw error;
49
+ }
50
+ }
51
+ /**
52
+ * Retry operation with exponential backoff
53
+ */
54
+ async retryWithReconnect(operation, operationName) {
55
+ const maxAttempts = this.options.maxReconnectAttempts ?? 3;
56
+ const initialDelay = this.options.initialRetryDelay ?? 1000;
57
+ const maxDelay = this.options.maxRetryDelay ?? 10000;
58
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
59
+ this.reconnectAttempts++;
60
+ const attemptNum = attempt + 1;
61
+ this.log(`Reconnect attempt ${attemptNum}/${maxAttempts} for ${operationName}...`);
62
+ // Exponential backoff with max delay
63
+ const delay = Math.min(initialDelay * Math.pow(2, attempt), maxDelay);
64
+ await this.sleep(delay);
65
+ try {
66
+ await this.reconnectBrowser();
67
+ this.log(`Reconnection successful, retrying ${operationName}...`);
68
+ return await operation();
69
+ }
70
+ catch (error) {
71
+ if (attempt === maxAttempts - 1) {
72
+ // Last attempt failed
73
+ throw this.createReconnectionFailedError(attemptNum, error);
74
+ }
75
+ this.log(`Reconnect attempt ${attemptNum} failed: ${error}`);
76
+ }
77
+ }
78
+ throw new Error('Reconnection logic error'); // Should never reach here
79
+ }
80
+ /**
81
+ * Check if error is a CDP connection error
82
+ */
83
+ isCDPConnectionError(error) {
84
+ const errorMessage = error?.message || '';
85
+ return (errorMessage.includes('Target closed') ||
86
+ errorMessage.includes('Protocol error') ||
87
+ errorMessage.includes('Session closed') ||
88
+ errorMessage.includes('Connection closed') ||
89
+ errorMessage.includes('WebSocket is not open'));
90
+ }
91
+ /**
92
+ * Reconnect browser instance
93
+ */
94
+ async reconnectBrowser() {
95
+ if (!this.browserFactory) {
96
+ throw new Error('Browser factory not set. Cannot reconnect.');
97
+ }
98
+ try {
99
+ // Close old browser if still connected
100
+ if (this.browser?.isConnected()) {
101
+ await this.browser.close().catch(() => {
102
+ // Ignore errors during close
103
+ });
104
+ }
105
+ }
106
+ catch (error) {
107
+ // Ignore errors during cleanup
108
+ }
109
+ // Create new browser instance
110
+ const newBrowser = await this.browserFactory();
111
+ this.browser = newBrowser;
112
+ this.log('Browser reconnected successfully');
113
+ // Notify callback if provided
114
+ if (this.options.onReconnect) {
115
+ await this.options.onReconnect(newBrowser);
116
+ }
117
+ }
118
+ /**
119
+ * Create user-friendly error for reconnection failure
120
+ */
121
+ createReconnectionFailedError(attempts, lastError) {
122
+ const message = `
123
+ ❌ Chrome DevTools接続エラー
124
+
125
+ ${attempts}回の再接続を試みましたが、Chrome DevToolsとの接続を回復できませんでした。
126
+
127
+ 📋 最後のエラー:
128
+ ${lastError?.message || 'Unknown error'}
129
+
130
+ 🔧 解決方法:
131
+ 1. Claude Codeを再起動してください
132
+ 2. Chromeブラウザを完全に終了して再起動してください
133
+ 3. chrome://extensions でChrome DevTools拡張機能を確認してください
134
+
135
+ 詳細: docs/troubleshooting.md#cdp-connection-error
136
+ `.trim();
137
+ const error = new Error(message);
138
+ error.name = 'CDPReconnectionError';
139
+ return error;
140
+ }
141
+ /**
142
+ * Log message if logging is enabled
143
+ */
144
+ log(message) {
145
+ if (this.options.enableLogging !== false) {
146
+ console.log(`[ConnectionManager] ${message}`);
147
+ }
148
+ }
149
+ /**
150
+ * Sleep for specified milliseconds
151
+ */
152
+ sleep(ms) {
153
+ return new Promise((resolve) => setTimeout(resolve, ms));
154
+ }
155
+ /**
156
+ * Get current browser instance
157
+ */
158
+ getBrowser() {
159
+ return this.browser;
160
+ }
161
+ /**
162
+ * Get total reconnection attempts made
163
+ */
164
+ getReconnectAttempts() {
165
+ return this.reconnectAttempts;
166
+ }
167
+ /**
168
+ * Reset reconnection attempt counter
169
+ */
170
+ resetReconnectAttempts() {
171
+ this.reconnectAttempts = 0;
172
+ }
173
+ /**
174
+ * Check if browser is currently connected
175
+ */
176
+ isConnected() {
177
+ return this.browser?.isConnected() ?? false;
178
+ }
179
+ }
180
+ /**
181
+ * Global connection manager instance
182
+ */
183
+ let globalConnectionManager = null;
184
+ /**
185
+ * Get or create global connection manager
186
+ */
187
+ export function getConnectionManager(options) {
188
+ if (!globalConnectionManager) {
189
+ globalConnectionManager = new BrowserConnectionManager(options);
190
+ }
191
+ return globalConnectionManager;
192
+ }
193
+ /**
194
+ * Reset global connection manager (for testing)
195
+ */
196
+ export function resetConnectionManager() {
197
+ globalConnectionManager = null;
198
+ }
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,8 +58,9 @@ 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
- const browser = await resolveBrowser({
63
+ const browserOptions = {
61
64
  browserUrl: args.browserUrl,
62
65
  headless: args.headless,
63
66
  executablePath: args.executablePath,
@@ -70,10 +73,31 @@ async function getContext() {
70
73
  chromeProfile: args.chromeProfile,
71
74
  userDataDir: args.userDataDir,
72
75
  logFile,
73
- });
76
+ };
77
+ const browser = await resolveBrowser(browserOptions);
78
+ // Browser factory function for reconnection
79
+ const browserFactory = async () => {
80
+ logger('Reconnecting browser...');
81
+ return await resolveBrowser(browserOptions);
82
+ };
74
83
  // Always recreate context if browser reference changed or context doesn't exist
75
84
  if (!context || context.browser !== browser) {
76
- context = await McpContext.from(browser, logger);
85
+ // Connection manager options with reconnect callback
86
+ const connectionOptions = {
87
+ onReconnect: async (newBrowser) => {
88
+ logger('Updating context with reconnected browser...');
89
+ await context.updateBrowser(newBrowser);
90
+ },
91
+ };
92
+ context = await McpContext.from(browser, logger, browserFactory, connectionOptions);
93
+ // Run UI health check only once per browser instance
94
+ if (!uiHealthCheckRun) {
95
+ uiHealthCheckRun = true;
96
+ // Run in background to avoid blocking startup
97
+ runStartupCheck(browser).catch(err => {
98
+ logger(`Startup check failed: ${err}`);
99
+ });
100
+ }
77
101
  }
78
102
  return context;
79
103
  }
@@ -138,6 +162,7 @@ const tools = [
138
162
  ...Object.values(chatgptWebTools),
139
163
  ...Object.values(deepResearchChatGPTTools),
140
164
  ...Object.values(consoleTools),
165
+ ...Object.values(diagnoseUiTools),
141
166
  ...Object.values(emulationTools),
142
167
  ...Object.values(extensionTools),
143
168
  ...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
  */
@@ -281,21 +282,26 @@ export const askChatGPTWeb = defineTool({
281
282
  response.appendResponseLine('✅ DeepResearchモード有効化完了');
282
283
  await new Promise((resolve) => setTimeout(resolve, 1000));
283
284
  // Verify mode was actually enabled (check for new UI indicators)
284
- const verification = await page.evaluate(() => {
285
+ const selectors = loadSelectors();
286
+ const verification = await page.evaluate((deleteText, sourcesText, deepResearchPlaceholders) => {
285
287
  // Check placeholder text
286
288
  const textarea = document.querySelector('textarea');
287
289
  const placeholder = textarea?.getAttribute('placeholder') || '';
288
- // Check for delete button
289
- const deleteButton = Array.from(document.querySelectorAll('button')).find(btn => btn.textContent?.includes('リサーチ:クリックして削除'));
290
- // Check for sources button
291
- const sourcesButton = Array.from(document.querySelectorAll('button')).find(btn => btn.textContent?.includes('情報源'));
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));
292
296
  return {
293
- hasCorrectPlaceholder: placeholder.includes('詳細なレポート') || placeholder.includes('リサーチ'),
297
+ hasCorrectPlaceholder,
294
298
  hasDeleteButton: !!deleteButton,
295
299
  hasSourcesButton: !!sourcesButton,
296
300
  placeholder: placeholder
297
301
  };
298
- });
302
+ }, getSelector('deepResearchDeleteButton').text, getSelector('deepResearchSourcesButton').text, Array.isArray(selectors.placeholders?.deepResearchMode)
303
+ ? selectors.placeholders.deepResearchMode
304
+ : [selectors.placeholders?.deepResearchMode || '']);
299
305
  if (verification.hasCorrectPlaceholder || verification.hasDeleteButton) {
300
306
  response.appendResponseLine('✅ モード確認完了: DeepResearch有効');
301
307
  }
@@ -310,17 +316,22 @@ export const askChatGPTWeb = defineTool({
310
316
  }
311
317
  // Step 4: Send question (with final mode verification)
312
318
  if (useDeepResearch) {
313
- const finalCheck = await page.evaluate(() => {
319
+ const selectorsForCheck = loadSelectors();
320
+ const finalCheck = await page.evaluate((deleteText, deepResearchPlaceholders) => {
314
321
  // Check placeholder text
315
322
  const textarea = document.querySelector('textarea');
316
323
  const placeholder = textarea?.getAttribute('placeholder') || '';
317
- // Check for delete button
318
- const deleteButton = Array.from(document.querySelectorAll('button')).find(btn => btn.textContent?.includes('リサーチ:クリックして削除'));
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));
319
328
  return {
320
- isEnabled: placeholder.includes('詳細なレポート') || placeholder.includes('リサーチ') || !!deleteButton,
329
+ isEnabled: placeholderMatches || !!deleteButton,
321
330
  placeholder: placeholder
322
331
  };
323
- });
332
+ }, getSelector('deepResearchDeleteButton').text, Array.isArray(selectorsForCheck.placeholders?.deepResearchMode)
333
+ ? selectorsForCheck.placeholders.deepResearchMode
334
+ : [selectorsForCheck.placeholders?.deepResearchMode || '']);
324
335
  if (!finalCheck.isEnabled) {
325
336
  response.appendResponseLine(`❌ エラー: DeepResearchモードが無効です。送信を中止します。(placeholder: ${finalCheck.placeholder})`);
326
337
  return;
@@ -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.1",
3
+ "version": "0.13.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\"",