@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.
- package/README.md +559 -0
- package/bin/cli.js +88 -0
- package/package.json +54 -0
- package/src/bridge/http-server.js +290 -0
- package/src/bridge/middleware.js +56 -0
- package/src/bridge/routes.js +1003 -0
- package/src/bridge-daemon.js +172 -0
- package/src/cli/auto-config.js +120 -0
- package/src/constants.js +13 -0
- package/src/index.js +279 -0
- package/src/mcp-bridge.js +136 -0
- package/src/metrics/error-codes.js +44 -0
- package/src/metrics/index.js +3 -0
- package/src/metrics/metrics-db.js +269 -0
- package/src/metrics/metrics-recorder.js +240 -0
- package/src/metrics/metrics-report.js +146 -0
- package/src/profiles/profile-db.js +159 -0
- package/src/profiles/profile-enricher.js +333 -0
- package/src/profiles/profile-manager.js +563 -0
- package/src/profiles/profile-repo.js +183 -0
- package/src/state/bridge-client.js +272 -0
- package/src/state/bridge-persistence.js +205 -0
- package/src/state/cache.js +38 -0
- package/src/state/extension-state.js +321 -0
- package/src/tools/action_tools.js +218 -0
- package/src/tools/analyze-page.js +247 -0
- package/src/tools/debug-mcp-state.js +172 -0
- package/src/tools/discover-apis.js +186 -0
- package/src/tools/execute-js.js +284 -0
- package/src/tools/export-session.js +171 -0
- package/src/tools/extract-data.js +395 -0
- package/src/tools/get-element.js +281 -0
- package/src/tools/get-network-trace.js +471 -0
- package/src/tools/index.js +110 -0
- package/src/tools/manage-site-profile.js +153 -0
- package/src/tools/paginate.js +444 -0
- package/src/tools/quick-scan.js +418 -0
- package/src/tools/screenshot_tools.js +117 -0
- package/src/utils/circuit-breaker.js +112 -0
- package/src/utils/extract-density.js +21 -0
- package/src/utils/logger.js +31 -0
- package/src/utils/paginate-detector.js +24 -0
- package/src/utils/rate-limiter.js +244 -0
- package/src/utils/run-script.js +37 -0
- package/src/utils/selector-validator.js +95 -0
- package/src/utils/state-validator.js +354 -0
- package/src/utils/tab-resolver.js +70 -0
- package/src/utils/workflow-helper.js +292 -0
- 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
|
+
};
|