@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,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
|
+
};
|