@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,136 @@
1
+ /**
2
+ * MCP Bridge Module
3
+ * Extension'dan MCP server'a veri gönderen helper fonksiyonları
4
+ * Bu dosyayı extension'da import etmek için kullanın
5
+ */
6
+
7
+ const MCP_SERVER_URL = 'http://localhost:3101';
8
+
9
+ export const MCPBridge = {
10
+ /**
11
+ * Seçili elementi MCP server'a gönder
12
+ */
13
+ async sendSelectedElement(elementData) {
14
+ try {
15
+ const response = await fetch(`${MCP_SERVER_URL}/api/element-selected`, {
16
+ method: 'POST',
17
+ headers: {
18
+ 'Content-Type': 'application/json'
19
+ },
20
+ body: JSON.stringify(elementData)
21
+ });
22
+
23
+ if (!response.ok) {
24
+ throw new Error(`HTTP ${response.status}`);
25
+ }
26
+
27
+ return await response.json();
28
+ } catch (error) {
29
+ console.warn('[MCP Bridge] ❌ Could not reach MCP server:', error.message);
30
+ return { success: false, error: error.message };
31
+ }
32
+ },
33
+
34
+ /**
35
+ * Network trace'i MCP server'a gönder
36
+ */
37
+ async sendNetworkTrace(traceData) {
38
+ try {
39
+ const response = await fetch(`${MCP_SERVER_URL}/api/network-trace`, {
40
+ method: 'POST',
41
+ headers: {
42
+ 'Content-Type': 'application/json'
43
+ },
44
+ body: JSON.stringify(traceData)
45
+ });
46
+
47
+ if (!response.ok) {
48
+ throw new Error(`HTTP ${response.status}`);
49
+ }
50
+
51
+ return await response.json();
52
+ } catch (error) {
53
+ console.warn('[MCP Bridge] ❌ Could not reach MCP server:', error.message);
54
+ return { success: false, error: error.message };
55
+ }
56
+ },
57
+
58
+ /**
59
+ * Kaydedilmiş seçimleri MCP server'a gönder
60
+ */
61
+ async sendSavedSelections(savedSelections) {
62
+ try {
63
+ const response = await fetch(`${MCP_SERVER_URL}/api/saved-selections`, {
64
+ method: 'POST',
65
+ headers: {
66
+ 'Content-Type': 'application/json'
67
+ },
68
+ body: JSON.stringify({ savedSelections })
69
+ });
70
+
71
+ if (!response.ok) {
72
+ throw new Error(`HTTP ${response.status}`);
73
+ }
74
+
75
+ return await response.json();
76
+ } catch (error) {
77
+ console.warn('[MCP Bridge] ❌ Could not reach MCP server:', error.message);
78
+ return { success: false, error: error.message };
79
+ }
80
+ },
81
+
82
+ /**
83
+ * Tüm state'i bir seferde sync et
84
+ */
85
+ async syncAll(data) {
86
+ try {
87
+ const response = await fetch(`${MCP_SERVER_URL}/api/sync-all`, {
88
+ method: 'POST',
89
+ headers: {
90
+ 'Content-Type': 'application/json'
91
+ },
92
+ body: JSON.stringify(data)
93
+ });
94
+
95
+ if (!response.ok) {
96
+ throw new Error(`HTTP ${response.status}`);
97
+ }
98
+
99
+ return await response.json();
100
+ } catch (error) {
101
+ console.warn('[MCP Bridge] ❌ Could not reach MCP server:', error.message);
102
+ return { success: false, error: error.message };
103
+ }
104
+ },
105
+
106
+ /**
107
+ * MCP server'ın çalışıp çalışmadığını kontrol et
108
+ */
109
+ async ping() {
110
+ try {
111
+ const response = await fetch(`${MCP_SERVER_URL}/api/ping`, {
112
+ method: 'POST',
113
+ headers: {
114
+ 'Content-Type': 'application/json'
115
+ }
116
+ });
117
+
118
+ return response.ok;
119
+ } catch {
120
+ return false;
121
+ }
122
+ },
123
+
124
+ /**
125
+ * Health check
126
+ */
127
+ async checkHealth() {
128
+ try {
129
+ const response = await fetch(`${MCP_SERVER_URL}/health`);
130
+ if (!response.ok) return null;
131
+ return await response.json();
132
+ } catch {
133
+ return null;
134
+ }
135
+ }
136
+ };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Error Code Catalog
3
+ * Standardized error codes for tool_calls.error_code field
4
+ */
5
+ export const ErrorCodes = {
6
+ E_TIMEOUT: 'E_TIMEOUT',
7
+ E_CSP_BLOCKED: 'E_CSP_BLOCKED',
8
+ E_NOT_FOUND: 'E_NOT_FOUND',
9
+ E_NOT_VISIBLE: 'E_NOT_VISIBLE',
10
+ E_SELECTOR_INVALID: 'E_SELECTOR_INVALID',
11
+ E_VALIDATION: 'E_VALIDATION',
12
+ E_RATE_LIMITED: 'E_RATE_LIMITED',
13
+ E_CIRCUIT_OPEN: 'E_CIRCUIT_OPEN',
14
+ E_STALE_CONNECTION: 'E_STALE_CONNECTION',
15
+ E_BRIDGE_DOWN: 'E_BRIDGE_DOWN',
16
+ E_DATA_DROPPED: 'E_DATA_DROPPED',
17
+ E_SECURITY: 'E_SECURITY',
18
+ E_UNKNOWN: 'E_UNKNOWN',
19
+ };
20
+
21
+ export const ResultTypes = {
22
+ SUCCESS: 'success',
23
+ ERROR: 'error',
24
+ TIMEOUT: 'timeout',
25
+ CIRCUIT_OPEN: 'circuit_open',
26
+ NULL_RESULT: 'null_result',
27
+ STALE_CONNECTION: 'stale_connection',
28
+ DATA_DROPPED: 'data_dropped',
29
+ };
30
+
31
+ export const ConnectionEventTypes = {
32
+ DISCONNECTED: 'disconnected',
33
+ RECONNECTING: 'reconnecting',
34
+ RECONNECTED: 'reconnected',
35
+ HEARTBEAT_TIMEOUT: 'heartbeat_timeout',
36
+ STALE_DETECTED: 'stale_detected',
37
+ CIRCUIT_BREAKER_OPEN: 'circuit_breaker_open',
38
+ CIRCUIT_BREAKER_CLOSE: 'circuit_breaker_close',
39
+ FULL_SYNC_FAILED: 'full_sync_failed',
40
+ POLLING_STALL: 'polling_stall',
41
+ BRIDGE_SPAWN: 'bridge_spawn',
42
+ BRIDGE_CRASH: 'bridge_crash',
43
+ DATA_DROPPED: 'data_dropped',
44
+ };
@@ -0,0 +1,3 @@
1
+ export { MetricsDB } from './metrics-db.js';
2
+ export { withMetrics, setMetricsDB } from './metrics-recorder.js';
3
+ export { ErrorCodes, ResultTypes, ConnectionEventTypes } from './error-codes.js';
@@ -0,0 +1,269 @@
1
+ /**
2
+ * MetricsDB — SQLite interface for tool analytics
3
+ *
4
+ * Stores every MCP tool call result and connection event.
5
+ * Uses WAL mode for concurrent reads/writes.
6
+ * Graceful degradation: if SQLite init fails, metrics are silently skipped.
7
+ */
8
+ import Database from 'better-sqlite3';
9
+ import { mkdirSync, existsSync } from 'fs';
10
+ import { dirname } from 'path';
11
+ import { homedir } from 'os';
12
+ import { join } from 'path';
13
+ import { logger } from '../utils/logger.js';
14
+
15
+ const DEFAULT_DB_PATH = join(homedir(), '.dombridge', 'metrics.db');
16
+
17
+ const CREATE_TOOL_CALLS = `
18
+ CREATE TABLE IF NOT EXISTS tool_calls (
19
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
20
+ timestamp TEXT NOT NULL,
21
+ domain TEXT,
22
+ tool TEXT NOT NULL,
23
+ action TEXT,
24
+ success INTEGER NOT NULL,
25
+ result_type TEXT NOT NULL,
26
+ error_code TEXT,
27
+ error_message TEXT,
28
+ duration_ms INTEGER,
29
+ selector TEXT,
30
+ retry_count INTEGER DEFAULT 0,
31
+ circuit_breaker_state TEXT,
32
+ tab_id INTEGER,
33
+ metadata TEXT,
34
+ extension_version TEXT,
35
+ bridge_connected INTEGER DEFAULT 1
36
+ )`;
37
+
38
+ const CREATE_CONNECTION_EVENTS = `
39
+ CREATE TABLE IF NOT EXISTS connection_events (
40
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
41
+ timestamp TEXT NOT NULL,
42
+ event_type TEXT NOT NULL,
43
+ duration_ms INTEGER,
44
+ retry_count INTEGER,
45
+ failure_reason TEXT,
46
+ metadata TEXT
47
+ )`;
48
+
49
+ const CREATE_TOOL_SNAPSHOTS = `
50
+ CREATE TABLE IF NOT EXISTS tool_snapshots (
51
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
52
+ date TEXT NOT NULL,
53
+ tool TEXT NOT NULL,
54
+ domain TEXT,
55
+ total_calls INTEGER,
56
+ success_count INTEGER,
57
+ fail_count INTEGER,
58
+ null_count INTEGER,
59
+ timeout_count INTEGER,
60
+ circuit_open_count INTEGER,
61
+ data_dropped_count INTEGER,
62
+ avg_duration_ms INTEGER,
63
+ max_duration_ms INTEGER,
64
+ p95_duration_ms INTEGER,
65
+ error_breakdown TEXT,
66
+ result_type_breakdown TEXT,
67
+ UNIQUE(date, tool, domain)
68
+ )`;
69
+
70
+ const CREATE_INDEXES = [
71
+ 'CREATE INDEX IF NOT EXISTS idx_tc_tool ON tool_calls(tool)',
72
+ 'CREATE INDEX IF NOT EXISTS idx_tc_domain ON tool_calls(domain)',
73
+ 'CREATE INDEX IF NOT EXISTS idx_tc_timestamp ON tool_calls(timestamp)',
74
+ 'CREATE INDEX IF NOT EXISTS idx_tc_result_type ON tool_calls(result_type)',
75
+ 'CREATE INDEX IF NOT EXISTS idx_tc_tool_success ON tool_calls(tool, success)',
76
+ 'CREATE INDEX IF NOT EXISTS idx_ce_event_type ON connection_events(event_type)',
77
+ 'CREATE INDEX IF NOT EXISTS idx_ce_timestamp ON connection_events(timestamp)',
78
+ 'CREATE INDEX IF NOT EXISTS idx_ts_date ON tool_snapshots(date)',
79
+ 'CREATE INDEX IF NOT EXISTS idx_ts_tool ON tool_snapshots(tool)',
80
+ ];
81
+
82
+ export class MetricsDB {
83
+ constructor(dbPath = DEFAULT_DB_PATH) {
84
+ this.available = false;
85
+ this.db = null;
86
+ this._stmts = null;
87
+
88
+ try {
89
+ const dir = dirname(dbPath);
90
+ if (!existsSync(dir)) {
91
+ mkdirSync(dir, { recursive: true });
92
+ }
93
+ this.db = new Database(dbPath);
94
+ this.db.pragma('journal_mode = WAL');
95
+ this.db.pragma('synchronous = NORMAL');
96
+ this._initTables();
97
+ this._prepareStatements();
98
+ this.available = true;
99
+ logger.info('Metrics', `SQLite initialized at ${dbPath}`);
100
+ } catch (err) {
101
+ logger.warn('Metrics', `SQLite init failed, metrics will be skipped: ${err.message}`);
102
+ }
103
+ }
104
+
105
+ _initTables() {
106
+ this.db.exec(CREATE_TOOL_CALLS);
107
+ this.db.exec(CREATE_CONNECTION_EVENTS);
108
+ this.db.exec(CREATE_TOOL_SNAPSHOTS);
109
+ for (const sql of CREATE_INDEXES) {
110
+ this.db.exec(sql);
111
+ }
112
+ }
113
+
114
+ _prepareStatements() {
115
+ this._stmts = {
116
+ insertToolCall: this.db.prepare(`
117
+ INSERT INTO tool_calls (timestamp, domain, tool, action, success, result_type,
118
+ error_code, error_message, duration_ms, selector, retry_count,
119
+ circuit_breaker_state, tab_id, metadata, extension_version, bridge_connected)
120
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
121
+ `),
122
+ insertConnectionEvent: this.db.prepare(`
123
+ INSERT INTO connection_events (timestamp, event_type, duration_ms, retry_count, failure_reason, metadata)
124
+ VALUES (?, ?, ?, ?, ?, ?)
125
+ `),
126
+ };
127
+ }
128
+
129
+ recordToolCall(data) {
130
+ if (!this.available) return;
131
+ try {
132
+ this._stmts.insertToolCall.run(
133
+ data.timestamp || new Date().toISOString(),
134
+ data.domain || null,
135
+ data.tool,
136
+ data.action || null,
137
+ data.success ? 1 : 0,
138
+ data.result_type || 'error',
139
+ data.error_code || null,
140
+ (data.error_message || '').slice(0, 200),
141
+ data.duration_ms || null,
142
+ data.selector || null,
143
+ data.retry_count || 0,
144
+ data.circuit_breaker_state || null,
145
+ data.tab_id || null,
146
+ data.metadata || null,
147
+ data.extension_version || null,
148
+ data.bridge_connected !== undefined ? (data.bridge_connected ? 1 : 0) : 1,
149
+ );
150
+ } catch (err) {
151
+ logger.warn('Metrics', `Failed to record tool call: ${err.message}`);
152
+ }
153
+ }
154
+
155
+ recordConnectionEvent(data) {
156
+ if (!this.available) return;
157
+ try {
158
+ this._stmts.insertConnectionEvent.run(
159
+ data.timestamp || new Date().toISOString(),
160
+ data.event_type,
161
+ data.duration_ms || null,
162
+ data.retry_count || null,
163
+ data.failure_reason || null,
164
+ data.metadata || null,
165
+ );
166
+ } catch (err) {
167
+ logger.warn('Metrics', `Failed to record connection event: ${err.message}`);
168
+ }
169
+ }
170
+
171
+ generateSnapshot(date) {
172
+ if (!this.available) return;
173
+ try {
174
+ const nextDay = new Date(date);
175
+ nextDay.setDate(nextDay.getDate() + 1);
176
+ const nextDayStr = nextDay.toISOString().slice(0, 10);
177
+
178
+ const rows = this.db.prepare(`
179
+ SELECT tool, domain, success, result_type, duration_ms, error_code
180
+ FROM tool_calls
181
+ WHERE timestamp >= ? AND timestamp < ?
182
+ `).all(date, nextDayStr);
183
+
184
+ const groups = {};
185
+ for (const row of rows) {
186
+ const key = `${row.tool}::${row.domain || '(none)'}`;
187
+ if (!groups[key]) {
188
+ groups[key] = {
189
+ tool: row.tool, domain: row.domain, total: 0, success: 0, fail: 0,
190
+ null_result: 0, timeout: 0, circuit_open: 0, data_dropped: 0,
191
+ durations: [], errors: {}, resultTypes: {},
192
+ };
193
+ }
194
+ const g = groups[key];
195
+ g.total++;
196
+ if (row.success) g.success++;
197
+ else g.fail++;
198
+ g.resultTypes[row.result_type] = (g.resultTypes[row.result_type] || 0) + 1;
199
+ if (row.result_type === 'null_result') g.null_result++;
200
+ if (row.result_type === 'timeout') g.timeout++;
201
+ if (row.result_type === 'circuit_open') g.circuit_open++;
202
+ if (row.result_type === 'data_dropped') g.data_dropped++;
203
+ if (row.duration_ms != null) g.durations.push(row.duration_ms);
204
+ if (row.error_code) g.errors[row.error_code] = (g.errors[row.error_code] || 0) + 1;
205
+ }
206
+
207
+ const upsert = this.db.prepare(`
208
+ INSERT INTO tool_snapshots (date, tool, domain, total_calls, success_count, fail_count,
209
+ null_count, timeout_count, circuit_open_count, data_dropped_count,
210
+ avg_duration_ms, max_duration_ms, p95_duration_ms, error_breakdown, result_type_breakdown)
211
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
212
+ ON CONFLICT(date, tool, domain) DO UPDATE SET
213
+ total_calls=excluded.total_calls, success_count=excluded.success_count,
214
+ fail_count=excluded.fail_count, null_count=excluded.null_count,
215
+ timeout_count=excluded.timeout_count, circuit_open_count=excluded.circuit_open_count,
216
+ data_dropped_count=excluded.data_dropped_count,
217
+ avg_duration_ms=excluded.avg_duration_ms, max_duration_ms=excluded.max_duration_ms,
218
+ p95_duration_ms=excluded.p95_duration_ms, error_breakdown=excluded.error_breakdown,
219
+ result_type_breakdown=excluded.result_type_breakdown
220
+ `);
221
+
222
+ for (const g of Object.values(groups)) {
223
+ const durations = g.durations.sort((a, b) => a - b);
224
+ const p95 = durations.length > 0 ? durations[Math.floor(durations.length * 0.95)] : null;
225
+ const avg = durations.length > 0 ? Math.round(durations.reduce((a, b) => a + b, 0) / durations.length) : null;
226
+ const max = durations.length > 0 ? durations[durations.length - 1] : null;
227
+
228
+ upsert.run(date, g.tool, g.domain, g.total, g.success, g.fail,
229
+ g.null_result, g.timeout, g.circuit_open, g.data_dropped,
230
+ avg, max, p95,
231
+ JSON.stringify(g.errors), JSON.stringify(g.resultTypes));
232
+ }
233
+ } catch (err) {
234
+ logger.warn('Metrics', `Failed to generate snapshot: ${err.message}`);
235
+ }
236
+ }
237
+
238
+ query(filters = {}) {
239
+ if (!this.available) return [];
240
+ try {
241
+ const conditions = [];
242
+ const params = [];
243
+ if (filters.tool) { conditions.push('tool = ?'); params.push(filters.tool); }
244
+ if (filters.domain) { conditions.push('domain = ?'); params.push(filters.domain); }
245
+ if (filters.success !== undefined) { conditions.push('success = ?'); params.push(filters.success ? 1 : 0); }
246
+ if (filters.result_type) { conditions.push('result_type = ?'); params.push(filters.result_type); }
247
+ if (filters.days) {
248
+ const since = new Date(Date.now() - filters.days * 86400000).toISOString();
249
+ conditions.push('timestamp >= ?'); params.push(since);
250
+ }
251
+ if (filters.today) {
252
+ const today = new Date().toISOString().slice(0, 10);
253
+ conditions.push('timestamp >= ?'); params.push(today);
254
+ }
255
+
256
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
257
+ return this.db.prepare(`SELECT * FROM tool_calls ${where} ORDER BY timestamp DESC LIMIT 1000`).all(...params);
258
+ } catch (err) {
259
+ logger.warn('Metrics', `Query failed: ${err.message}`);
260
+ return [];
261
+ }
262
+ }
263
+
264
+ close() {
265
+ if (this.db) {
266
+ try { this.db.close(); } catch { /* ignore */ }
267
+ }
268
+ }
269
+ }
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Metrics Recorder — withMetrics wrapper for tool handlers
3
+ *
4
+ * Wraps tool handlers to record success/fail/timeout/circuit_open/stale
5
+ * results into SQLite via MetricsDB. Normalizes inconsistent isError
6
+ * usage across tools into consistent result_type values.
7
+ */
8
+ import { ErrorCodes, ResultTypes } from './error-codes.js';
9
+
10
+ let metricsDB = null;
11
+
12
+ /**
13
+ * Inject the MetricsDB instance. Called once at startup.
14
+ */
15
+ export function setMetricsDB(db) {
16
+ metricsDB = db;
17
+ }
18
+
19
+ /**
20
+ * Normalize result_type from inconsistent tool handler responses.
21
+ *
22
+ * Tool handlers return isError inconsistently:
23
+ * - Some set isError: true for errors
24
+ * - Some return timeout messages without isError
25
+ * - Some throw Error objects
26
+ *
27
+ * This function normalizes them into a consistent result_type.
28
+ */
29
+ function normalizeResultType(result, error) {
30
+ if (error) {
31
+ const msg = error.message || '';
32
+ if (msg.includes('Circuit breaker OPEN') || msg.includes('Circuit breaker HALF_OPEN')) {
33
+ return ResultTypes.CIRCUIT_OPEN;
34
+ }
35
+ return ResultTypes.ERROR;
36
+ }
37
+
38
+ if (!result) {
39
+ return ResultTypes.NULL_RESULT;
40
+ }
41
+
42
+ const text = result.content?.[0]?.text || '';
43
+
44
+ if (result.isError) {
45
+ if (text.includes('did not respond within') || text.includes('Timeout') || text.includes('timeout')) {
46
+ return ResultTypes.TIMEOUT;
47
+ }
48
+ if (text.includes('not connected') || text.includes('Extension not connected')) {
49
+ return ResultTypes.STALE_CONNECTION;
50
+ }
51
+ return ResultTypes.ERROR;
52
+ }
53
+
54
+ // Defensive fallback: even if isError:true is missing, detect error text patterns.
55
+ // Some tool handlers (e.g. execute-js.js hard guards) returned { content: [...] }
56
+ // without isError flag, causing false-success in metrics. See CHANGELOG fix.
57
+ if (text.startsWith('Security Error:') || text.startsWith('Error:') ||
58
+ text.includes('\n❌ Error:') || text.startsWith('Validation') ||
59
+ text.startsWith('⛔')) {
60
+ return ResultTypes.ERROR;
61
+ }
62
+
63
+ return ResultTypes.SUCCESS;
64
+ }
65
+
66
+ /**
67
+ * Extract error code from result or error.
68
+ */
69
+ function extractErrorCode(result, error) {
70
+ if (error) {
71
+ const msg = error.message || '';
72
+ if (msg.includes('Circuit breaker OPEN') || msg.includes('Circuit breaker HALF_OPEN')) return ErrorCodes.E_CIRCUIT_OPEN;
73
+ if (msg.includes('not connected')) return ErrorCodes.E_STALE_CONNECTION;
74
+ return ErrorCodes.E_UNKNOWN;
75
+ }
76
+
77
+ if (!result) return null;
78
+
79
+ const text = result.content?.[0]?.text || '';
80
+
81
+ if (text.includes('did not respond within') || text.includes('Timeout') || text.includes('timeout')) return ErrorCodes.E_TIMEOUT;
82
+ if (text.includes('CSP') || text.includes('Content Security Policy')) return ErrorCodes.E_CSP_BLOCKED;
83
+ if (text.includes('not found') || text.includes('not visible') || text.includes('occluded')) return ErrorCodes.E_NOT_FOUND;
84
+ if (text.includes('not visible') || text.includes('blocked by')) return ErrorCodes.E_NOT_VISIBLE;
85
+ if (text.includes('Invalid selector') || text.includes('selector')) return ErrorCodes.E_SELECTOR_INVALID;
86
+ if (text.includes('Validation') || text.includes('validation') || text.includes('required')) return ErrorCodes.E_VALIDATION;
87
+ if (text.includes('Rate limit') || text.includes('rate limit')) return ErrorCodes.E_RATE_LIMITED;
88
+ if (text.includes('Security') || text.includes('blocked') || text.includes('not allowed')) return ErrorCodes.E_SECURITY;
89
+
90
+ return ErrorCodes.E_UNKNOWN;
91
+ }
92
+
93
+ /**
94
+ * Extract domain from a URL string.
95
+ */
96
+ function domainFromUrl(url) {
97
+ if (!url) return null;
98
+ try { return new URL(url).hostname; } catch { return null; }
99
+ }
100
+
101
+ /**
102
+ * Extract domain for a tool call.
103
+ *
104
+ * Priority: bridgeClient.activeTabUrl → args.url → args.tabUrl → null
105
+ * Most tool calls don't receive a `url` arg directly; the active tab's URL
106
+ * from bridge state is the authoritative source for which site the tool
107
+ * was operating on.
108
+ */
109
+ function extractDomain(args, bridgeClient) {
110
+ return domainFromUrl(bridgeClient?.activeTabUrl) ||
111
+ domainFromUrl(args?.url) ||
112
+ domainFromUrl(args?.tabUrl) ||
113
+ null;
114
+ }
115
+
116
+ /**
117
+ * Extract metadata specific to each tool.
118
+ */
119
+ function extractMetadata(toolName, args) {
120
+ const meta = {};
121
+
122
+ if (toolName === 'execute_js' && args.code) {
123
+ meta.code_size = args.code.length;
124
+ if (args.cspBypass !== undefined) meta.csp_bypass = args.cspBypass;
125
+ if (args.context) meta.context = args.context;
126
+ }
127
+
128
+ if (toolName === 'execute_action') {
129
+ if (args.selectorInfo) {
130
+ meta.selector_type = args.selectorInfo.css ? 'css' : args.selectorInfo.xpath ? 'xpath' : args.selectorInfo.text ? 'text' : 'unknown';
131
+ }
132
+ if (args.skipValidation !== undefined) meta.skip_validation = args.skipValidation;
133
+ }
134
+
135
+ if (toolName === 'extract_data') {
136
+ if (args.verbose !== undefined) meta.verbose = args.verbose;
137
+ }
138
+
139
+ if (toolName === 'get_element' && args.selectorInfo) {
140
+ meta.selection_method = args.selectorInfo.css ? 'css' : args.selectorInfo.xpath ? 'xpath' : args.selectorInfo.text ? 'text' : 'manual';
141
+ }
142
+
143
+ return Object.keys(meta).length > 0 ? JSON.stringify(meta) : null;
144
+ }
145
+
146
+ /**
147
+ * Get extension version from package.json or bridge client.
148
+ */
149
+ function getExtensionVersion() {
150
+ // Version will be replaced by actual version lookup from bridge client state
151
+ return '2.7.5';
152
+ }
153
+
154
+ /**
155
+ * Wrap a tool handler with metrics recording.
156
+ *
157
+ * @param {string} toolName - Tool name (e.g., 'execute_js')
158
+ * @param {Function} handler - Original async handler (args, bridgeClient) => result
159
+ * @returns {Function} Wrapped handler
160
+ */
161
+ export function withMetrics(toolName, handler) {
162
+ return async function wrappedHandler(args, bridgeClient) {
163
+ if (!metricsDB || !metricsDB.available) {
164
+ // Metrics not available — pass through without recording
165
+ return handler(args, bridgeClient);
166
+ }
167
+
168
+ const start = Date.now();
169
+ const bridgeConnected = bridgeClient?.isConnected ?? true;
170
+ const circuitBreakerState = bridgeClient?.circuitBreakers?.[toolName]?.state || null;
171
+
172
+ let result = null;
173
+ let error = null;
174
+
175
+ try {
176
+ result = await handler(args, bridgeClient);
177
+ } catch (err) {
178
+ error = err;
179
+ }
180
+
181
+ const duration_ms = Date.now() - start;
182
+ const resultType = normalizeResultType(result, error);
183
+ const errorCode = extractErrorCode(result, error);
184
+ const domain = extractDomain(args, bridgeClient);
185
+ const action = args.actionType || null;
186
+ const selector = args.selectorInfo?.css || args.selectorInfo?.xpath || args.selectorInfo?.text || null;
187
+ const metadata = extractMetadata(toolName, args);
188
+ const errorMessage = error ? error.message : (result?.content?.[0]?.text || '');
189
+ const extensionVersion = getExtensionVersion();
190
+
191
+ // If handler threw, wrap it as MCP error response
192
+ if (error) {
193
+ metricsDB.recordToolCall({
194
+ timestamp: new Date().toISOString(),
195
+ domain,
196
+ tool: toolName,
197
+ action,
198
+ success: 0,
199
+ result_type: resultType,
200
+ error_code: errorCode,
201
+ error_message: errorMessage.slice(0, 200),
202
+ duration_ms,
203
+ selector,
204
+ retry_count: 0,
205
+ circuit_breaker_state: circuitBreakerState || 'not_used',
206
+ tab_id: args.tabId || null,
207
+ metadata,
208
+ extension_version: extensionVersion,
209
+ bridge_connected: bridgeConnected ? 1 : 0,
210
+ });
211
+
212
+ // Return MCP error format
213
+ return {
214
+ content: [{ type: 'text', text: `❌ Error: ${error.message}` }],
215
+ isError: true,
216
+ };
217
+ }
218
+
219
+ metricsDB.recordToolCall({
220
+ timestamp: new Date().toISOString(),
221
+ domain,
222
+ tool: toolName,
223
+ action,
224
+ success: resultType === ResultTypes.SUCCESS ? 1 : 0,
225
+ result_type: resultType,
226
+ error_code: resultType !== ResultTypes.SUCCESS ? errorCode : null,
227
+ error_message: resultType !== ResultTypes.SUCCESS ? errorMessage.slice(0, 200) : null,
228
+ duration_ms,
229
+ selector,
230
+ retry_count: 0,
231
+ circuit_breaker_state: circuitBreakerState || 'not_used',
232
+ tab_id: args.tabId || null,
233
+ metadata,
234
+ extension_version: extensionVersion,
235
+ bridge_connected: bridgeConnected ? 1 : 0,
236
+ });
237
+
238
+ return result;
239
+ };
240
+ }