@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,1003 @@
1
+ /**
2
+ * HTTP Bridge Routes
3
+ * Extension ile iletişim için API endpoints
4
+ */
5
+
6
+ import {
7
+ extensionData,
8
+ updateSelectedElement,
9
+ updateNetworkTrace,
10
+ updateWebSocketTrace,
11
+ updateSavedSelections,
12
+ updatePageAnalysis,
13
+ syncAll,
14
+ updateConnection,
15
+ updateDisconnect,
16
+ addCapturedEndpoint
17
+ } from '../state/extension-state.js';
18
+ import { connectionHealth } from './http-server.js';
19
+ import { resetAllCircuitBreakers } from '../utils/circuit-breaker.js';
20
+ import { logger } from '../utils/logger.js';
21
+ import { persistStateNow } from '../state/bridge-persistence.js';
22
+ import { MetricsDB } from '../metrics/metrics-db.js';
23
+ import { spawn } from 'child_process';
24
+ import { dirname, join } from 'path';
25
+ import { fileURLToPath } from 'url';
26
+
27
+ // Shared MetricsDB instance for connection event recording
28
+ const metricsDB = new MetricsDB();
29
+
30
+ // Track last known session ID to detect page refreshes
31
+ let lastSessionId = null;
32
+
33
+ // Captured auth tokens: domain → token value
34
+ const capturedAuthTokens = {};
35
+
36
+ export const setupRoutes = (app, httpPort) => {
37
+ /**
38
+ * Health check endpoint
39
+ */
40
+ app.get('/health', (req, res) => {
41
+ const uptimeSeconds = Math.floor(process.uptime());
42
+ const health = connectionHealth.getStatus();
43
+ res.json({
44
+ status: 'ok',
45
+ isConnected: health.connected,
46
+ lastUpdate: extensionData.lastUpdateTime,
47
+ hasData: !!extensionData.selectedElement,
48
+ httpPort: httpPort,
49
+ uptime: `${uptimeSeconds}s`,
50
+ connection: health,
51
+ dataState: {
52
+ selectedElement: !!extensionData.selectedElement,
53
+ networkMatches: extensionData.networkTrace.totalMatches,
54
+ savedSelections: extensionData.savedSelections.length
55
+ }
56
+ });
57
+ });
58
+
59
+ /**
60
+ * State sync endpoint for secondary instances
61
+ */
62
+ app.get('/api/sync-state', (req, res) => {
63
+ // exportSessionResult, jsExecutionResult, captureScreenshotResult kasıtlı olarak dışarıda —
64
+ // bu field'lar raw cookie, token ve kod çıktısı içerebilir; reconnect için gerekli değil.
65
+ res.json({
66
+ selectedElement: extensionData.selectedElement,
67
+ networkTrace: extensionData.networkTrace,
68
+ websocketTrace: extensionData.websocketTrace,
69
+ websocketConnections: extensionData.websocketConnections,
70
+ pageAnalysis: extensionData.pageAnalysis,
71
+ savedSelections: extensionData.savedSelections,
72
+ isConnected: extensionData.isConnected,
73
+ lastUpdateTime: extensionData.lastUpdateTime,
74
+ apiEndpoints: extensionData.apiEndpoints // NEW: for manage_site_profile save flow
75
+ });
76
+ });
77
+
78
+ /**
79
+ * Extension'dan element seçimi aldığında
80
+ */
81
+ app.post('/api/element-selected', (req, res) => {
82
+ updateSelectedElement(req.body);
83
+ res.json({ success: true });
84
+ });
85
+
86
+ /**
87
+ * Extension'dan network trace aldığında
88
+ */
89
+ app.post('/api/network-trace', (req, res) => {
90
+ updateNetworkTrace(req.body);
91
+ res.json({ success: true });
92
+ });
93
+
94
+ /**
95
+ * Extension'dan WebSocket trace aldığında (NEW v2.3)
96
+ */
97
+ app.post('/api/websocket-trace', (req, res) => {
98
+ updateWebSocketTrace(req.body);
99
+ res.json({ success: true });
100
+ });
101
+
102
+ /**
103
+ * Extension'dan kaydedilmiş seçimleri aldığında
104
+ */
105
+ app.post('/api/saved-selections', (req, res) => {
106
+ updateSavedSelections(req.body.savedSelections);
107
+ res.json({ success: true });
108
+ });
109
+
110
+ /**
111
+ * MCP-side tool (discover_apis / execute_js capture) pushes a captured
112
+ * endpoint into bridge state. Dedupe happens inside addCapturedEndpoint.
113
+ * Returns the persisted entry so the caller can update its local cache.
114
+ */
115
+ app.post('/api/captured-endpoint', (req, res) => {
116
+ const { domain, method, url, status, contentType } = req.body || {};
117
+ if (!domain || !url) {
118
+ return res.status(400).json({ success: false, error: 'domain_and_url_required' });
119
+ }
120
+ addCapturedEndpoint({ domain, method, url, status, contentType });
121
+ const entry = extensionData.apiEndpoints.find(
122
+ (e) => e.domain === domain && e.url === url
123
+ );
124
+ res.json({ success: true, entry: entry || null });
125
+ });
126
+
127
+ /**
128
+ * Tüm state'i bir seferde sync etmek için
129
+ */
130
+ app.post('/api/sync-all', (req, res) => {
131
+ syncAll(req.body);
132
+ res.json({ success: true });
133
+ });
134
+
135
+ /**
136
+ * Phase 1.2: Full-sync endpoint — extension pushes all state after server restart
137
+ * This is called when the extension detects serverStartedAt has changed,
138
+ * meaning the MCP server restarted and lost all in-memory state.
139
+ */
140
+ app.post('/api/full-sync', (req, res) => {
141
+ const { selectedElement, networkTrace, websocketConnections, pageAnalysis, savedSelections, sessionId, pageUrl } = req.body;
142
+
143
+ if (selectedElement) {
144
+ updateSelectedElement(selectedElement);
145
+ }
146
+
147
+ if (networkTrace) {
148
+ updateNetworkTrace(networkTrace);
149
+ }
150
+
151
+ if (websocketConnections) {
152
+ updateWebSocketTrace(websocketConnections);
153
+ }
154
+
155
+ if (pageAnalysis) {
156
+ updatePageAnalysis(pageAnalysis);
157
+ }
158
+
159
+ if (savedSelections && savedSelections.length > 0) {
160
+ updateSavedSelections(savedSelections);
161
+ }
162
+
163
+ if (sessionId) {
164
+ updateConnection(sessionId);
165
+ }
166
+
167
+ if (pageUrl) {
168
+ extensionData.activeTabUrl = pageUrl;
169
+ }
170
+
171
+ extensionData.isConnected = true;
172
+ extensionData.lastUpdateTime = Date.now();
173
+
174
+ logger.info('Bridge', 'Full-sync received from extension after server restart');
175
+ res.json({ success: true, message: 'Full state sync received' });
176
+ });
177
+
178
+ /**
179
+ * Extension connection test
180
+ */
181
+ app.post('/api/ping', (req, res) => {
182
+ updateConnection();
183
+ connectionHealth.recordHeartbeat();
184
+ res.json({ pong: true, health: connectionHealth.getStatus() });
185
+ });
186
+
187
+ /**
188
+ * Lightweight heartbeat endpoint
189
+ * Extension calls this every 15s to keep connection alive
190
+ */
191
+ app.post('/api/heartbeat', (req, res) => {
192
+ const { sessionId, pageUrl } = req.body;
193
+
194
+ // Detect page refresh: new session ID means the page reloaded
195
+ const isNewSession = sessionId && lastSessionId && sessionId !== lastSessionId;
196
+ if (sessionId) lastSessionId = sessionId;
197
+ if (pageUrl) extensionData.activeTabUrl = pageUrl;
198
+
199
+ // Circuit breaker reset: if extension reconnects after disconnect,
200
+ // breakers that tripped during the outage should not block new requests.
201
+ if (isNewSession) {
202
+ resetAllCircuitBreakers();
203
+ }
204
+
205
+ updateConnection(sessionId || null);
206
+ connectionHealth.recordHeartbeat(sessionId || null);
207
+
208
+ // Return any pending requests so extension can process them
209
+ const pendingCount =
210
+ (extensionData.jsExecutionRequest ? 1 : 0) +
211
+ (extensionData.actionExecutionRequest ? 1 : 0) +
212
+ (extensionData.captureScreenshotRequest ? 1 : 0) +
213
+ (extensionData.rawNetworkRequests?.length || 0) +
214
+ (extensionData.analyzePageRequests?.length || 0) +
215
+ (extensionData.selectElementRequest ? 1 : 0);
216
+
217
+ // If new session detected and server has a previous element selection, ask extension to restore it
218
+ let needsRestore = isNewSession && !!extensionData.selectedElement;
219
+
220
+ // Tab navigation: clear pendingNavigation flag and signal restore if element exists
221
+ if (extensionData.pendingNavigation) {
222
+ extensionData.pendingNavigation = false;
223
+ if (!needsRestore && extensionData.selectedElement) {
224
+ needsRestore = true;
225
+ }
226
+ }
227
+
228
+ const response = {
229
+ alive: true,
230
+ pendingRequests: pendingCount,
231
+ serverUptime: Math.floor(process.uptime()),
232
+ serverStartedAt: connectionHealth.serverStartedAt // Phase 1.4: restart detection
233
+ };
234
+
235
+ if (needsRestore) {
236
+ response.needsRestore = true;
237
+ response.selectedElement = extensionData.selectedElement;
238
+ }
239
+
240
+ res.json(response);
241
+ });
242
+
243
+ /**
244
+ * Explicit disconnect from extension (page unload, tab close)
245
+ */
246
+ app.post('/api/disconnect', (req, res) => {
247
+ const { reason } = req.body;
248
+ updateDisconnect(reason || 'page-unload');
249
+ connectionHealth.markDisconnected(reason || 'page-unload');
250
+ res.json({ success: true });
251
+ });
252
+
253
+ /**
254
+ * Soft disconnect — tab navigation signal
255
+ * Unlike /api/disconnect, this does NOT set isConnected=false.
256
+ * Instead sets pendingNavigation=true so heartbeat can restore state.
257
+ */
258
+ app.post('/api/soft-disconnect', (req, res) => {
259
+ const { tabId } = req.body;
260
+ extensionData.pendingNavigation = true;
261
+ logger.debug('Bridge', `Soft disconnect (tab navigation, tabId=${tabId})`);
262
+ res.json({ success: true });
263
+ });
264
+
265
+ /**
266
+ * Sayfa veri kaynağı analizi
267
+ */
268
+ app.post('/api/page-analysis', (req, res) => {
269
+ updatePageAnalysis(req.body);
270
+ res.json({ success: true });
271
+ });
272
+
273
+ /**
274
+ * JavaScript Execution Endpoint
275
+ * Extension'dan gelen JS execution requestlerini handle eder
276
+ */
277
+ app.post('/api/execute-js', (req, res) => {
278
+ const { code, timeout, result } = req.body;
279
+
280
+ if (result !== undefined) {
281
+ // Extension'dan sonuç geldi, requestId-keyed map'e kaydet (race-condition fix)
282
+ const rid = req.body.requestId || req.body.id || `js-${Date.now()}`;
283
+ extensionData.jsExecutionResults = extensionData.jsExecutionResults || {};
284
+ extensionData.jsExecutionResults[rid] = {
285
+ code,
286
+ result,
287
+ timestamp: new Date().toISOString(),
288
+ requestId: rid,
289
+ ...(req.body.__typeHint ? { __typeHint: req.body.__typeHint } : {})
290
+ };
291
+ res.json({ success: true });
292
+ } else {
293
+ // MCP'den gelen execution request - extension'a forward et
294
+ extensionData.jsExecutionRequest = {
295
+ code,
296
+ timeout: timeout || 5000,
297
+ id: req.body.id || req.body.requestId || `js-${Date.now()}`,
298
+ timestamp: new Date().toISOString(),
299
+ context: req.body.context || 'page',
300
+ ...(req.body.tabId ? { tabId: req.body.tabId } : {})
301
+ };
302
+ res.json({ success: true, queued: true });
303
+ }
304
+ });
305
+
306
+
307
+
308
+
309
+
310
+ /**
311
+ * RPA Action Endpoint
312
+ * MCP'den gelen RPA actions'ı (click, type, navigate) queue'lar
313
+ */
314
+ app.post('/api/execute-action', (req, res) => {
315
+ const { actionType, selectorInfo, text, url, result } = req.body;
316
+
317
+ if (result !== undefined) {
318
+ // Extension'dan sonuç geldi, state'e kaydet
319
+ extensionData.actionExecutionResult = {
320
+ actionType,
321
+ result,
322
+ timestamp: new Date().toISOString(),
323
+ requestId: req.body.requestId || req.body.id
324
+ };
325
+ res.json({ success: true });
326
+ } else {
327
+ // MCP'den gelen action request - extension'a forward et
328
+ extensionData.actionExecutionRequest = {
329
+ actionType,
330
+ selectorInfo,
331
+ text,
332
+ url,
333
+ id: req.body.id || req.body.requestId || `action-${Date.now()}`,
334
+ timestamp: new Date().toISOString(),
335
+ ...(req.body.tabId ? { tabId: req.body.tabId } : {})
336
+ };
337
+ res.json({ success: true, queued: true });
338
+ }
339
+ });
340
+
341
+ /**
342
+ * Analyze Page Endpoint
343
+ * MCP tool page analysis request'lerini kuyruğa alır; extension sonucu buraya gönderir.
344
+ */
345
+ app.post('/api/analyze-page', (req, res) => {
346
+ const { result } = req.body;
347
+
348
+ if (result !== undefined) {
349
+ const requestId = req.body.requestId || req.body.id;
350
+ if (requestId) {
351
+ extensionData.analyzePageResults[requestId] = {
352
+ result,
353
+ timestamp: new Date().toISOString(),
354
+ requestId
355
+ };
356
+ }
357
+ res.json({ success: true });
358
+ } else {
359
+ const queuedRequest = {
360
+ id: req.body.id || req.body.requestId || `analyze-${Date.now()}`,
361
+ timestamp: new Date().toISOString(),
362
+ ...(req.body.tabId ? { tabId: req.body.tabId } : {})
363
+ };
364
+ extensionData.analyzePageRequests.push(queuedRequest);
365
+ res.json({ success: true, queued: true });
366
+ }
367
+ });
368
+
369
+ /**
370
+ * Screenshot Endpoint
371
+ * MCP'den gelen viewport screenshot işlemlerini sıraya alır
372
+ */
373
+ app.post('/api/capture-screenshot', (req, res) => {
374
+ const { result } = req.body;
375
+
376
+ if (result !== undefined) {
377
+ if (result.error) {
378
+ extensionData.captureScreenshotResult = {
379
+ error: result.error,
380
+ timestamp: new Date().toISOString(),
381
+ requestId: req.body.requestId || req.body.id
382
+ };
383
+ } else {
384
+ extensionData.captureScreenshotResult = {
385
+ dataUrl: result.dataUrl,
386
+ timestamp: new Date().toISOString(),
387
+ requestId: req.body.requestId || req.body.id
388
+ };
389
+ }
390
+ res.json({ success: true });
391
+ } else {
392
+ extensionData.captureScreenshotRequest = {
393
+ id: req.body.id || req.body.requestId || `screenshot-${Date.now()}`,
394
+ timestamp: new Date().toISOString(),
395
+ ...(req.body.tabId ? { tabId: req.body.tabId } : {})
396
+ };
397
+ res.json({ success: true, queued: true });
398
+ }
399
+ });
400
+
401
+ /**
402
+ * Polling Endpoint: Bekleyen request'leri döndürür
403
+ * Extension bu endpoint'i periyodik olarak check eder
404
+ */
405
+ app.get('/api/pending-requests', (req, res) => {
406
+ const pendingRequests = [];
407
+
408
+ // JS execution request varsa
409
+ if (extensionData.jsExecutionRequest) {
410
+ pendingRequests.push({
411
+ type: 'execute_js',
412
+ data: extensionData.jsExecutionRequest
413
+ });
414
+ }
415
+
416
+
417
+
418
+ // RPA Action request varsa
419
+ if (extensionData.actionExecutionRequest) {
420
+ pendingRequests.push({
421
+ type: 'execute_action',
422
+ data: extensionData.actionExecutionRequest
423
+ });
424
+ }
425
+
426
+ // Screenshot request varsa
427
+ if (extensionData.captureScreenshotRequest) {
428
+ pendingRequests.push({
429
+ type: 'capture_screenshot',
430
+ data: extensionData.captureScreenshotRequest
431
+ });
432
+ }
433
+
434
+ // Raw network discovery request varsa
435
+ for (const request of (extensionData.rawNetworkRequests || [])) {
436
+ pendingRequests.push({
437
+ type: 'get_raw_network',
438
+ data: request
439
+ });
440
+ }
441
+
442
+ // Analyze page requests varsa
443
+ for (const request of (extensionData.analyzePageRequests || [])) {
444
+ pendingRequests.push({
445
+ type: 'analyze_page',
446
+ data: request
447
+ });
448
+ }
449
+
450
+ // Programmatic element selection request varsa
451
+ if (extensionData.selectElementRequest) {
452
+ pendingRequests.push({
453
+ type: 'select_element',
454
+ data: extensionData.selectElementRequest
455
+ });
456
+ }
457
+
458
+ // Export session request varsa
459
+ if (extensionData.exportSessionRequest) {
460
+ pendingRequests.push({
461
+ type: 'export_session',
462
+ data: extensionData.exportSessionRequest
463
+ });
464
+ }
465
+
466
+ // Tab list request varsa
467
+ if (extensionData.tabsRequest) {
468
+ pendingRequests.push({
469
+ type: 'get_tabs',
470
+ data: extensionData.tabsRequest
471
+ });
472
+ }
473
+
474
+ res.json({
475
+ success: true,
476
+ hasPending: pendingRequests.length > 0,
477
+ requests: pendingRequests
478
+ });
479
+ });
480
+
481
+ /**
482
+ * Raw Network Discovery Endpoint (discover_apis tool)
483
+ * MCP tool bir raw network keşfi kuyruğa alır; extension sonucu buraya gönderir.
484
+ */
485
+ app.post('/api/raw-network-requests', (req, res) => {
486
+ const { result } = req.body;
487
+
488
+ if (result !== undefined) {
489
+ // Extension'dan sonuç geldi
490
+ const requestId = req.body.requestId || req.body.id;
491
+ extensionData.rawNetworkResults[requestId] = {
492
+ requests: result.requests || [],
493
+ total: result.total || 0,
494
+ error: result.error || null,
495
+ timestamp: new Date().toISOString(),
496
+ requestId
497
+ };
498
+ res.json({ success: true });
499
+ } else {
500
+ // MCP tool'dan request — extension'a forward et
501
+ extensionData.rawNetworkRequests.push({
502
+ urlPattern: req.body.urlPattern || null,
503
+ method: req.body.method || 'all',
504
+ limit: req.body.limit || 50,
505
+ includeBody: req.body.includeBody || false,
506
+ id: req.body.id || `raw-net-${Date.now()}`,
507
+ timestamp: new Date().toISOString(),
508
+ ...(req.body.tabId ? { tabId: req.body.tabId } : {})
509
+ });
510
+ res.json({ success: true, queued: true });
511
+ }
512
+ });
513
+
514
+ /**
515
+ * Programmatic Element Selection Endpoint (select_element tool)
516
+ * MCP tool bir selector iletir; extension elementi bulup /api/element-selected'a gönderir.
517
+ */
518
+ app.post('/api/select-element', (req, res) => {
519
+ const { result } = req.body;
520
+
521
+ if (result !== undefined) {
522
+ // Extension'dan selection sonucu geldi (hata veya başarı)
523
+ extensionData.selectElementResult = {
524
+ success: result.success || false,
525
+ error: result.error || null,
526
+ element: result.element || null,
527
+ timestamp: new Date().toISOString(),
528
+ requestId: req.body.requestId || req.body.id
529
+ };
530
+ res.json({ success: true });
531
+ } else {
532
+ // MCP tool'dan selector request — extension'a forward et
533
+ extensionData.selectElementRequest = {
534
+ selectorInfo: req.body.selectorInfo,
535
+ triggerNetworkTrace: req.body.triggerNetworkTrace !== false,
536
+ id: req.body.id || `sel-elem-${Date.now()}`,
537
+ timestamp: new Date().toISOString(),
538
+ ...(req.body.tabId ? { tabId: req.body.tabId } : {})
539
+ };
540
+ res.json({ success: true, queued: true });
541
+ }
542
+ });
543
+
544
+ /**
545
+ * Clear Pending Request: Extension request'i çalıştırdıktan sonra temizler
546
+ */
547
+ app.post('/api/clear-request', (req, res) => {
548
+ const { type, requestId } = req.body;
549
+
550
+ if (type === 'execute_js' && extensionData.jsExecutionRequest?.id === requestId) {
551
+ extensionData.jsExecutionRequest = null;
552
+ }
553
+
554
+ if (type === 'execute_action' && extensionData.actionExecutionRequest?.id === requestId) {
555
+ extensionData.actionExecutionRequest = null;
556
+ }
557
+
558
+ if (type === 'capture_screenshot' && extensionData.captureScreenshotRequest?.id === requestId) {
559
+ extensionData.captureScreenshotRequest = null;
560
+ }
561
+
562
+ if (type === 'get_raw_network') {
563
+ extensionData.rawNetworkRequests = (extensionData.rawNetworkRequests || [])
564
+ .filter(request => request.id !== requestId);
565
+ }
566
+
567
+ if (type === 'analyze_page') {
568
+ extensionData.analyzePageRequests = (extensionData.analyzePageRequests || [])
569
+ .filter(request => request.id !== requestId);
570
+ }
571
+
572
+ if (type === 'select_element' && extensionData.selectElementRequest?.id === requestId) {
573
+ extensionData.selectElementRequest = null;
574
+ }
575
+
576
+ if (type === 'export_session' && extensionData.exportSessionRequest?.id === requestId) {
577
+ extensionData.exportSessionRequest = null;
578
+ }
579
+
580
+ if (type === 'get_tabs' && extensionData.tabsRequest?.id === requestId) {
581
+ extensionData.tabsRequest = null;
582
+ }
583
+
584
+ res.json({ success: true });
585
+ });
586
+
587
+ /**
588
+ * Export Session Endpoint
589
+ * MCP tool bir export_session isteği kuyruğa alır; extension cookie+storage okuyup sonucu buraya gönderir.
590
+ */
591
+ app.post('/api/export-session', (req, res) => {
592
+ const { result, error } = req.body;
593
+
594
+ if (result !== undefined || error !== undefined) {
595
+ // Extension'dan sonuç geldi
596
+ extensionData.exportSessionResult = {
597
+ result: result || null,
598
+ error: error || null,
599
+ timestamp: new Date().toISOString(),
600
+ requestId: req.body.requestId || req.body.id
601
+ };
602
+ res.json({ success: true });
603
+ } else {
604
+ // MCP tool'dan request — extension'a forward et
605
+ extensionData.exportSessionRequest = {
606
+ id: req.body.id || `export-session-${Date.now()}`,
607
+ timestamp: new Date().toISOString(),
608
+ ...(req.body.tabId ? { tabId: req.body.tabId } : {})
609
+ };
610
+ res.json({ success: true, queued: true });
611
+ }
612
+ });
613
+
614
+ /**
615
+ * Tab List Endpoint (multi-tab support)
616
+ * Extension service worker'dan tab listesini çeker; debug_mcp_state ve tabId targeting için kullanılır.
617
+ */
618
+ app.post('/api/tabs', (req, res) => {
619
+ const { result } = req.body;
620
+
621
+ if (result !== undefined) {
622
+ // Extension'dan sonuç geldi
623
+ extensionData.tabsResult = {
624
+ tabs: result.tabs || [],
625
+ timestamp: new Date().toISOString(),
626
+ requestId: req.body.requestId || req.body.id
627
+ };
628
+ res.json({ success: true });
629
+ } else {
630
+ // MCP tool'dan request — extension'a forward et
631
+ extensionData.tabsRequest = {
632
+ id: req.body.id || `tabs-${Date.now()}`,
633
+ timestamp: new Date().toISOString()
634
+ };
635
+ res.json({ success: true, queued: true });
636
+ }
637
+ });
638
+
639
+ /**
640
+ * Clear All Pending Requests
641
+ * Service worker restart olduğunda in-flight pending request'leri temizler.
642
+ * SW terminate → restart döngüsünde server tarafında asılı kalan request'lerin deadlock'unu önler.
643
+ */
644
+ app.post('/api/clear-all-requests', (req, res) => {
645
+ // Sadece content-script üzerinden işlenen request'leri temizle.
646
+ // export_session ve get_tabs SW içinde işlendiğinden SW restart'ta silinmemeli —
647
+ // aksi hâlde MCP tool request gönderirken SW yeni session açıp hepsini siliyordu (race condition).
648
+ extensionData.jsExecutionRequest = null;
649
+ extensionData.actionExecutionRequest = null;
650
+ extensionData.captureScreenshotRequest = null;
651
+ extensionData.rawNetworkRequests = [];
652
+ extensionData.analyzePageRequests = [];
653
+ extensionData.selectElementRequest = null;
654
+ res.json({ success: true, cleared: true });
655
+ });
656
+
657
+ /**
658
+ * Auth token capture: SW → MCP server → execute_js
659
+ */
660
+ app.post('/api/capture-token', (req, res) => {
661
+ const { domain, token } = req.body || {};
662
+ if (domain && token) {
663
+ capturedAuthTokens[domain] = token;
664
+ }
665
+ res.json({ success: true });
666
+ });
667
+
668
+ app.get('/api/capture-token', (req, res) => {
669
+ const domain = req.query.domain;
670
+ if (domain) {
671
+ res.json({ token: capturedAuthTokens[domain] || null });
672
+ } else {
673
+ res.json({ tokens: capturedAuthTokens });
674
+ }
675
+ });
676
+
677
+ /**
678
+ * Insight stats: execute_js disambiguation fırsatları vs save_site_profile çağrıları
679
+ * /stop skill bu endpoint'i kontrol eder
680
+ */
681
+ app.get('/api/insight-stats', (req, res) => {
682
+ const opportunities = extensionData.insightOpportunities || {};
683
+ const saves = extensionData.profileSaves || {};
684
+ const unsaved = Object.keys(opportunities).filter(
685
+ domain => !saves[domain] || saves[domain] === 0
686
+ );
687
+ res.json({
688
+ opportunities,
689
+ saves,
690
+ unsaved,
691
+ hasUnsaved: unsaved.length > 0
692
+ });
693
+ });
694
+
695
+ /**
696
+ * Background execute_js auto-capture endpoint
697
+ * sandbox.html fetch interceptor'ından gelen API endpoint verilerini profile'a kaydeder.
698
+ */
699
+ app.post('/api/bg-capture', (req, res) => {
700
+ const { domain, url, method, status, contentType } = req.body || {};
701
+ if (!domain || !url) return res.json({ success: false, error: 'missing domain or url' });
702
+
703
+ try {
704
+ addCapturedEndpoint({
705
+ domain,
706
+ method: method || 'GET',
707
+ url,
708
+ status: status || null,
709
+ contentType: contentType || null
710
+ });
711
+ res.json({ success: true });
712
+ } catch (err) {
713
+ res.json({ success: false, error: err.message });
714
+ }
715
+ });
716
+
717
+ // ============================================================================
718
+ // Phase 2.3: Bridge Client GET Endpoints
719
+ // These endpoints allow the MCP thin client to read state and poll for results
720
+ // via HTTP instead of direct memory access.
721
+ // ============================================================================
722
+
723
+ /**
724
+ * GET /api/connection-status
725
+ * Returns connection state for the MCP thin client.
726
+ */
727
+ app.get('/api/connection-status', (req, res) => {
728
+ res.json({
729
+ isConnected: extensionData.isConnected,
730
+ activeTabUrl: extensionData.activeTabUrl || '',
731
+ lastUpdateTime: extensionData.lastUpdateTime,
732
+ pendingNavigation: extensionData.pendingNavigation || false,
733
+ currentSessionId: extensionData.currentSessionId || null,
734
+ sessionStartedAt: extensionData.sessionStartedAt || null
735
+ });
736
+ });
737
+
738
+ /**
739
+ * GET /api/selected-element
740
+ * Returns the currently selected element.
741
+ */
742
+ app.get('/api/selected-element', (req, res) => {
743
+ res.json({ element: extensionData.selectedElement || null });
744
+ });
745
+
746
+ /**
747
+ * GET /api/page-analysis
748
+ * Returns the current page analysis data.
749
+ */
750
+ app.get('/api/page-analysis', (req, res) => {
751
+ res.json({ analysis: extensionData.pageAnalysis || null });
752
+ });
753
+
754
+ /**
755
+ * GET /api/network-trace
756
+ * Returns the current network trace data.
757
+ */
758
+ app.get('/api/network-trace', (req, res) => {
759
+ res.json({ trace: extensionData.networkTrace || null });
760
+ });
761
+
762
+ /**
763
+ * GET /api/websocket-trace
764
+ * Returns the current WebSocket trace and connections data.
765
+ */
766
+ app.get('/api/websocket-trace', (req, res) => {
767
+ res.json({
768
+ trace: extensionData.websocketTrace || null,
769
+ connections: extensionData.websocketConnections || null
770
+ });
771
+ });
772
+
773
+ /**
774
+ * GET /api/state
775
+ * Returns all state fields for the MCP thin client.
776
+ * Includes request fields for validation (e.g. validateNoPendingExecution).
777
+ * Result fields are NOT included — use GET /api/result/:type for those.
778
+ */
779
+ app.get('/api/state', (req, res) => {
780
+ res.json({
781
+ // Connection & status
782
+ isConnected: extensionData.isConnected,
783
+ activeTabUrl: extensionData.activeTabUrl || '',
784
+ pendingNavigation: extensionData.pendingNavigation || false,
785
+ lastUpdateTime: extensionData.lastUpdateTime,
786
+ currentSessionId: extensionData.currentSessionId || null,
787
+ sessionStartedAt: extensionData.sessionStartedAt || null,
788
+ _connectionHealth: connectionHealth.getStatus(),
789
+
790
+ // Data fields
791
+ selectedElement: extensionData.selectedElement,
792
+ pageAnalysis: extensionData.pageAnalysis,
793
+ networkTrace: extensionData.networkTrace,
794
+ websocketTrace: extensionData.websocketTrace,
795
+ websocketConnections: extensionData.websocketConnections,
796
+ savedSelections: extensionData.savedSelections || [],
797
+ apiEndpoints: extensionData.apiEndpoints || [], // NEW: for manage_site_profile save flow
798
+
799
+ // Request fields (for validation — e.g. check pending JS execution)
800
+ jsExecutionRequest: extensionData.jsExecutionRequest || null,
801
+ actionExecutionRequest: extensionData.actionExecutionRequest || null,
802
+ selectElementRequest: extensionData.selectElementRequest || null,
803
+ captureScreenshotRequest: extensionData.captureScreenshotRequest || null,
804
+ exportSessionRequest: extensionData.exportSessionRequest || null,
805
+ tabsRequest: extensionData.tabsRequest || null,
806
+ analyzePageRequests: extensionData.analyzePageRequests || [],
807
+ rawNetworkRequests: extensionData.rawNetworkRequests || [],
808
+
809
+ // Result fields (for validation — e.g. check stale result)
810
+ jsExecutionResult: extensionData.jsExecutionResult || null,
811
+ actionExecutionResult: extensionData.actionExecutionResult || null,
812
+ selectElementResult: extensionData.selectElementResult || null,
813
+ captureScreenshotResult: extensionData.captureScreenshotResult || null,
814
+ exportSessionResult: extensionData.exportSessionResult || null,
815
+ tabsResult: extensionData.tabsResult || null,
816
+ analyzePageResults: extensionData.analyzePageResults || {},
817
+ rawNetworkResults: extensionData.rawNetworkResults || {},
818
+
819
+ // Profile & insight tracking
820
+ insightOpportunities: extensionData.insightOpportunities || {},
821
+ profileSaves: extensionData.profileSaves || {},
822
+
823
+ // Restart signal — MCP server reads this to decide whether to exit
824
+ restartRequestedAt: extensionData.restartRequestedAt || null
825
+ });
826
+ });
827
+
828
+ /**
829
+ * GET /api/result/:type
830
+ * Returns and consumes (clears) a result by type. Used by the MCP thin client
831
+ * for result polling instead of direct memory access.
832
+ *
833
+ * Supported types:
834
+ * js-execution, action-execution, select-element, capture-screenshot,
835
+ * export-session, tabs, analyze-page, raw-network
836
+ *
837
+ * For analyze-page and raw-network, requestId query param is required.
838
+ * For single-result types, requestId is optional (matches against requestId/id).
839
+ */
840
+ app.get('/api/result/:type', (req, res) => {
841
+ const { type } = req.params;
842
+ const { requestId } = req.query;
843
+
844
+ // Single-result types: read, match, consume
845
+ const singleResultTypes = {
846
+ 'action-execution': { field: 'actionExecutionResult', clear: () => { extensionData.actionExecutionResult = null; } },
847
+ 'select-element': { field: 'selectElementResult', clear: () => { extensionData.selectElementResult = null; } },
848
+ 'capture-screenshot': { field: 'captureScreenshotResult', clear: () => { extensionData.captureScreenshotResult = null; } },
849
+ 'export-session': { field: 'exportSessionResult', clear: () => { extensionData.exportSessionResult = null; } },
850
+ 'tabs': { field: 'tabsResult', clear: () => { extensionData.tabsResult = null; } },
851
+ };
852
+
853
+ // Map-result types: read by requestId, delete entry, consume
854
+ const mapResultTypes = {
855
+ 'js-execution': 'jsExecutionResults',
856
+ 'analyze-page': 'analyzePageResults',
857
+ 'raw-network': 'rawNetworkResults',
858
+ };
859
+
860
+ if (singleResultTypes[type]) {
861
+ const { field, clear } = singleResultTypes[type];
862
+ const result = extensionData[field];
863
+
864
+ if (!result) {
865
+ return res.json({ found: false });
866
+ }
867
+
868
+ // Match by requestId if provided
869
+ if (requestId && result.requestId !== requestId && result.id !== requestId) {
870
+ return res.json({ found: false });
871
+ }
872
+
873
+ // Consume (clear) the result
874
+ clear();
875
+ res.json({ found: true, result });
876
+ } else if (mapResultTypes[type]) {
877
+ const mapField = mapResultTypes[type];
878
+
879
+ if (!requestId || !extensionData[mapField][requestId]) {
880
+ return res.json({ found: false });
881
+ }
882
+
883
+ const result = extensionData[mapField][requestId];
884
+ delete extensionData[mapField][requestId]; // consume
885
+ res.json({ found: true, result });
886
+ } else {
887
+ res.status(400).json({ error: `Unknown result type: ${type}` });
888
+ }
889
+ });
890
+
891
+ /**
892
+ * POST /api/metrics — Extension-side event reporting
893
+ * Accepts connection events from the extension (heartbeat failures, sync failures, etc.)
894
+ */
895
+ app.post('/api/metrics', (req, res) => {
896
+ const { event_type, duration_ms, retry_count, failure_reason, metadata } = req.body;
897
+
898
+ if (!event_type) {
899
+ return res.status(400).json({ success: false, error: 'event_type is required' });
900
+ }
901
+
902
+ metricsDB.recordConnectionEvent({
903
+ timestamp: new Date().toISOString(),
904
+ event_type,
905
+ duration_ms: duration_ms || null,
906
+ retry_count: retry_count || null,
907
+ failure_reason: failure_reason || null,
908
+ metadata: metadata ? (typeof metadata === 'string' ? metadata : JSON.stringify(metadata)) : null,
909
+ });
910
+
911
+ res.json({ success: true });
912
+ });
913
+
914
+ /**
915
+ * POST /api/restart — Restart bridge daemon + signal MCP server to restart
916
+ * Extension popup uses this to restart bridge with new code.
917
+ * Flow: set restart signal → persist state → spawn new process → shutdown current process
918
+ * MCP server detects restartRequestedAt via /api/state and exits with process.exit(0),
919
+ * which causes Claude Code to restart it with fresh code.
920
+ */
921
+ app.post('/api/restart', (req, res) => {
922
+ const origin = req.get('Origin') || req.get('Referer') || '';
923
+ if (origin && !/^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?/.test(origin) && !/^chrome-extension:\/\//.test(origin)) {
924
+ return res.status(403).json({ success: false, error: 'Forbidden' });
925
+ }
926
+
927
+ // Signal MCP server to restart (set before persist so it survives bridge restart)
928
+ extensionData.restartRequestedAt = Date.now();
929
+
930
+ res.json({ success: true, message: 'Restarting bridge and MCP server...', restartRequestedAt: extensionData.restartRequestedAt });
931
+
932
+ // Persist state before restart (now includes restartRequestedAt)
933
+ try {
934
+ persistStateNow(extensionData);
935
+ console.error('[MCP Bridge] 📝 State persisted before restart');
936
+ } catch (err) {
937
+ console.error('[MCP Bridge] ⚠️ Failed to persist state before restart:', err.message);
938
+ }
939
+
940
+ // Record restart event in metrics
941
+ metricsDB.recordConnectionEvent({
942
+ event_type: 'bridge_spawn',
943
+ failure_reason: 'restart',
944
+ metadata: JSON.stringify({ port: httpPort, pid: process.pid, restarted: true }),
945
+ });
946
+
947
+ // Spawn new bridge daemon process
948
+ const currentDir = dirname(fileURLToPath(import.meta.url));
949
+ const bridgePath = join(currentDir, '..', 'bridge-daemon.js');
950
+
951
+ console.error('[MCP Bridge] 🔄 Spawning new bridge daemon...');
952
+ const newProcess = spawn('node', [bridgePath], {
953
+ detached: true,
954
+ stdio: 'ignore',
955
+ env: { ...process.env, MCP_PORT: String(httpPort) },
956
+ });
957
+ newProcess.unref();
958
+
959
+ // Shutdown current process after short delay
960
+ setTimeout(() => {
961
+ console.error('[MCP Bridge] 🛑 Current process shutting down for restart...');
962
+ process.exit(0);
963
+ }, 300);
964
+ });
965
+
966
+ /**
967
+ * Kill Endpoint: Yeni sunucu başladığında port konfliktini çözmek için eski sunucuyu kapatır.
968
+ * /api/die is a kill, not a coordinated restart — clear the restart signal.
969
+ */
970
+ app.post('/api/die', (req, res) => {
971
+ const origin = req.get('Origin') || req.get('Referer') || '';
972
+ if (origin && !/^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?/.test(origin) && !/^chrome-extension:\/\//.test(origin)) {
973
+ return res.status(403).json({ success: false, error: 'Forbidden' });
974
+ }
975
+
976
+ // Clear restart signal — /api/die is a kill, not a coordinated restart
977
+ extensionData.restartRequestedAt = null;
978
+
979
+ res.json({ success: true, message: 'Shutting down...' });
980
+
981
+ // Phase 1.3: Persist state before exit to survive restart
982
+ try {
983
+ persistStateNow(extensionData);
984
+ console.error('[MCP Bridge] 📝 State persisted before termination');
985
+ } catch (err) {
986
+ console.error('[MCP Bridge] ⚠️ Failed to persist state before termination:', err.message);
987
+ }
988
+
989
+ setTimeout(() => {
990
+ console.error('[MCP Bridge] 🛑 Received termination request (/api/die). Exiting...');
991
+ process.exit(0);
992
+ }, 200); // 100ms → 200ms to allow persist to complete
993
+ });
994
+
995
+ /**
996
+ * POST /api/clear-restart-signal — Clear the restart signal after MCP server has restarted.
997
+ * Called by the fresh MCP server instance after detecting restartRequestedAt in /api/state.
998
+ */
999
+ app.post('/api/clear-restart-signal', (req, res) => {
1000
+ extensionData.restartRequestedAt = null;
1001
+ res.json({ success: true });
1002
+ });
1003
+ };