@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,31 @@
1
+ /**
2
+ * MCP-compliant logger
3
+ *
4
+ * MCP stdio transport uses stdout exclusively for JSON-RPC messages.
5
+ * ALL diagnostic output MUST go to stderr — never stdout.
6
+ *
7
+ * Levels:
8
+ * info — startup, lifecycle events (always visible)
9
+ * warn — recoverable issues (always visible)
10
+ * error — failures (always visible)
11
+ * debug — verbose detail (only when MCP_DEBUG=1)
12
+ */
13
+
14
+ const isDebug = () => process.env.MCP_DEBUG === '1';
15
+
16
+ const PREFIX = '[MCP]';
17
+
18
+ function fmt(level, context, ...args) {
19
+ const tag = context ? `${PREFIX}[${context}]` : PREFIX;
20
+ process.stderr.write(`${tag} ${level} ${args.map(a =>
21
+ a instanceof Error ? a.stack || a.message :
22
+ typeof a === 'object' ? JSON.stringify(a) : String(a)
23
+ ).join(' ')}\n`);
24
+ }
25
+
26
+ export const logger = {
27
+ info: (context, ...args) => fmt('INFO ', context, ...args),
28
+ warn: (context, ...args) => fmt('WARN ', context, ...args),
29
+ error: (context, ...args) => fmt('ERROR', context, ...args),
30
+ debug: (context, ...args) => isDebug() && fmt('DEBUG', context, ...args),
31
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Pagination Strategy Detector
3
+ *
4
+ * Detects the best pagination strategy for a page based on probed page state,
5
+ * next button presence, and infinite scroll detection.
6
+ *
7
+ * Used by tools/paginate.js when strategy='auto' to avoid defaulting to scroll
8
+ * when the page has explicit URL-based pagination metadata (Trendyol, etc.).
9
+ */
10
+
11
+ export function detectPaginationStrategy({ pageState, nextButtonSelector, hasInfiniteScroll } = {}) {
12
+ if (nextButtonSelector) return 'button';
13
+
14
+ const totalPages = pageState?.widgetList?.totalPages
15
+ || pageState?.__NEXT_DATA__?.props?.pageProps?.totalPages
16
+ || pageState?.pagination?.totalPages
17
+ || 0;
18
+
19
+ if (totalPages > 1) return 'url_increment';
20
+
21
+ if (hasInfiniteScroll) return 'scroll';
22
+
23
+ return 'auto';
24
+ }
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Rate Limiter & Retry Strategy
3
+ * Implements exponential backoff with jitter to prevent rapid sequential calls
4
+ *
5
+ * Best Practices:
6
+ * - Exponential backoff: 1s, 2s, 4s, 8s
7
+ * - Jitter: +/- 20% randomization
8
+ * - Per-tool rate limits
9
+ * - Retry-After header support
10
+ *
11
+ * References:
12
+ * - https://docs.anthropic.com/en/api/rate-limits
13
+ * - https://www.anthropic.com/engineering/code-execution-with-mcp
14
+ */
15
+
16
+ /**
17
+ * Rate limit configuration per tool
18
+ */
19
+ const RATE_LIMITS = {
20
+ // Discovery tools
21
+ 'get_selected_element': { minInterval: 500, maxRetries: 5 },
22
+ 'get_network_trace': { minInterval: 1000, maxRetries: 3 },
23
+ 'get_websocket_trace': { minInterval: 1000, maxRetries: 3 },
24
+
25
+ // Low-frequency tools (expensive operations)
26
+ 'execute_js': { minInterval: 2000, maxRetries: 3 },
27
+ 'debug_mcp_state': { minInterval: 100, maxRetries: 2 },
28
+
29
+ // Default fallback
30
+ 'default': { minInterval: 1000, maxRetries: 3 }
31
+ };
32
+
33
+ /**
34
+ * Track last execution time per tool
35
+ */
36
+ const lastExecutionTime = new Map();
37
+
38
+ /**
39
+ * Track retry counts per tool
40
+ */
41
+ const retryCount = new Map();
42
+
43
+ /**
44
+ * Rate Limiter Class
45
+ */
46
+ export class RateLimiter {
47
+ /**
48
+ * Execute a function with rate limiting and retry logic
49
+ * @param {string} toolName - Name of the tool
50
+ * @param {Function} handler - Async function to execute
51
+ * @param {object} options - { maxRetries, minInterval, onRetry }
52
+ * @returns {Promise<any>} - Handler result
53
+ */
54
+ static async executeWithRetry(toolName, handler, options = {}) {
55
+ const config = RATE_LIMITS[toolName] || RATE_LIMITS.default;
56
+ const maxRetries = options.maxRetries ?? config?.maxRetries ?? 3;
57
+ const minInterval = options.minInterval ?? config?.minInterval ?? 1000;
58
+
59
+ let attempt = 0;
60
+ let lastError = null;
61
+
62
+ while (attempt < maxRetries) {
63
+ try {
64
+ // Rate limiting: wait if called too soon
65
+ await this.enforceRateLimit(toolName, minInterval);
66
+
67
+ // Execute handler
68
+ const result = await handler();
69
+
70
+ // Success: reset retry count
71
+ retryCount.delete(toolName);
72
+
73
+ return result;
74
+
75
+ } catch (error) {
76
+ lastError = error;
77
+ attempt++;
78
+
79
+ // Check if error is retryable
80
+ if (!this.isRetryableError(error) || attempt >= maxRetries) {
81
+ throw error;
82
+ }
83
+
84
+ // Calculate backoff delay
85
+ const delay = this.calculateBackoff(attempt, minInterval);
86
+
87
+ // Call retry callback if provided
88
+ if (options.onRetry) {
89
+ options.onRetry(attempt, delay, error);
90
+ }
91
+
92
+ // Wait before retrying
93
+ await this.sleep(delay);
94
+ }
95
+ }
96
+
97
+ // All retries exhausted
98
+ throw new Error(`${toolName} failed after ${maxRetries} attempts: ${lastError?.message || 'Unknown error'}`);
99
+ }
100
+
101
+ /**
102
+ * Enforce rate limit for a tool
103
+ * @param {string} toolName - Tool name
104
+ * @param {number} minInterval - Minimum interval in ms
105
+ */
106
+ static async enforceRateLimit(toolName, minInterval) {
107
+ const lastTime = lastExecutionTime.get(toolName);
108
+
109
+ if (lastTime) {
110
+ const elapsed = Date.now() - lastTime;
111
+ const remaining = minInterval - elapsed;
112
+
113
+ if (remaining > 0) {
114
+ await this.sleep(remaining);
115
+ }
116
+ }
117
+
118
+ // Update last execution time
119
+ lastExecutionTime.set(toolName, Date.now());
120
+ }
121
+
122
+ /**
123
+ * Calculate exponential backoff with jitter
124
+ * @param {number} attempt - Retry attempt number (1-indexed)
125
+ * @param {number} baseDelay - Base delay in ms
126
+ * @returns {number} - Delay in ms
127
+ */
128
+ static calculateBackoff(attempt, baseDelay = 1000) {
129
+ // Exponential backoff: baseDelay * 2^(attempt-1)
130
+ // attempt 1: 1s, attempt 2: 2s, attempt 3: 4s, attempt 4: 8s
131
+ const exponential = baseDelay * Math.pow(2, attempt - 1);
132
+
133
+ // Add jitter: +/- 20%
134
+ const jitter = exponential * 0.2 * (Math.random() * 2 - 1);
135
+
136
+ // Cap at 30 seconds
137
+ const delay = Math.min(exponential + jitter, 30000);
138
+
139
+ return Math.floor(delay);
140
+ }
141
+
142
+ /**
143
+ * Check if error is retryable
144
+ * @param {Error} error - Error object
145
+ * @returns {boolean}
146
+ */
147
+ static isRetryableError(error) {
148
+ const message = error?.message?.toLowerCase() || '';
149
+
150
+ // Retryable errors:
151
+ // - Timeout errors
152
+ // - Connection errors
153
+ // - Rate limit errors (429)
154
+ // - Server errors (5xx)
155
+ // - "Waiting for extension" errors
156
+ const retryablePatterns = [
157
+ 'timeout',
158
+ 'timed out',
159
+ 'connection',
160
+ 'network',
161
+ 'rate limit',
162
+ 'too many requests',
163
+ 'server error',
164
+ '5xx',
165
+ 'waiting for',
166
+ 'no data',
167
+ 'not ready'
168
+ ];
169
+
170
+ const isRetryable = retryablePatterns.some(pattern => message.includes(pattern));
171
+
172
+ // Non-retryable errors:
173
+ // - Validation errors
174
+ // - Missing prerequisites
175
+ // - Invalid arguments
176
+ const nonRetryablePatterns = [
177
+ 'invalid',
178
+ 'missing required',
179
+ 'prerequisite',
180
+ 'not allowed',
181
+ 'forbidden'
182
+ ];
183
+
184
+ const isNonRetryable = nonRetryablePatterns.some(pattern => message.includes(pattern));
185
+
186
+ return isRetryable && !isNonRetryable;
187
+ }
188
+
189
+ /**
190
+ * Sleep utility
191
+ * @param {number} ms - Milliseconds to sleep
192
+ * @returns {Promise<void>}
193
+ */
194
+ static sleep(ms) {
195
+ return new Promise(resolve => setTimeout(resolve, ms));
196
+ }
197
+
198
+ /**
199
+ * Get rate limit stats
200
+ * @returns {object} - Stats object
201
+ */
202
+ static getStats() {
203
+ return {
204
+ lastExecutionTimes: Object.fromEntries(lastExecutionTime),
205
+ retryCounts: Object.fromEntries(retryCount)
206
+ };
207
+ }
208
+
209
+ /**
210
+ * Reset rate limiter state
211
+ * @param {string|null} toolName - Tool name to reset, or null for all
212
+ */
213
+ static reset(toolName = null) {
214
+ if (toolName) {
215
+ lastExecutionTime.delete(toolName);
216
+ retryCount.delete(toolName);
217
+ } else {
218
+ lastExecutionTime.clear();
219
+ retryCount.clear();
220
+ }
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Simple helper: wait with validation
226
+ * @param {number} ms - Milliseconds
227
+ * @param {BridgeClient|object} bridgeClient - State source to check
228
+ * @param {Function} validation - Validation function that returns boolean
229
+ * @param {number} maxWait - Maximum wait time in ms
230
+ * @returns {Promise<boolean>} - True if validation passed, false if timeout
231
+ */
232
+ export async function waitForCondition(ms, bridgeClient, validation, maxWait = 5000) {
233
+ const startTime = Date.now();
234
+
235
+ while (Date.now() - startTime < maxWait) {
236
+ if (validation(bridgeClient)) {
237
+ return true;
238
+ }
239
+
240
+ await RateLimiter.sleep(ms);
241
+ }
242
+
243
+ return false;
244
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * runScript — execute_js pipeline shared helper
3
+ * Tüm tool'lar bu helper'ı kullanır: extract_data, check_site_changes, vb.
4
+ *
5
+ * Phase 2.4: Refactored from (code, extensionData, httpPort, ...) to (code, bridgeClient, ...).
6
+ * Now uses bridgeClient.queueRequest() and bridgeClient.waitForResult() instead of
7
+ * direct HTTP fetch and in-memory result polling.
8
+ */
9
+
10
+ export async function runScript(code, bridgeClient, timeoutMs = 12000, options = {}) {
11
+ const requestId = `script-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
12
+
13
+ await bridgeClient.queueRequest('execute-js', {
14
+ code,
15
+ timeout: timeoutMs,
16
+ id: requestId,
17
+ ...(options.tabId !== undefined ? { tabId: options.tabId } : {})
18
+ });
19
+
20
+ const resultItem = await bridgeClient.waitForResult('js-execution', requestId, timeoutMs + 3000);
21
+
22
+ if (!resultItem) {
23
+ throw new Error('Timeout');
24
+ }
25
+
26
+ // CSP auto-bypass wraps result in { _cspBypassed, result }. Unwrap it.
27
+ let rawResult = resultItem.result;
28
+ if (rawResult && rawResult._cspBypassed !== undefined && rawResult.result !== undefined) {
29
+ rawResult = rawResult.result;
30
+ }
31
+
32
+ if (rawResult && rawResult.error) {
33
+ throw new Error(rawResult.error);
34
+ }
35
+
36
+ return rawResult;
37
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Shared selector validation script.
3
+ * Injected into page context to check element existence, visibility, and suggest alternatives.
4
+ * Used by execute_action (pre-action) and get_element (post-failure).
5
+ */
6
+
7
+ export const VALIDATION_SCRIPT = (css) => `(function() {
8
+ var all = document.querySelectorAll(${JSON.stringify(css)});
9
+ var matchCount = all.length;
10
+ var el = all[0];
11
+ if (!el) {
12
+ var tagMatch = ${JSON.stringify(css)}.match(/^[a-zA-Z]*/);
13
+ var tag = (tagMatch && tagMatch[0]) ? tagMatch[0] : '*';
14
+ var STRUCTURAL_TAGS = ['html','head','body','script','style','meta','link','noscript','title','base','template'];
15
+ var similar = Array.from(document.querySelectorAll(tag))
16
+ .filter(function(e) { return STRUCTURAL_TAGS.indexOf(e.tagName.toLowerCase()) === -1; })
17
+ .slice(0, 3)
18
+ .map(function(e) {
19
+ return {
20
+ tag: e.tagName.toLowerCase(),
21
+ id: e.id || null,
22
+ cls: Array.from(e.classList).slice(0, 3).join('.'),
23
+ text: (e.textContent || '').trim().substring(0, 40)
24
+ };
25
+ });
26
+ return { found: false, matchCount: 0, alternatives: similar };
27
+ }
28
+ var rect = el.getBoundingClientRect();
29
+ var visible = rect.width > 0 && rect.height > 0 && el.offsetParent !== null;
30
+ var cx = rect.left + rect.width / 2;
31
+ var cy = rect.top + rect.height / 2;
32
+ var topEl = document.elementFromPoint(cx, cy);
33
+ var occluded = topEl !== null && topEl !== el && !el.contains(topEl);
34
+ return {
35
+ found: true,
36
+ matchCount: matchCount,
37
+ visible: visible,
38
+ occluded: occluded,
39
+ occluder: occluded ? (topEl.className || topEl.tagName.toLowerCase()) : null,
40
+ disabled: !!(el.disabled || el.getAttribute('aria-disabled') === 'true')
41
+ };
42
+ })()`;
43
+
44
+ /**
45
+ * Format alternatives list from VALIDATION_SCRIPT result.
46
+ * @param {Array} alternatives
47
+ * @returns {string}
48
+ */
49
+ export function formatAlternatives(alternatives) {
50
+ if (!alternatives || alternatives.length === 0) return '';
51
+ // Bug #2 fix: dedup by tag#id.class key to avoid duplicate similar-element lines
52
+ const seen = new Set();
53
+ const unique = alternatives.filter(a => {
54
+ const key = `${a.tag}${a.id ? '#' + a.id : ''}${a.cls ? '.' + a.cls : ''}`;
55
+ if (seen.has(key)) return false;
56
+ seen.add(key);
57
+ return true;
58
+ });
59
+ return unique.map(a =>
60
+ ` - \`${a.tag}${a.id ? '#' + a.id : ''}${a.cls ? '.' + a.cls : ''}\` — "${a.text}"`
61
+ ).join('\n');
62
+ }
63
+
64
+ /**
65
+ * Rank similar element candidates by semantic relevance.
66
+ * Penalizes hidden/cookie/OneTrust patterns and rewards search/semantic
67
+ * indicators (placeholder, aria-label, type=search, name=q, etc.).
68
+ *
69
+ * @param {Array} candidates - Array of { selector, text, attributes } objects.
70
+ * @param {string} originalSelector - The CSS selector that failed (context hint).
71
+ * @returns {Array} New array sorted by descending relevance score.
72
+ */
73
+ export function rankSimilarElements(candidates, _originalSelector) {
74
+ const score = (c) => {
75
+ let s = 0;
76
+ const sel = c.selector || '';
77
+ const attrs = c.attributes || {};
78
+
79
+ // Penalize hidden/cookie/OT patterns
80
+ if (/type=["']?(hidden|checkbox)["']?/i.test(sel)) s -= 50;
81
+ if (/ot-group-id|onetrust|cookie|category-switch-handler/i.test(sel)) s -= 40;
82
+ if (/type=["']?submit["']?/i.test(sel)) s -= 30;
83
+
84
+ // Reward search/semantic
85
+ if (attrs.placeholder || /placeholder/i.test(sel)) s += 30;
86
+ if (attrs['aria-label'] || /aria-label/i.test(sel)) s += 20;
87
+ if (attrs.type === 'search' || /type=["']?search["']?/i.test(sel)) s += 25;
88
+ if (attrs.name === 'q' || /name=["']?q["']?/i.test(sel)) s += 20;
89
+ if (/search|ara|query|find/i.test(sel)) s += 15;
90
+
91
+ return s;
92
+ };
93
+
94
+ return [...candidates].sort((a, b) => score(b) - score(a));
95
+ }