@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,471 @@
1
+ /**
2
+ * Tool: get_network_trace
3
+ * Retrieves network API calls that match the selected DOM element
4
+ *
5
+ * Phase 2.4: Refactored from (args, extensionData) to (args, bridgeClient).
6
+ * All state reads go through bridgeClient property getters.
7
+ */
8
+
9
+ import { StateValidator } from '../utils/state-validator.js';
10
+ import { RateLimiter } from '../utils/rate-limiter.js';
11
+ import { validatePrerequisites, formatHardGuard, suggestNextTools, formatWorkflowSuggestions } from '../utils/workflow-helper.js';
12
+ import { enrichProfile } from '../profiles/profile-enricher.js';
13
+ import { extractDomain, getSavedSsrPaths } from '../profiles/profile-manager.js';
14
+ import { runScript } from '../utils/run-script.js';
15
+
16
+ export const getNetworkTraceTool = {
17
+ name: 'get_network_trace',
18
+ description: `Retrieve network API calls (REST or WebSocket) that match the selected DOM element's data.
19
+
20
+ WORKFLOW POSITION: 🟒 Second Step - Call after get_element
21
+
22
+ PREREQUISITES:
23
+ - βœ… get_element must be called first (or element selected manually)
24
+ - Extension must have captured network activity
25
+
26
+ TAB CONTEXT NOTE: Network trace is stored globally (not per-tab). It reflects
27
+ the tab that last triggered element selection. If you called get_element({ tabId: N })
28
+ and then switched context, call get_element again before get_network_trace to ensure
29
+ the trace matches the correct tab's data.
30
+
31
+ PARAMETERS:
32
+ - minConfidence: Minimum confidence threshold (0.0-1.0). Default: 0.5 (REST), 0.1 (WebSocket β€” real-time data scores lower on fuzzy matching)
33
+ - limit: Max results to show (default: 3 β€” top matches by confidence)
34
+ - protocol: 'rest' (default) | 'ws' | 'all' β€” switch to WebSocket trace for real-time sites
35
+
36
+ RETURNS: Top matching API calls with:
37
+ - Confidence scores (how likely the API populates this element)
38
+ - πŸ” Smart Analysis: Headers, GraphQL parsing for top matches
39
+ - JSON paths to matched values (use paths to extract data via execute_js if needed)
40
+
41
+ COMMON WORKFLOWS:
42
+ 1. get_element β†’ get_network_trace (REST APIs)
43
+ 2. get_element β†’ get_network_trace(protocol:'ws') (WebSocket: Binance, Discord, trading apps)
44
+
45
+ Use protocol:'ws' when dealing with real-time sites (stock prices, live chat, notifications).`,
46
+
47
+ inputSchema: {
48
+ type: 'object',
49
+ properties: {
50
+ minConfidence: {
51
+ type: 'number',
52
+ description: 'Minimum confidence score (0.0-1.0). Default: 0.5 (REST), 0.1 (WebSocket β€” real-time payloads score lower on fuzzy matching)',
53
+ default: 0.5
54
+ },
55
+ limit: {
56
+ type: 'number',
57
+ description: 'Maximum number of results. Default: 3',
58
+ default: 3
59
+ },
60
+ protocol: {
61
+ type: 'string',
62
+ enum: ['rest', 'ws', 'all'],
63
+ description: "Protocol to trace. 'rest' = HTTP/XHR (default), 'ws' = WebSocket, 'all' = both",
64
+ default: 'rest'
65
+ },
66
+ verbose: {
67
+ type: 'boolean',
68
+ description: 'Return full detail: all data paths, request headers, GraphQL fields, response bodies (default: false)',
69
+ default: false
70
+ }
71
+ }
72
+ },
73
+
74
+ handler: async (args, bridgeClient) => {
75
+ // Hard guard: element must be selected first
76
+ const prereqValidation = validatePrerequisites('get_network_trace', bridgeClient);
77
+ if (!prereqValidation.valid) {
78
+ return formatHardGuard('get_network_trace', prereqValidation);
79
+ }
80
+
81
+ return await RateLimiter.executeWithRetry(
82
+ 'get_network_trace',
83
+ async () => {
84
+ const protocol = args?.protocol || 'rest';
85
+ const callerTabId = args?.tabId;
86
+
87
+ // Tab-mismatch guard for both REST and WS:
88
+ // If the stored trace is for a different tab than the caller, refuse
89
+ // rather than leaking the previous tab's data. Caller can re-select
90
+ // an element on the right tab, or drop tabId to use the latest trace.
91
+ const traceTabId = protocol === 'ws'
92
+ ? bridgeClient.websocketTrace?.tabId
93
+ : bridgeClient.networkTrace?.tabId;
94
+ if (traceTabId && callerTabId && traceTabId !== callerTabId) {
95
+ return {
96
+ content: [{
97
+ type: 'text',
98
+ text: `⚠️ ${protocol === 'ws' ? 'WebSocket' : 'Network'} trace is for tab ${traceTabId}, but you targeted tab ${callerTabId}.\n` +
99
+ `Re-select an element on tab ${callerTabId} or call without tabId to use the latest trace.\n\n` +
100
+ `Call: \`get_element({ tabId: ${callerTabId}, ... })\` to refresh.`
101
+ }]
102
+ };
103
+ }
104
+
105
+ // WebSocket real-time data typically scores lower on fuzzy text matching
106
+ // because the payload is binary/structured (Socket.IO frames, protobuf).
107
+ // Lower default to surface useful matches; users can raise it for precision.
108
+ const defaultMinConfidence = protocol === 'ws' ? 0.1 : 0.5;
109
+ const minConfidence = (args?.minConfidence ?? defaultMinConfidence) * 100;
110
+ const limit = args?.limit || 3;
111
+ const verbose = args?.verbose || false;
112
+
113
+ // WebSocket trace path
114
+ if (protocol === 'ws') {
115
+ return formatWebSocketTrace(bridgeClient, minConfidence, limit, verbose);
116
+ }
117
+
118
+ // Validate network trace data (REST path)
119
+ const validation = StateValidator.validateNetworkTrace(bridgeClient);
120
+ if (!validation.valid) {
121
+ if (validation.error === 'No network trace available' || validation.error === 'Network trace has no matches') {
122
+ // Navigation detection: if stored element has a different pageUrl than current URL,
123
+ // the user navigated away. Attempt to re-resolve the selector on the new page.
124
+ const currentPageUrl = bridgeClient.activeTabUrl || '';
125
+ const elementPageUrl = bridgeClient.selectedElement?.pageUrl || '';
126
+ const navigated = elementPageUrl && currentPageUrl && elementPageUrl !== currentPageUrl;
127
+
128
+ if (navigated && bridgeClient.selectedElement?.cssSelector) {
129
+ try {
130
+ const reResolveResult = await runScript(
131
+ `(function() {
132
+ var el = document.querySelector(${JSON.stringify(bridgeClient.selectedElement.cssSelector)});
133
+ if (!el) return { found: false, reason: 'selector_no_match' };
134
+ return {
135
+ found: true,
136
+ tagName: el.tagName,
137
+ textContent: (el.textContent || '').trim().substring(0, 200),
138
+ attributes: (function() {
139
+ var attrs = {};
140
+ for (var i = 0; i < el.attributes.length; i++) {
141
+ attrs[el.attributes[i].name] = el.attributes[i].value;
142
+ }
143
+ return attrs;
144
+ })()
145
+ };
146
+ })()`,
147
+ bridgeClient,
148
+ 5000
149
+ );
150
+
151
+ if (reResolveResult && reResolveResult.found) {
152
+ return {
153
+ content: [{
154
+ type: 'text',
155
+ text: `⚠️ Page navigation detected (from ${elementPageUrl} β†’ ${currentPageUrl}).\n\n` +
156
+ `The selector \`${bridgeClient.selectedElement.cssSelector}\` still matches an element on the new page:\n\n` +
157
+ `- **Tag:** ${reResolveResult.tagName}\n` +
158
+ `- **Text:** ${reResolveResult.textContent.substring(0, 100)}${reResolveResult.textContent.length > 100 ? '...' : ''}\n` +
159
+ `- **Attributes:** ${Object.entries(reResolveResult.attributes || {}).slice(0, 5).map(([k, v]) => `${k}="${v}"`).join(', ')}\n\n` +
160
+ `However, the network trace data was cleared during navigation.\n\n` +
161
+ `**Next steps:**\n` +
162
+ `1. \`get_element({ css: '${bridgeClient.selectedElement.cssSelector}' })\` β€” re-select on the new page\n` +
163
+ `2. \`get_network_trace()\` β€” capture fresh network data\n` +
164
+ `3. \`discover_apis()\` β€” list all API calls on the new page`
165
+ }]
166
+ };
167
+ } else {
168
+ return {
169
+ content: [{
170
+ type: 'text',
171
+ text: `⚠️ Page navigation detected (from ${elementPageUrl} β†’ ${currentPageUrl}).\n\n` +
172
+ `The previous selector \`${bridgeClient.selectedElement.cssSelector}\` no longer matches any element on the new page.\n\n` +
173
+ `**Next steps:**\n` +
174
+ `1. \`analyze_page()\` β€” understand the new page structure\n` +
175
+ `2. \`get_element({ css: '...' })\` β€” select a new element\n` +
176
+ `3. \`get_network_trace()\` β€” trace APIs on the new page`
177
+ }]
178
+ };
179
+ }
180
+ } catch (_) {
181
+ // Re-resolve failed β€” fall through to default message
182
+ }
183
+ }
184
+ // SSR fallback: check saved window.__*__PROPS paths
185
+ // SSR fallback: check saved window.__*__PROPS paths
186
+ const pageUrl = bridgeClient.selectedElement?.pageUrl || bridgeClient.activeTabUrl || '';
187
+ const domain = extractDomain(pageUrl);
188
+ const ssrPaths = domain ? getSavedSsrPaths(domain) : [];
189
+
190
+ let ssrHint = '';
191
+ if (ssrPaths.length > 0) {
192
+ ssrHint = `\n\n⚠️ SSR-detected site. Try extract_data() instead:\n`;
193
+ ssrHint += `- Window props: ${ssrPaths.map(p => p.key).join(', ')}`;
194
+ }
195
+
196
+ return {
197
+ content: [{
198
+ type: 'text',
199
+ text: `No matching API response found for the selected element.${ssrHint}\n\n` +
200
+ `The page may be static/SSR-driven, the value may be cached, or the selected text may not appear verbatim in captured response bodies.\n\n` +
201
+ `TRY:\n` +
202
+ `- extract_data() β€” list window.__*__PROPS paths\n` +
203
+ `- execute_js({ code: "window.__PATH__.fieldName" }) β€” extract value\n` +
204
+ `- manage_site_profile({ action: 'save', dataSchema: {...} }) β€” persist schema`
205
+ }]
206
+ };
207
+ }
208
+ return StateValidator.formatValidationError(validation);
209
+ }
210
+
211
+ const trace = validation.data;
212
+ const elementPageUrl = bridgeClient.selectedElement?.pageUrl || '';
213
+ const currentPageUrl = bridgeClient.activeTabUrl || '';
214
+ const isStale = elementPageUrl && currentPageUrl && elementPageUrl !== currentPageUrl;
215
+ const filteredMatches = trace.matches
216
+ .filter(m => (m.matchInfo?.confidence || m.confidence || 0) >= minConfidence)
217
+ .slice(0, limit);
218
+
219
+ // Auto-save: yΓΌksek gΓΌvenilirlikli match'leri profil'e kaydet
220
+ const pageUrl = bridgeClient.selectedElement?.pageUrl || bridgeClient.activeTabUrl || '';
221
+ const domain = extractDomain(pageUrl);
222
+ if (domain && filteredMatches.some(m => (m.matchInfo?.confidence ?? m.confidence ?? 0) >= 80)) {
223
+ enrichProfile(domain, 'get_network_trace', { matches: filteredMatches });
224
+ }
225
+
226
+ // Format output
227
+ let output = `## Network Trace\n\n`;
228
+ if (isStale) {
229
+ output += `⚠️ Selected element is from a different page (${elementPageUrl}).\n`;
230
+ output += ` Current tab: ${currentPageUrl}\n`;
231
+ output += ` Network trace data may not match. Re-select with \`get_element\` for accurate results.\n\n`;
232
+ }
233
+ const searchTerm = trace.elementValue != null ? trace.elementValue : null;
234
+ if (searchTerm != null) {
235
+ output += `Searched for: "${searchTerm}"\n`;
236
+ } else {
237
+ output += `⚠️ Element state cleared (page navigation detected).\n`;
238
+ output += `Call \`get_element({ selectorInfo: { css: 'YOUR_SELECTOR' } })\` to re-select an element for accurate matching.\n`;
239
+ output += `Showing all captured API calls sorted by confidence:\n`;
240
+ }
241
+ output += `Total matches: ${trace.totalMatches}\n`;
242
+ output += `Showing: ${filteredMatches.length} (confidence >= ${minConfidence}%)\n\n`;
243
+
244
+ if (filteredMatches.length === 0) {
245
+ output += `No matches found with confidence >= ${minConfidence}%\n\n`;
246
+ output += `TRY:\n`;
247
+ output += `- Lower minConfidence parameter\n`;
248
+ output += `- Check get_selected_element output for static data sources (SSR/Initial State)\n`;
249
+ } else {
250
+ output += `## πŸ“‘ API Details\n\n`;
251
+
252
+ filteredMatches.forEach((match, idx) => {
253
+ const confidence = match.matchInfo?.confidence || match.confidence || 0;
254
+ const isTopMatch = idx < 3 || confidence >= 90;
255
+ const method = match.method || 'GET';
256
+ const status = match.status || match.statusCode || '';
257
+ const url = match.url || match.requestUrl || '(URL not captured)';
258
+
259
+ output += `### [${idx + 1}] ${method}${status ? ` ${status}` : ''} (Confidence: ${confidence.toFixed(1)}%)\n`;
260
+ output += `πŸ”— URL: ${url}\n`;
261
+
262
+ const paths = match.dataPath || match.matchInfo?.paths || [];
263
+ if (Array.isArray(paths) && paths.length > 0) {
264
+ const pathStrings = paths.map(p =>
265
+ typeof p === 'string' ? p : (p.path || p.jsonPath || p.key || JSON.stringify(p))
266
+ );
267
+ output += `πŸ“ Matches Path: ${pathStrings.join(', ')}\n`;
268
+ }
269
+
270
+ // GraphQL detection (summary: op name only; verbose: full fields)
271
+ const isGQL = isGraphQLRequest(match);
272
+ if (isGQL) {
273
+ const parsed = parseGraphQLRequest(match);
274
+ if (parsed) {
275
+ if (parsed.type === 'batch') {
276
+ output += `βš›οΈ GraphQL: Batched (${parsed.queries.length} queries)\n`;
277
+ } else {
278
+ output += `βš›οΈ GraphQL: ${parsed.operation} ${parsed.operationName || 'Anonymous'}`;
279
+ if (verbose) {
280
+ output += ` β€” Fields: ${parsed.fields.slice(0, 10).join(', ')}${parsed.fields.length > 10 ? '...' : ''}`;
281
+ }
282
+ output += `\n`;
283
+ }
284
+ }
285
+ }
286
+
287
+ // Headers (summary: content-type only for top match; verbose: auth headers too)
288
+ if (isTopMatch && match.requestHeaders) {
289
+ if (verbose) {
290
+ const critical = ['cookie', 'authorization', 'x-api-key', 'user-agent', 'x-csrf-token', 'content-type'];
291
+ const headersToShow = Object.entries(match.requestHeaders)
292
+ .filter(([k]) => critical.includes(k.toLowerCase()));
293
+ if (headersToShow.length > 0) {
294
+ output += `\nπŸ” Headers:\n\`\`\`\n`;
295
+ headersToShow.forEach(([k, v]) => output += `${k}: ${v}\n`);
296
+ output += `\`\`\`\n`;
297
+ }
298
+ } else {
299
+ const ct = Object.entries(match.requestHeaders || {}).find(([k]) => k.toLowerCase() === 'content-type');
300
+ if (ct) output += ` Content-Type: ${ct[1]}\n`;
301
+ }
302
+ }
303
+ output += `\n---\n\n`;
304
+ });
305
+
306
+ // Add workflow suggestions
307
+ const suggestions = suggestNextTools('get_network_trace');
308
+ output += formatWorkflowSuggestions(suggestions);
309
+ }
310
+
311
+ return {
312
+ content: [{
313
+ type: 'text',
314
+ text: output
315
+ }]
316
+ };
317
+ },
318
+ { maxRetries: 3 }
319
+ );
320
+ }
321
+ };
322
+
323
+ /**
324
+ * WebSocket Trace (Merged from get-websocket-trace.js)
325
+ * Use get_network_trace({ protocol: 'ws' }) to invoke.
326
+ */
327
+ function formatWebSocketTrace(bridgeClient, minConfPercent, limit) {
328
+ const wsConnections = bridgeClient.websocketConnections;
329
+ const selectedElement = bridgeClient.selectedElement;
330
+
331
+ if (!wsConnections?.connections?.length) {
332
+ return {
333
+ content: [{
334
+ type: 'text',
335
+ text: 'No WebSocket data available\n\nThis page may not use WebSockets.\nTry get_network_trace() (REST) instead.'
336
+ }]
337
+ };
338
+ }
339
+
340
+ let elementValue = '';
341
+ if (selectedElement) {
342
+ const htmlMatch = selectedElement.outerHTML?.match(/>([^<]+)</);
343
+ if (htmlMatch) elementValue = htmlMatch[1].trim();
344
+ if (selectedElement.attributes) {
345
+ const dataVal = selectedElement.attributes['data-symbol-full'] ||
346
+ selectedElement.attributes['data-symbol-short'] ||
347
+ selectedElement.attributes['data-value'];
348
+ if (dataVal) elementValue = dataVal;
349
+ }
350
+ }
351
+
352
+ const matches = [];
353
+ for (const conn of wsConnections.connections) {
354
+ for (const msg of (conn.messages || [])) {
355
+ const msgData = typeof msg.data === 'string' ? msg.data : JSON.stringify(msg.data || '');
356
+ let confidence = 0;
357
+ let matchedValue = null;
358
+ const dataPaths = [];
359
+
360
+ if (elementValue && msgData.includes(elementValue)) {
361
+ confidence = 80;
362
+ matchedValue = elementValue;
363
+ if (msg.parsed && typeof msg.parsed === 'object') {
364
+ wsFindPaths(msg.parsed, elementValue, '', dataPaths);
365
+ }
366
+ }
367
+ if (!matchedValue && elementValue) {
368
+ const numbers = elementValue.match(/[\d.,]+/g);
369
+ if (numbers) {
370
+ for (const num of numbers) {
371
+ const cleaned = num.replace(/,/g, '').replace(/\./g, '');
372
+ if (cleaned.length > 3 && msgData.includes(cleaned)) {
373
+ confidence = 60; matchedValue = cleaned; break;
374
+ }
375
+ }
376
+ }
377
+ }
378
+ if (msg.direction === 'received') confidence += 10;
379
+
380
+ if (confidence >= minConfPercent) {
381
+ matches.push({
382
+ connectionUrl: conn.url, direction: msg.direction,
383
+ confidence, matchedValue, dataPath: dataPaths,
384
+ data: msgData.substring(0, 500), timestamp: msg.timestamp
385
+ });
386
+ }
387
+ }
388
+ }
389
+
390
+ matches.sort((a, b) => b.confidence - a.confidence);
391
+ const top = matches.slice(0, limit);
392
+
393
+ let output = `## WebSocket Trace\n\nSearched for: "${elementValue || 'N/A'}"\n`;
394
+ output += `Connections: ${wsConnections.totalConnections}, Messages: ${wsConnections.totalMessages}\n`;
395
+ output += `Matches: ${matches.length} (showing top ${top.length})\n\n`;
396
+
397
+ if (top.length === 0) {
398
+ output += `No matches found with confidence >= ${minConfPercent.toFixed(0)}%\nTry lowering minConfidence or use get_network_trace() for REST APIs.\n`;
399
+ } else {
400
+ top.forEach((m, idx) => {
401
+ const dir = m.direction === 'received' ? '[IN]' : '[OUT]';
402
+ output += `---\n[${idx + 1}] ${dir} Confidence: ${m.confidence.toFixed(0)}%\n`;
403
+ output += `URL: ${m.connectionUrl?.substring(0, 70)}...\n`;
404
+ if (m.matchedValue) output += `Matched: "${m.matchedValue}"\n`;
405
+ if (m.dataPath?.length > 0) output += `Path: ${m.dataPath.slice(0, 2).join(', ')}\n`;
406
+ output += `Data: ${m.data.substring(0, 200)}${m.data.length > 200 ? '...' : ''}\n\n`;
407
+ });
408
+ }
409
+
410
+ return { content: [{ type: 'text', text: output }] };
411
+ }
412
+
413
+ function wsFindPaths(obj, value, path, results, visited = new WeakSet()) {
414
+ if (!obj || typeof obj !== 'object' || visited.has(obj)) return;
415
+ visited.add(obj);
416
+ for (const [key, val] of Object.entries(obj)) {
417
+ const newPath = path ? `${path}.${key}` : key;
418
+ if (String(val) === String(value)) results.push(newPath);
419
+ else if (typeof val === 'object' && val !== null) wsFindPaths(val, value, newPath, results, visited);
420
+ }
421
+ }
422
+
423
+ /**
424
+ * GraphQL Helpers (Merged from analyze-graphql.js)
425
+ */
426
+
427
+ function isGraphQLRequest(request) {
428
+ const url = request.url || '';
429
+ const method = request.method || '';
430
+ if (url.includes('/graphql') || url.includes('/api/graphql')) return true;
431
+ if (method === 'POST' && request.requestBody) {
432
+ try {
433
+ const body = typeof request.requestBody === 'string' ? JSON.parse(request.requestBody) : request.requestBody;
434
+ return !!(body.query || (Array.isArray(body) && body[0]?.query));
435
+ } catch { return false; }
436
+ }
437
+ return false;
438
+ }
439
+
440
+ function parseGraphQLRequest(request) {
441
+ try {
442
+ const body = typeof request.requestBody === 'string' ? JSON.parse(request.requestBody) : request.requestBody;
443
+ if (Array.isArray(body)) return { type: 'batch', queries: body.map(parseSingleQuery) };
444
+ return { type: 'single', ...parseSingleQuery(body) };
445
+ } catch { return null; }
446
+ }
447
+
448
+ function parseSingleQuery(body) {
449
+ const query = body.query || '';
450
+ const variables = body.variables || {};
451
+ const operationName = body.operationName || extractOperationName(query);
452
+ const operation = query.trim().startsWith('mutation') ? 'mutation' : 'query';
453
+ const fields = extractFields(query);
454
+ return { query, variables, operationName, operation, fields };
455
+ }
456
+
457
+ function extractOperationName(query) {
458
+ const match = query.match(/(?:query|mutation)\s+(\w+)/);
459
+ return match ? match[1] : null;
460
+ }
461
+
462
+ function extractFields(query) {
463
+ const fields = [];
464
+ const cleaned = query.replace(/#.*$/gm, '');
465
+ const fieldMatches = cleaned.matchAll(/\b([a-zA-Z_]\w*)\s*(?:\(|{|:)/g);
466
+ for (const match of fieldMatches) {
467
+ const field = match[1];
468
+ if (!['query', 'mutation', 'fragment', 'on', 'type'].includes(field)) fields.push(field);
469
+ }
470
+ return [...new Set(fields)];
471
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Tool Registry
3
+ * TΓΌm MCP toollarΔ±nΔ± merkezi olarak yΓΆnetir
4
+ *
5
+ * TOOL COUNT: 13
6
+ * REMOVED: get_selected_element, select_element β†’ merged into get_element
7
+ * get_websocket_trace β†’ merged into get_network_trace(protocol:'ws')
8
+ * load_site_profile, save_site_profile, check_site_changes β†’ merged into manage_site_profile
9
+ * CHANGED: get_network_trace β€” default limit 10β†’3, +protocol param, path serialization fix
10
+ * analyze_page β€” +includeApis param, +verbose param, summary/verbose modes
11
+ * discover_apis β€” default limit 50β†’3, sorted by status, +verbose, +force params
12
+ * debug_mcp_state β€” description updated (troubleshooting only)
13
+ * ADDED: quick_scan β€” first-call orchestrator (r6/03)
14
+ * paginate β€” multi-page / infinite scroll navigation (r6/04)
15
+ * manage_site_profile β€” unified profile CRUD (6 actions: load/save/check/delete/list/update)
16
+ */
17
+
18
+ import { getElementTool } from './get-element.js';
19
+ import { getNetworkTraceTool } from './get-network-trace.js';
20
+ import { debugMcpStateTool } from './debug-mcp-state.js';
21
+ import { executeJsTool } from './execute-js.js';
22
+ import { executeActionTool } from './action_tools.js';
23
+ import { captureScreenshotTool } from './screenshot_tools.js';
24
+ import { discoverApisTool } from './discover-apis.js';
25
+ import { analyzePageTool } from './analyze-page.js';
26
+ import { extractDataTool } from './extract-data.js';
27
+ import { exportSessionTool } from './export-session.js';
28
+ import { manageSiteProfileTool } from './manage-site-profile.js';
29
+ import { quickScanTool } from './quick-scan.js';
30
+ import { paginateTool } from './paginate.js';
31
+ import { withMetrics } from '../metrics/metrics-recorder.js';
32
+
33
+ /**
34
+ * TΓΌm kullanΔ±labilir toollar
35
+ *
36
+ * AUTONOMOUS WORKFLOW (no manual browser interaction):
37
+ * analyze_page({ includeApis: true })
38
+ * β†’ get_element({ selectorInfo: { css: '...' } })
39
+ * β†’ get_network_trace()
40
+ *
41
+ * MANUAL WORKFLOW (user clicks element in browser):
42
+ * get_element()
43
+ * β†’ get_network_trace()
44
+ *
45
+ * WEBSOCKET SITES (Binance, Discord, trading platforms):
46
+ * get_element()
47
+ * β†’ get_network_trace({ protocol: 'ws' })
48
+ *
49
+ * PROFILE MANAGEMENT (replaces load/save/check_site_changes):
50
+ * manage_site_profile({ action: 'list' })
51
+ * manage_site_profile({ action: 'load', domain: 'x.com' })
52
+ * manage_site_profile({ action: 'save', domain: 'x.com' })
53
+ * manage_site_profile({ action: 'check', domain: 'x.com' })
54
+ * manage_site_profile({ action: 'delete', domain: 'x.com' })
55
+ * manage_site_profile({ action: 'update', domain: 'x.com', notes: '...' })
56
+ */
57
+ export const allTools = [
58
+ // ── First Step ─────────────────────────────────────────────────────────────
59
+ { ...quickScanTool, handler: withMetrics('quick_scan', quickScanTool.handler) },
60
+ { ...analyzePageTool, handler: withMetrics('analyze_page', analyzePageTool.handler) },
61
+ { ...discoverApisTool, handler: withMetrics('discover_apis', discoverApisTool.handler) },
62
+ { ...extractDataTool, handler: withMetrics('extract_data', extractDataTool.handler) },
63
+ { ...exportSessionTool, handler: withMetrics('export_session', exportSessionTool.handler) },
64
+
65
+ // ── Unified Profile Management (replaces 3 old tools) ──────────────────────
66
+ { ...manageSiteProfileTool, handler: withMetrics('manage_site_profile', manageSiteProfileTool.handler) },
67
+
68
+ // ── Element Selection ───────────────────────────────────────────────────────
69
+ { ...getElementTool, handler: withMetrics('get_element', getElementTool.handler) },
70
+
71
+ // ── Data Tracing ───────────────────────────────────────────────────────────
72
+ { ...getNetworkTraceTool, handler: withMetrics('get_network_trace', getNetworkTraceTool.handler) },
73
+
74
+ // ── Execution ──────────────────────────────────────────────────────────────
75
+ { ...executeJsTool, handler: withMetrics('execute_js', executeJsTool.handler) },
76
+ { ...executeActionTool, handler: withMetrics('execute_action', executeActionTool.handler) },
77
+
78
+ // ── Navigation ─────────────────────────────────────────────────────────────
79
+ { ...paginateTool, handler: withMetrics('paginate', paginateTool.handler) },
80
+
81
+ // ── Visual & Debug ─────────────────────────────────────────────────────────
82
+ { ...captureScreenshotTool, handler: withMetrics('capture_screenshot', captureScreenshotTool.handler) },
83
+ { ...debugMcpStateTool, handler: withMetrics('debug_mcp_state', debugMcpStateTool.handler) },
84
+ ];
85
+
86
+ /**
87
+ * Tool listesini MCP formatΔ±nda dΓΆndΓΌr
88
+ */
89
+ export const getToolsList = () => {
90
+ return allTools.map(tool => ({
91
+ name: tool.name,
92
+ description: tool.description,
93
+ inputSchema: tool.inputSchema
94
+ }));
95
+ };
96
+
97
+ /**
98
+ * Δ°sme gΓΆre tool handler'Δ±nΔ± bul
99
+ */
100
+ export const getToolHandler = (toolName) => {
101
+ const tool = allTools.find(t => t.name === toolName);
102
+ return tool ? tool.handler : null;
103
+ };
104
+
105
+ /**
106
+ * Tool var mΔ± kontrol et
107
+ */
108
+ export const hasTool = (toolName) => {
109
+ return allTools.some(t => t.name === toolName);
110
+ };