@yusufffararatt/dombridge-mcp 2.7.5

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.
Files changed (49) hide show
  1. package/README.md +559 -0
  2. package/bin/cli.js +88 -0
  3. package/package.json +54 -0
  4. package/src/bridge/http-server.js +290 -0
  5. package/src/bridge/middleware.js +56 -0
  6. package/src/bridge/routes.js +1003 -0
  7. package/src/bridge-daemon.js +172 -0
  8. package/src/cli/auto-config.js +120 -0
  9. package/src/constants.js +13 -0
  10. package/src/index.js +279 -0
  11. package/src/mcp-bridge.js +136 -0
  12. package/src/metrics/error-codes.js +44 -0
  13. package/src/metrics/index.js +3 -0
  14. package/src/metrics/metrics-db.js +269 -0
  15. package/src/metrics/metrics-recorder.js +240 -0
  16. package/src/metrics/metrics-report.js +146 -0
  17. package/src/profiles/profile-db.js +159 -0
  18. package/src/profiles/profile-enricher.js +333 -0
  19. package/src/profiles/profile-manager.js +563 -0
  20. package/src/profiles/profile-repo.js +183 -0
  21. package/src/state/bridge-client.js +272 -0
  22. package/src/state/bridge-persistence.js +205 -0
  23. package/src/state/cache.js +38 -0
  24. package/src/state/extension-state.js +321 -0
  25. package/src/tools/action_tools.js +218 -0
  26. package/src/tools/analyze-page.js +247 -0
  27. package/src/tools/debug-mcp-state.js +172 -0
  28. package/src/tools/discover-apis.js +186 -0
  29. package/src/tools/execute-js.js +284 -0
  30. package/src/tools/export-session.js +171 -0
  31. package/src/tools/extract-data.js +395 -0
  32. package/src/tools/get-element.js +281 -0
  33. package/src/tools/get-network-trace.js +471 -0
  34. package/src/tools/index.js +110 -0
  35. package/src/tools/manage-site-profile.js +153 -0
  36. package/src/tools/paginate.js +444 -0
  37. package/src/tools/quick-scan.js +418 -0
  38. package/src/tools/screenshot_tools.js +117 -0
  39. package/src/utils/circuit-breaker.js +112 -0
  40. package/src/utils/extract-density.js +21 -0
  41. package/src/utils/logger.js +31 -0
  42. package/src/utils/paginate-detector.js +24 -0
  43. package/src/utils/rate-limiter.js +244 -0
  44. package/src/utils/run-script.js +37 -0
  45. package/src/utils/selector-validator.js +95 -0
  46. package/src/utils/state-validator.js +354 -0
  47. package/src/utils/tab-resolver.js +70 -0
  48. package/src/utils/workflow-helper.js +292 -0
  49. package/src/utils/workflow-state.js +177 -0
@@ -0,0 +1,321 @@
1
+ /**
2
+ * Extension State Management
3
+ * Chrome extension'dan gelen gerçek verileri merkezi olarak saklar
4
+ *
5
+ * Phase 1.1: Persistence hooks integrated — every state mutation
6
+ * schedules a debounced write to bridge-state.json via bridge-persistence.js.
7
+ */
8
+
9
+ import { schedulePersist } from './bridge-persistence.js';
10
+
11
+ // Shared state across MCP instances via HTTP
12
+
13
+ export const extensionData = {
14
+ // Son seçilen element
15
+ selectedElement: null,
16
+
17
+ // Network trace sonuçları
18
+ networkTrace: {
19
+ timestamp: null,
20
+ tabId: null,
21
+ totalMatches: 0,
22
+ matches: [],
23
+ elementValue: null,
24
+ values: [] // Multi-value filtering için
25
+ },
26
+
27
+ // WebSocket trace sonuçları (NEW v2.3)
28
+ websocketTrace: {
29
+ timestamp: null,
30
+ tabId: null,
31
+ totalMatches: 0,
32
+ matches: [],
33
+ elementValue: null,
34
+ filterValues: [],
35
+ connectionsAnalyzed: 0,
36
+ totalMessagesAnalyzed: 0
37
+ },
38
+
39
+ // Raw WebSocket connections data (NEW - from interceptor)
40
+ websocketConnections: {
41
+ timestamp: null,
42
+ tabId: null,
43
+ connections: [],
44
+ totalConnections: 0,
45
+ totalMessages: 0
46
+ },
47
+
48
+ // Kaydedilmiş seçimler
49
+ savedSelections: [],
50
+
51
+ // Sayfa veri kaynağı analizi
52
+ pageAnalysis: {
53
+ timestamp: null,
54
+ ssrDetected: false,
55
+ initialStateDetected: false,
56
+ embeddedDataDetected: false,
57
+ networkCaptureRate: 0,
58
+ totalNetworkRequests: 0,
59
+ capturedResponseBodies: 0,
60
+ detectedDataSources: [],
61
+ recommendation: null
62
+ },
63
+
64
+ // Aktif tab URL'i (heartbeat'ten gelir, domain tespiti için kullanılır)
65
+ activeTabUrl: '',
66
+
67
+ // Extension'ın aktif olup olmadığı
68
+ isConnected: false,
69
+
70
+ // Tab navigasyonu sırasında soft disconnect — isConnected false yapılmaz
71
+ pendingNavigation: false,
72
+ lastUpdateTime: null,
73
+
74
+ // Oturum takibi (sayfa yenilemelerini reconnect olarak tespit eder)
75
+ currentSessionId: null,
76
+ sessionStartedAt: null,
77
+
78
+ // JS Execution request/result (polling için)
79
+ jsExecutionRequest: null,
80
+ jsExecutionResult: null, // ESKİ: tekli result — kaldırılmayacak (backward compat)
81
+ jsExecutionResults: {}, // YENİ: requestId-keyed Map (race-condition fix)
82
+
83
+ // RPA Action request/result
84
+ actionExecutionRequest: null,
85
+ actionExecutionResult: null,
86
+
87
+ // Screenshot request/result
88
+ captureScreenshotRequest: null,
89
+ captureScreenshotResult: null,
90
+
91
+ // Raw network discovery queue/results (discover_apis tool)
92
+ rawNetworkRequests: [],
93
+ rawNetworkResults: {},
94
+
95
+ // Captured API endpoints from discover_apis (NEW — for manage_site_profile save flow)
96
+ // Each entry: { domain, method, url, status, contentType, firstSeenAt, lastSeenAt }
97
+ apiEndpoints: [],
98
+
99
+ // Analyze page queue/results
100
+ analyzePageRequests: [],
101
+ analyzePageResults: {},
102
+
103
+ // Programmatic element selection request/result (select_element tool)
104
+ selectElementRequest: null,
105
+ selectElementResult: null,
106
+
107
+ // Export session request/result
108
+ exportSessionRequest: null,
109
+ exportSessionResult: null,
110
+
111
+ // Tab list request/result (for multi-tab support)
112
+ tabsRequest: null,
113
+ tabsResult: null,
114
+
115
+ // Insight tracking: execute_js disambiguation fırsatları vs save_site_profile çağrıları
116
+ // { "x.com": 3 } formatında — domain başına sayaç
117
+ insightOpportunities: {},
118
+ profileSaves: {},
119
+
120
+ // Restart signal: MCP server reads this via /api/state to decide whether to exit
121
+ // Set by bridge daemon on POST /api/restart, cleared by MCP server on startup
122
+ restartRequestedAt: null,
123
+ };
124
+
125
+ /**
126
+ * Extension state güncelleme fonksiyonları
127
+ */
128
+ export const updateSelectedElement = (data) => {
129
+ const { cssSelector, xpath, outerHTML, tagName, attributes, sessionInfo, stableSelector, stableSelectorMeta, pageURL, pageTitle } = data;
130
+
131
+ extensionData.selectedElement = {
132
+ cssSelector,
133
+ xpath,
134
+ outerHTML,
135
+ tagName,
136
+ attributes,
137
+ sessionInfo: sessionInfo || null,
138
+ stableSelector: stableSelector || null,
139
+ stableSelectorMeta: stableSelectorMeta || null,
140
+ pageUrl: pageURL || null,
141
+ pageTitle: pageTitle || null,
142
+ timestamp: Date.now()
143
+ };
144
+
145
+ extensionData.isConnected = true;
146
+ extensionData.lastUpdateTime = Date.now();
147
+ schedulePersist(extensionData);
148
+ };
149
+
150
+ export const updateNetworkTrace = (data) => {
151
+ const { totalMatches, matches, elementValue, values, tabId } = data;
152
+
153
+ extensionData.networkTrace = {
154
+ timestamp: Date.now(),
155
+ tabId: tabId || null,
156
+ totalMatches,
157
+ matches: matches || [],
158
+ elementValue,
159
+ values: values || []
160
+ };
161
+
162
+ extensionData.lastUpdateTime = Date.now();
163
+ schedulePersist(extensionData);
164
+ };
165
+
166
+ export const updateWebSocketTrace = (data) => {
167
+ // Support both raw WebSocket data (from interceptor) and processed trace data
168
+ if (data.connections !== undefined) {
169
+ // Raw WebSocket data from interceptor
170
+ extensionData.websocketConnections = {
171
+ timestamp: Date.now(),
172
+ tabId: data.tabId || null,
173
+ connections: data.connections || [],
174
+ totalConnections: data.totalConnections || 0,
175
+ totalMessages: data.totalMessages || 0
176
+ };
177
+ } else {
178
+ // Processed trace data (legacy format with matches)
179
+ const { totalMatches, matches, elementValue, filterValues, connectionsAnalyzed, totalMessagesAnalyzed, tabId } = data;
180
+ extensionData.websocketTrace = {
181
+ timestamp: Date.now(),
182
+ tabId: tabId || null,
183
+ totalMatches: totalMatches || 0,
184
+ matches: matches || [],
185
+ elementValue: elementValue || null,
186
+ filterValues: filterValues || [],
187
+ connectionsAnalyzed: connectionsAnalyzed || 0,
188
+ totalMessagesAnalyzed: totalMessagesAnalyzed || 0
189
+ };
190
+ }
191
+
192
+ extensionData.lastUpdateTime = Date.now();
193
+ schedulePersist(extensionData);
194
+ };
195
+
196
+ export const updateSavedSelections = (savedSelections) => {
197
+ extensionData.savedSelections = savedSelections || [];
198
+ extensionData.lastUpdateTime = Date.now();
199
+ };
200
+
201
+ export const updatePageAnalysis = (data) => {
202
+ const {
203
+ ssrDetected,
204
+ initialStateDetected,
205
+ embeddedDataDetected,
206
+ networkCaptureRate,
207
+ totalNetworkRequests,
208
+ capturedResponseBodies,
209
+ detectedDataSources,
210
+ recommendation
211
+ } = data;
212
+
213
+ extensionData.pageAnalysis = {
214
+ timestamp: Date.now(),
215
+ ssrDetected: ssrDetected || false,
216
+ initialStateDetected: initialStateDetected || false,
217
+ embeddedDataDetected: embeddedDataDetected || false,
218
+ networkCaptureRate: networkCaptureRate || 0,
219
+ totalNetworkRequests: totalNetworkRequests || 0,
220
+ capturedResponseBodies: capturedResponseBodies || 0,
221
+ detectedDataSources: detectedDataSources || [],
222
+ recommendation: recommendation || null
223
+ };
224
+
225
+ extensionData.lastUpdateTime = Date.now();
226
+ schedulePersist(extensionData);
227
+ };
228
+
229
+ export const syncAll = (data) => {
230
+ const { selectedElement, networkTrace, savedSelections } = data;
231
+
232
+ if (selectedElement) {
233
+ updateSelectedElement(selectedElement);
234
+ }
235
+
236
+ if (networkTrace) {
237
+ updateNetworkTrace(networkTrace);
238
+ }
239
+
240
+ if (savedSelections) {
241
+ updateSavedSelections(savedSelections);
242
+ }
243
+
244
+ extensionData.isConnected = true;
245
+ extensionData.lastUpdateTime = Date.now();
246
+ };
247
+
248
+ export const updateConnection = (sessionId = null) => {
249
+ const isNewSession = sessionId && sessionId !== extensionData.currentSessionId;
250
+ if (isNewSession) {
251
+ extensionData.currentSessionId = sessionId;
252
+ extensionData.sessionStartedAt = Date.now();
253
+ }
254
+ extensionData.isConnected = true;
255
+ extensionData.lastUpdateTime = Date.now();
256
+ schedulePersist(extensionData);
257
+ };
258
+
259
+ export const updateDisconnect = (reason = 'explicit') => {
260
+ void reason; // reason bilgisi connectionHealth event'larında tutuluyor
261
+ extensionData.isConnected = false;
262
+ extensionData.lastUpdateTime = Date.now();
263
+ };
264
+
265
+ /**
266
+ * Clear all content-script-processed pending requests.
267
+ * Called when extension connection is lost (stale/timeout) to prevent orphan requests
268
+ * from causing tool timeouts. Mirrors the logic in /api/clear-all-requests route.
269
+ */
270
+ export const clearAllPendingRequests = (data = extensionData) => {
271
+ data.jsExecutionRequest = null;
272
+ data.actionExecutionRequest = null;
273
+ data.captureScreenshotRequest = null;
274
+ data.rawNetworkRequests = [];
275
+ data.analyzePageRequests = [];
276
+ data.selectElementRequest = null;
277
+ };
278
+
279
+ /**
280
+ * Add a captured API endpoint to extension state for later profile persistence.
281
+ * Dedup by (domain, method, url). Called by discover_apis tool.
282
+ * @param {{domain: string, method: string, url: string, status?: number, contentType?: string}} endpoint
283
+ */
284
+ export const addCapturedEndpoint = (endpoint) => {
285
+ if (!endpoint || !endpoint.domain || !endpoint.url) return;
286
+ const key = `${endpoint.domain}::${(endpoint.method || 'GET').toUpperCase()}::${endpoint.url}`;
287
+ const exists = extensionData.apiEndpoints.some(
288
+ (e) => `${e.domain}::${(e.method || 'GET').toUpperCase()}::${e.url}` === key
289
+ );
290
+ if (exists) {
291
+ // Update timestamp + status if changed
292
+ const idx = extensionData.apiEndpoints.findIndex(
293
+ (e) => `${e.domain}::${(e.method || 'GET').toUpperCase()}::${e.url}` === key
294
+ );
295
+ extensionData.apiEndpoints[idx] = {
296
+ ...extensionData.apiEndpoints[idx],
297
+ ...endpoint,
298
+ lastSeenAt: Date.now()
299
+ };
300
+ } else {
301
+ const now = Date.now();
302
+ extensionData.apiEndpoints.push({
303
+ ...endpoint,
304
+ method: (endpoint.method || 'GET').toUpperCase(),
305
+ firstSeenAt: now,
306
+ lastSeenAt: now
307
+ });
308
+ }
309
+ extensionData.lastUpdateTime = Date.now();
310
+ schedulePersist(extensionData);
311
+ };
312
+
313
+ /**
314
+ * Get captured endpoints filtered by domain. Used by manage_site_profile save action.
315
+ * @param {string} domain
316
+ * @returns {Array}
317
+ */
318
+ export const getCapturedEndpoints = (domain) => {
319
+ if (!domain) return extensionData.apiEndpoints;
320
+ return extensionData.apiEndpoints.filter((e) => e.domain === domain);
321
+ };
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Action Tools for AI RPA Integration
3
+ * Yapay zeka asistanlarının DOM üzerinde otonom işlemler (tıklama, doldurma) yapmasını sağlar
4
+ *
5
+ * Phase 2.4: Refactored from (args, extensionData, httpPort) to (args, bridgeClient).
6
+ */
7
+
8
+ import { runScript } from '../utils/run-script.js';
9
+ import { VALIDATION_SCRIPT, formatAlternatives, rankSimilarElements } from '../utils/selector-validator.js';
10
+ import { StateValidator } from '../utils/state-validator.js';
11
+
12
+ export const executeActionTool = {
13
+ name: 'execute_action',
14
+ description: `This is a tool from the dombridge MCP server.
15
+ Perform safe, autonomous interactions with a Chrome tab (click, type, scroll).
16
+
17
+ WORKFLOW POSITION: 🟠 Third Step - Use after getting elements
18
+
19
+ PREREQUISITES:
20
+ - ✅ Extension must be connected
21
+ - Appropriate element selectors (from get_element or execute_js)
22
+
23
+ MULTI-TAB: Call debug_mcp_state() first to get tab IDs, then pass tabId to target a specific tab.
24
+
25
+ SECURITY & LIMITS:
26
+ - 🛡️ Automatically blocked on banking/crypto domains (Garanti, İş Bankası, Binance, etc.).
27
+ - 🛡️ Password and hidden inputs are always blocked.
28
+ - ⏱️ Operations are queued and executed via isolated action-engine.
29
+
30
+ PARAMETERS:
31
+ - actionType: 'click', 'type', 'pressKey', or 'scroll'
32
+ - selectorInfo: Object with 'css', 'xpath', or 'text' selector
33
+ - text:
34
+ - For 'type': text to enter
35
+ - For 'pressKey': key name e.g. 'Enter', 'Tab', 'Escape'
36
+ - For 'scroll': "y" pixels (e.g. "500") or "x,y" (e.g. "0,800"). selectorInfo is optional for scroll.
37
+ - url: Current page URL (for domain safety validation — REQUIRED)
38
+
39
+ pressKey dispatches a full keydown/keypress/keyup sequence compatible with React SPA event systems.
40
+ For React forms, use pressKey with key='Enter' to submit instead of native form.submit().
41
+
42
+ BEST PRACTICES:
43
+ - ⚠️ PREFER 'css' or 'xpath' selectors. 'text' selectors may fail on complex/nested DOM.
44
+ - CSS selectors are pre-validated before the action fires — no need for a separate execute_js check step.
45
+ - Use skipValidation: true only if the element appears after a dynamic event (animation, lazy-load).
46
+
47
+ WORKFLOW:
48
+ 1. execute_action → selector is auto-validated (exists, visible, not occluded) before firing
49
+
50
+ EXAMPLES:
51
+ execute_action({ actionType: 'click', selectorInfo: { css: 'button.submit' }, url: 'https://example.com' })
52
+ execute_action({ actionType: 'scroll', selectorInfo: { css: 'body' }, text: '800', url: 'https://example.com' })
53
+ `,
54
+ inputSchema: {
55
+ type: 'object',
56
+ properties: {
57
+ actionType: {
58
+ type: 'string',
59
+ enum: ['click', 'type', 'pressKey', 'scroll', 'navigate'],
60
+ description: 'Type of action to perform. navigate: go to a URL in the current tab (pass target URL in text parameter)'
61
+ },
62
+ selectorInfo: {
63
+ type: 'object',
64
+ properties: {
65
+ css: { type: 'string' },
66
+ xpath: { type: 'string' },
67
+ text: { type: 'string', description: '⚠️ UNRELIABLE on nested DOM. Use xpath or css instead.' }
68
+ },
69
+ description: 'Selector to find the element. Provide at least one valid locator. PREFER xpath or css over text.'
70
+ },
71
+ text: {
72
+ type: 'string',
73
+ description: 'The text to enter (type), key name (pressKey), scroll amount px (scroll), or target URL (navigate)'
74
+ },
75
+ url: {
76
+ type: 'string',
77
+ description: 'The target URL of the active tab for domain safety check'
78
+ },
79
+ tabId: {
80
+ type: 'number',
81
+ description: 'Target tab ID (optional). Omit to use active tab. Get IDs from debug_mcp_state().'
82
+ },
83
+ skipValidation: {
84
+ type: 'boolean',
85
+ description: 'Skip pre-action selector validation (default: false). Use only when element appears after a dynamic event (animation, lazy-load) that the validator cannot see.',
86
+ default: false
87
+ }
88
+ },
89
+ required: ['actionType', 'url']
90
+ },
91
+ handler: async (args, bridgeClient) => {
92
+ // Connection check — use same StateValidator as execute_js for consistency
93
+ const connValidation = StateValidator.validateConnection(bridgeClient);
94
+ if (!connValidation.valid) {
95
+ return StateValidator.formatValidationError(connValidation);
96
+ }
97
+
98
+ const { actionType, selectorInfo, url, tabId, skipValidation = false } = args;
99
+ const text = args.text || ''; // Prevent undefined → Chrome can't serialize undefined
100
+ const normalizedSelectorInfo = selectorInfo || {};
101
+ const requestId = `action-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
102
+
103
+ // Pre-action selector validation (CSS only, skip for scroll + body/html structural elements)
104
+ const css = normalizedSelectorInfo.css;
105
+ if (css && !skipValidation && actionType !== 'scroll') {
106
+ try {
107
+ const v = await runScript(VALIDATION_SCRIPT(css), bridgeClient, 5000, { tabId });
108
+ if (v && !v.found) {
109
+ // Map alternatives to a unified shape, rank by semantic relevance,
110
+ // then format. VALIDATION_SCRIPT returns {tag, id, cls, text}; we
111
+ // convert to {selector, text, attributes} so the ranker can score
112
+ // by placeholder/aria-label/search indicators.
113
+ const candidates = (v.alternatives || []).map(a => ({
114
+ selector: `${a.tag}${a.id ? '#' + a.id : ''}${a.cls ? '.' + a.cls : ''}`,
115
+ text: a.text || '',
116
+ attributes: {}
117
+ }));
118
+ const similarList = rankSimilarElements(candidates, css);
119
+ const similarForFormat = similarList.map(c => ({
120
+ tag: c.selector.split(/[#.]/)[0],
121
+ id: (c.selector.match(/#([^.#]+)/) || [])[1] || null,
122
+ cls: c.selector.split('.').slice(1).join('.'),
123
+ text: c.text
124
+ }));
125
+ const altLines = formatAlternatives(similarForFormat);
126
+ return {
127
+ isError: true,
128
+ content: [{ type: 'text', text:
129
+ `❌ Error: Selector not found: \`${css}\`\nREQUIRED STEPS:\n1. Verify: \`execute_js({ code: "document.querySelector('${css}')" })\`\n2. Use \`get_element()\` to find the correct selector${altLines ? '\n\nSimilar elements on page:\n' + altLines : ''}`
130
+ }]
131
+ };
132
+ }
133
+ if (v && v.found && !v.visible) {
134
+ const ambiguityNote = (v.matchCount || 0) > 1
135
+ ? `\n\nℹ️ Note: \`${css}\` matches ${v.matchCount} elements. \`querySelector\` returns the first one — if that's the wrong element, narrow the selector (e.g. \`${css}:nth-of-type(1)\` or a class/id-based selector).`
136
+ : '';
137
+ return {
138
+ isError: true,
139
+ content: [{ type: 'text', text:
140
+ `❌ Error: Element exists but is not visible: \`${css}\`\nREQUIRED STEPS:\n1. Scroll to it: \`execute_action({ actionType: 'scroll', text: '500', url: '${url}' })\`\n2. Or use \`execute_js\` to check why it is hidden (display:none, visibility:hidden, zero dimensions)${ambiguityNote}`
141
+ }]
142
+ };
143
+ }
144
+ if (v && v.found && v.occluded) {
145
+ return {
146
+ isError: true,
147
+ content: [{ type: 'text', text:
148
+ `❌ Error: Element is blocked by \`${v.occluder || 'another element'}\`\nREQUIRED STEPS:\n1. Close the overlay/modal first\n2. Or use \`execute_js\` to dismiss it`
149
+ }]
150
+ };
151
+ }
152
+ // disabled: warn but proceed (state can change on focus)
153
+ } catch (_) {
154
+ // Validation failure is non-blocking — proceed with action
155
+ }
156
+ }
157
+
158
+ try {
159
+ // Send action request via bridge daemon
160
+ await bridgeClient.queueRequest('execute-action', {
161
+ actionType,
162
+ selectorInfo: normalizedSelectorInfo,
163
+ text,
164
+ url,
165
+ id: requestId,
166
+ ...(tabId !== undefined ? { tabId } : {})
167
+ });
168
+
169
+ // Wait for extension to process and return result
170
+ const timeout = 10000;
171
+ const resultItem = await bridgeClient.waitForResult('action-execution', requestId, timeout + 3000);
172
+
173
+ if (resultItem) {
174
+ if (resultItem.result && resultItem.result.error) {
175
+ return {
176
+ content: [
177
+ {
178
+ type: 'text',
179
+ text: `❌ Action Error: ${resultItem.result.error}\n\nReview safety-guard restrictions or element selectors.`
180
+ }
181
+ ],
182
+ isError: true
183
+ };
184
+ }
185
+
186
+ return {
187
+ content: [
188
+ {
189
+ type: 'text',
190
+ text: `✅ Action executed successfully:\n\n${JSON.stringify(resultItem.result, null, 2)}`
191
+ }
192
+ ]
193
+ };
194
+ }
195
+
196
+ return {
197
+ content: [
198
+ {
199
+ type: 'text',
200
+ text: `❌ Action Timeout: The extension did not report back within ${timeout}ms. Check if the page is still loading or if Chrome disconnected.`
201
+ }
202
+ ],
203
+ isError: true
204
+ };
205
+
206
+ } catch (e) {
207
+ return {
208
+ content: [
209
+ {
210
+ type: 'text',
211
+ text: `❌ Server Error: ${e.message}`
212
+ }
213
+ ],
214
+ isError: true
215
+ };
216
+ }
217
+ }
218
+ };