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