@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,290 @@
1
+ /**
2
+ * HTTP Bridge Server Setup
3
+ * Express server with Singleton Gateway, Heartbeat, and Graceful Shutdown
4
+ *
5
+ * Improvements:
6
+ * - Singleton pattern: prevents port conflicts between multiple agent instances
7
+ * - Heartbeat tracking: detects stale connections automatically
8
+ * - Graceful shutdown: properly closes connections on SIGINT/SIGTERM
9
+ * - Connection health logging: timestamps for diagnostics
10
+ */
11
+
12
+ import express from 'express';
13
+ import { setupMiddleware, errorHandler } from './middleware.js';
14
+ import { setupRoutes } from './routes.js';
15
+ import {
16
+ CONNECTION_STALE_TIMEOUT_MS,
17
+ STALE_MONITOR_CHECK_INTERVAL_MS,
18
+ HEALTH_CHECK_FETCH_TIMEOUT_MS,
19
+ CONNECTION_HEALTH_MAX_EVENTS,
20
+ } from '../constants.js';
21
+ import { logger } from '../utils/logger.js';
22
+ import { clearAllPendingRequests } from '../state/extension-state.js';
23
+ import { persistStateNow } from '../state/bridge-persistence.js';
24
+
25
+ /**
26
+ * Connection health tracker
27
+ * Monitors extension heartbeats and detects stale connections
28
+ */
29
+ export const connectionHealth = {
30
+ lastHeartbeat: null,
31
+ heartbeatCount: 0,
32
+ connectionStartedAt: null,
33
+ disconnectedAt: null,
34
+ reconnectCount: 0,
35
+ staleTimeoutMs: CONNECTION_STALE_TIMEOUT_MS,
36
+ events: [], // last CONNECTION_HEALTH_MAX_EVENTS connection events
37
+ currentSessionId: null,
38
+ knownSessionIds: new Set(), // Track all ever-seen session IDs to avoid multi-tab false reconnects
39
+ serverStartedAt: Date.now(), // Phase 1.4: Server start timestamp for restart detection
40
+
41
+ recordHeartbeat(sessionId = null) {
42
+ const now = Date.now();
43
+ const wasStale = this.isStale();
44
+ // Only count as "new session" if this session ID has NEVER been seen before
45
+ const isNewSession = sessionId && !this.knownSessionIds.has(sessionId);
46
+
47
+ this.lastHeartbeat = now;
48
+ this.heartbeatCount++;
49
+ if (sessionId) {
50
+ this.knownSessionIds.add(sessionId);
51
+ // Cleanup: cap at 100 to prevent unbounded growth
52
+ if (this.knownSessionIds.size > 100) {
53
+ const iter = this.knownSessionIds.values();
54
+ for (let i = 0; i < 50; i++) {
55
+ this.knownSessionIds.delete(iter.next().value);
56
+ }
57
+ }
58
+ this.currentSessionId = sessionId;
59
+ }
60
+
61
+ if (!this.connectionStartedAt || wasStale || isNewSession) {
62
+ // New connection, reconnection after stale, or truly new page session
63
+ if (this.connectionStartedAt && (wasStale || isNewSession)) {
64
+ this.reconnectCount++;
65
+ this.addEvent(isNewSession ? 'new-session' : 'reconnected');
66
+ } else {
67
+ this.addEvent('connected');
68
+ }
69
+ this.connectionStartedAt = now;
70
+ this.disconnectedAt = null;
71
+ }
72
+ },
73
+
74
+ isStale() {
75
+ if (!this.lastHeartbeat) return true;
76
+ return (Date.now() - this.lastHeartbeat) > this.staleTimeoutMs;
77
+ },
78
+
79
+ markDisconnected(reason = 'timeout') {
80
+ this.disconnectedAt = Date.now();
81
+ this.addEvent(`disconnected:${reason}`);
82
+ },
83
+
84
+ addEvent(type) {
85
+ this.events.push({
86
+ type,
87
+ timestamp: Date.now(),
88
+ time: new Date().toISOString()
89
+ });
90
+ if (this.events.length > CONNECTION_HEALTH_MAX_EVENTS) {
91
+ this.events.shift();
92
+ }
93
+ },
94
+
95
+ getStatus() {
96
+ const stale = this.isStale();
97
+ const uptimeMs = this.connectionStartedAt
98
+ ? Date.now() - this.connectionStartedAt
99
+ : 0;
100
+ const lastHeartbeatAge = this.lastHeartbeat
101
+ ? Math.floor((Date.now() - this.lastHeartbeat) / 1000)
102
+ : null;
103
+
104
+ return {
105
+ connected: !stale && !!this.lastHeartbeat,
106
+ stale,
107
+ lastHeartbeat: this.lastHeartbeat
108
+ ? new Date(this.lastHeartbeat).toISOString()
109
+ : null,
110
+ lastHeartbeatAge,
111
+ heartbeatCount: this.heartbeatCount,
112
+ uptimeMs,
113
+ uptimeHuman: formatUptime(uptimeMs),
114
+ reconnectCount: this.reconnectCount,
115
+ currentSessionId: this.currentSessionId,
116
+ knownSessionCount: this.knownSessionIds.size,
117
+ recentEvents: this.events.slice(-5)
118
+ };
119
+ }
120
+ };
121
+
122
+ function formatUptime(ms) {
123
+ if (ms < 1000) return `${ms}ms`;
124
+ const secs = Math.floor(ms / 1000);
125
+ if (secs < 60) return `${secs}s`;
126
+ const mins = Math.floor(secs / 60);
127
+ if (mins < 60) return `${mins}m ${secs % 60}s`;
128
+ const hours = Math.floor(mins / 60);
129
+ return `${hours}h ${mins % 60}m`;
130
+ }
131
+
132
+ /**
133
+ * Stale connection monitor
134
+ * Periodically checks if extension connection is alive
135
+ */
136
+ let staleCheckInterval = null;
137
+
138
+ function startStaleMonitor(extensionData) {
139
+ if (staleCheckInterval) clearInterval(staleCheckInterval);
140
+
141
+ staleCheckInterval = setInterval(() => {
142
+ if (connectionHealth.lastHeartbeat && connectionHealth.isStale()) {
143
+ if (extensionData.isConnected) {
144
+ extensionData.isConnected = false;
145
+ clearAllPendingRequests(extensionData);
146
+ connectionHealth.markDisconnected('timeout');
147
+ logger.warn('Bridge', `Extension connection stale (no heartbeat for ${Math.round(CONNECTION_STALE_TIMEOUT_MS/1000)}s) — pending requests cleared`);
148
+ }
149
+ }
150
+ }, STALE_MONITOR_CHECK_INTERVAL_MS);
151
+ }
152
+
153
+ /**
154
+ * Check if existing instance is healthy
155
+ */
156
+ async function checkExistingInstance(port) {
157
+ try {
158
+ const controller = new AbortController();
159
+ const timeout = setTimeout(() => controller.abort(), HEALTH_CHECK_FETCH_TIMEOUT_MS);
160
+ const response = await fetch(`http://localhost:${port}/health`, {
161
+ signal: controller.signal
162
+ });
163
+ clearTimeout(timeout);
164
+ if (response.ok) {
165
+ const data = await response.json();
166
+ return data;
167
+ }
168
+ } catch {
169
+ // Not responding
170
+ }
171
+ return null;
172
+ }
173
+
174
+ export const createHttpServer = (port) => {
175
+ const app = express();
176
+
177
+ // Middleware'leri kur
178
+ setupMiddleware(app);
179
+
180
+ // Route'ları kur
181
+ setupRoutes(app, port);
182
+
183
+ // Error handler
184
+ app.use(errorHandler);
185
+
186
+ return app;
187
+ };
188
+
189
+ export const startHttpServer = async (app, port, extensionData) => {
190
+ let isSecondary = false;
191
+ let httpServer = null;
192
+
193
+ // Check if port is already in use
194
+ const existingHealth = await checkExistingInstance(port);
195
+
196
+ if (existingHealth && existingHealth.status === 'ok') {
197
+ logger.debug('Bridge', `Existing instance on port ${port}, terminating...`);
198
+ try {
199
+ await fetch(`http://localhost:${port}/api/die`, { method: 'POST' }).catch(() => {});
200
+ await new Promise(r => setTimeout(r, 1000));
201
+ } catch (e) {
202
+ logger.debug('Bridge', 'Failed to kill existing instance:', e);
203
+ }
204
+ }
205
+
206
+ // Try to start as primary
207
+ httpServer = await new Promise((resolve) => {
208
+ const server = app.listen(port, '127.0.0.1', () => {
209
+ logger.info('Bridge', `HTTP server started on port ${port} (PRIMARY)`);
210
+ connectionHealth.addEvent('server:started');
211
+ resolve(server);
212
+ });
213
+
214
+ server.on('error', (err) => {
215
+ if (err.code === 'EADDRINUSE') {
216
+ isSecondary = true;
217
+ connectionHealth.addEvent('secondary:port-conflict');
218
+ logger.warn('Bridge', `Port ${port} still busy — running as SECONDARY`);
219
+ resolve(null);
220
+ } else {
221
+ connectionHealth.addEvent(`error:${err.code}`);
222
+ resolve(null);
223
+ }
224
+ });
225
+ });
226
+
227
+ // Only monitor stale connections if we are the primary server
228
+ if (!isSecondary && httpServer) {
229
+ startStaleMonitor(extensionData);
230
+ }
231
+
232
+ // Setup graceful shutdown
233
+ setupGracefulShutdown(httpServer, extensionData);
234
+
235
+ return { httpServer, isSecondary };
236
+ };
237
+
238
+ /**
239
+ * Graceful Shutdown Handler
240
+ * Properly closes HTTP server and cleans up resources
241
+ */
242
+ function setupGracefulShutdown(httpServer, extensionData) {
243
+ let isShuttingDown = false;
244
+
245
+ const shutdown = (signal) => {
246
+ if (isShuttingDown) return;
247
+ isShuttingDown = true;
248
+
249
+ logger.info('Bridge', `${signal} received — shutting down gracefully...`);
250
+
251
+ connectionHealth.addEvent(`shutdown:${signal}`);
252
+
253
+ // Phase 1.1: Persist state before shutdown
254
+ try {
255
+ persistStateNow(extensionData);
256
+ logger.info('Bridge', 'State persisted before shutdown');
257
+ } catch (err) {
258
+ logger.warn('Bridge', 'Failed to persist state before shutdown:', err.message);
259
+ }
260
+
261
+ // Stop stale monitor
262
+ if (staleCheckInterval) {
263
+ clearInterval(staleCheckInterval);
264
+ staleCheckInterval = null;
265
+ }
266
+
267
+ // Close HTTP server
268
+ if (httpServer) {
269
+ httpServer.close(() => {
270
+ logger.info('Bridge', 'HTTP server closed');
271
+ process.exit(0);
272
+ });
273
+
274
+ // Force exit after 5 seconds
275
+ setTimeout(() => {
276
+ process.exit(0);
277
+ }, 5000);
278
+ } else {
279
+ process.exit(0);
280
+ }
281
+ };
282
+
283
+ process.on('SIGINT', () => shutdown('SIGINT'));
284
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
285
+
286
+ // Windows does not support SIGTERM
287
+ if (process.platform === 'win32') {
288
+ process.on('SIGHUP', () => shutdown('SIGHUP'));
289
+ }
290
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * HTTP Bridge Middleware
3
+ * CORS, error handling ve diğer middleware'ler
4
+ */
5
+
6
+ import cors from 'cors';
7
+ import express from 'express';
8
+
9
+ export const setupMiddleware = (app) => {
10
+ // CORS - Only allow localhost and Chrome extensions (not arbitrary origins)
11
+ app.use(cors({
12
+ origin: (origin, callback) => {
13
+ if (
14
+ !origin ||
15
+ origin === 'null' ||
16
+ /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(origin) ||
17
+ /^chrome-extension:\/\//.test(origin)
18
+ ) {
19
+ callback(null, true);
20
+ } else {
21
+ callback(new Error('CORS: origin not allowed'));
22
+ }
23
+ }
24
+ }));
25
+
26
+ // JSON parsing — per-route limits (single middleware, no global pre-parse)
27
+ // Global express.json() is intentionally absent: it would reject large payloads before route checks run.
28
+ const smallJson = express.json({ limit: '4kb' }); // default — unknown/small endpoints
29
+ const mediumJson = express.json({ limit: '1mb' }); // execute-js/action/screenshot
30
+ const largeJson = express.json({ limit: '50mb' }); // network-trace/sync-all/websocket-trace
31
+ const largePaths = ['/api/network-trace', '/api/sync-all', '/api/websocket-trace', '/api/page-analysis', '/api/element-selected'];
32
+ const mediumPaths = ['/api/execute-js', '/api/execute-action', '/api/capture-screenshot', '/api/export-session', '/api/raw-network-requests', '/api/analyze-page'];
33
+ app.use((req, res, next) => {
34
+ if (largePaths.includes(req.path)) return largeJson(req, res, next);
35
+ if (mediumPaths.includes(req.path)) return mediumJson(req, res, next);
36
+ return smallJson(req, res, next);
37
+ });
38
+
39
+ // Request logging (Muted for MCP Stdio JSON-RPC compatibility)
40
+ app.use((req, res, next) => {
41
+ /*
42
+ if (req.path !== '/health') {
43
+ console.error(`[MCP Bridge] ${req.method} ${req.path}`);
44
+ }
45
+ */
46
+ next();
47
+ });
48
+ };
49
+
50
+ export const errorHandler = (err, req, res, _next) => {
51
+ console.error('[MCP Bridge] Error:', err.message);
52
+ res.status(500).json({
53
+ success: false,
54
+ error: err.message
55
+ });
56
+ };