@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,284 @@
1
+ /**
2
+ * Tool: execute_js
3
+ * Execute custom JavaScript code in the active Chrome tab's page context
4
+ *
5
+ * Phase 2.4: Refactored from (args, extensionData, httpPort) to (args, bridgeClient).
6
+ * All state access goes through BridgeClient HTTP calls.
7
+ */
8
+
9
+ import { StateValidator } from '../utils/state-validator.js';
10
+ import { RateLimiter } from '../utils/rate-limiter.js';
11
+ import { validatePrerequisites, formatHardGuard } from '../utils/workflow-helper.js';
12
+
13
+ function analyzeCodeForInsights(code, domain) {
14
+ if (!domain || domain === 'localhost' || domain === '127.0.0.1') return null;
15
+ const selectorRegex = /querySelector(?:All)?\s*\(\s*['"`]([^'"`]+)['"`]/g;
16
+ const selectors = [];
17
+ let m;
18
+ while ((m = selectorRegex.exec(code)) !== null) {
19
+ if (!selectors.includes(m[1])) selectors.push(m[1]);
20
+ }
21
+ if (selectors.length === 0) return null;
22
+ const hasDisambiguation = /\.filter\s*\(|\.closest\s*\(|\.find\s*\(/.test(code);
23
+ return { selectors, hasDisambiguation, domain };
24
+ }
25
+
26
+ export const executeJsTool = {
27
+ name: 'execute_js',
28
+ description: `Execute custom JavaScript code in a Chrome tab's page context.
29
+
30
+ WORKFLOW POSITION: πŸ”΄ Advanced - Use with caution
31
+
32
+ PREREQUISITES:
33
+ - βœ… Extension must be connected
34
+ - Chrome tab must be open (active tab used by default)
35
+
36
+ MULTI-TAB: Call debug_mcp_state() first to get tab IDs, then pass tabId to target a specific tab.
37
+ Example: execute_js({ code: "document.title", tabId: 142 })
38
+
39
+ SECURITY & LIMITS:
40
+ - ⚠️ Code runs in page context (full page access)
41
+ - ⚠️ Max code size: 10KB
42
+ - ⚠️ Timeout: 5-30 seconds (configurable)
43
+ - ❌ 'ev' + 'al()' and 'Function()' blocked
44
+ - ❌ No setInterval (memory leak risk)
45
+ - ⚠️ cspBypass: true reloads the tab β€” all SPA state is LOST (open modals, form inputs, navigation history, React/Vue component state). Only use after a confirmed CSP error, never as default.
46
+
47
+ PARAMETERS:
48
+ - code: JavaScript code to execute (required)
49
+ - timeout: Execution timeout in ms (default: 5000, max: 30000)
50
+ - tabId: Target a specific tab by ID (optional β€” omit to use active tab)
51
+ - context: 'page' (default) or 'background'. Use 'background' to run in the extension service worker β€” bypasses CORS, no DOM access, can use fetch with credentials.
52
+
53
+ WORKFLOW:
54
+ 1. execute_js (Automatically waits for result)
55
+
56
+ EXAMPLE:
57
+ execute_js({ code: "document.querySelectorAll('a').length" })
58
+
59
+ Use this for advanced DOM queries, data extraction, or page manipulation.
60
+ Note: This tool is BLOCKING. It will wait up to 10s for the result from Chrome.`,
61
+
62
+ inputSchema: {
63
+ type: 'object',
64
+ properties: {
65
+ code: {
66
+ type: 'string',
67
+ description: 'JavaScript code to execute'
68
+ },
69
+ timeout: {
70
+ type: 'number',
71
+ description: 'Execution timeout in ms (default: 5000, max: 30000)',
72
+ default: 5000
73
+ },
74
+ waitResult: {
75
+ type: 'boolean',
76
+ description: 'Whether to wait for results (default: true)',
77
+ default: true
78
+ },
79
+ tabId: {
80
+ type: 'number',
81
+ description: 'Target tab ID (optional). Omit to use active tab. Get IDs from debug_mcp_state().'
82
+ },
83
+ context: {
84
+ type: 'string',
85
+ enum: ['page', 'background'],
86
+ description: "Execution context. 'page' (default): runs in page via content script, has DOM access, subject to CORS. 'background': runs in extension service worker, no DOM access, no CORS restrictions, has access to fetch/chrome.cookies/chrome.tabs."
87
+ },
88
+ cspBypass: {
89
+ type: 'boolean',
90
+ description: "Enable domain-specific CSP bypass when execute_js fails with CSP/unsafe-eval errors. Reloads the tab ONCE and strips CSP headers for that domain only. WARNING: tab reload resets SPA state. Only use after a CSP error β€” do not set by default.",
91
+ default: false
92
+ }
93
+ },
94
+ required: ['code']
95
+ },
96
+
97
+ handler: async (args, bridgeClient) => {
98
+ // CRITICAL: Check for pending execution FIRST
99
+ const noPendingValidation = StateValidator.validateNoPendingExecution(bridgeClient);
100
+ if (!noPendingValidation.valid) {
101
+ return StateValidator.formatValidationError(noPendingValidation);
102
+ }
103
+
104
+ // Hard guard: block if dependency check fails
105
+ const prereqValidation = validatePrerequisites('execute_js', bridgeClient);
106
+ if (!prereqValidation.valid) {
107
+ return formatHardGuard('execute_js', prereqValidation);
108
+ }
109
+
110
+ return await RateLimiter.executeWithRetry(
111
+ 'execute_js',
112
+ async () => {
113
+ const { code, timeout = 5000, waitResult = true, tabId, context = 'page', cspBypass = false } = args || {};
114
+
115
+ // Validate input
116
+ if (!code || typeof code !== 'string') {
117
+ return {
118
+ isError: true,
119
+ content: [{
120
+ type: 'text',
121
+ text: 'Error: Code parameter required (must be a string)\n\nExample: execute_js({ code: "document.title" })'
122
+ }]
123
+ };
124
+ }
125
+
126
+ // Security: Code size limit
127
+ if (code.length > 10000) {
128
+ return {
129
+ isError: true,
130
+ content: [{
131
+ type: 'text',
132
+ text: `Error: Code too large (${code.length} bytes, max 10KB)\n\nTIP: Break complex operations into smaller chunks`
133
+ }]
134
+ };
135
+ }
136
+
137
+ // Security: Block eval and Function constructor
138
+ if (code.includes('ev' + 'al(') || code.includes('Function(')) {
139
+ return {
140
+ isError: true,
141
+ content: [{
142
+ type: 'text',
143
+ text: 'Security Error: ev' + 'al() and Function() constructor are not allowed\n\nUse direct JavaScript instead'
144
+ }]
145
+ };
146
+ }
147
+
148
+ // Security: Block infinite loops
149
+ if (/while\s*\(\s*true\s*\)/i.test(code) || /for\s*\(\s*;\s*;\s*\)/.test(code)) {
150
+ return {
151
+ isError: true,
152
+ content: [{
153
+ type: 'text',
154
+ text: 'Security Error: Infinite loop detected (while(true) / for(;;)) β€” not allowed\n\nUse a bounded loop with a counter or break condition instead'
155
+ }]
156
+ };
157
+ }
158
+
159
+ // Security: Warn about setInterval
160
+ if (code.includes('setInterval')) {
161
+ return {
162
+ content: [{
163
+ type: 'text',
164
+ text: 'Warning: setInterval detected (memory leak risk)\n\nUse setTimeout for one-time delayed execution instead'
165
+ }]
166
+ };
167
+ }
168
+
169
+ // Validate timeout
170
+ const validTimeout = Math.min(Math.max(timeout, 1000), 30000); // 1s-30s
171
+
172
+ // Validate connection
173
+ const connValidation = StateValidator.validateConnection(bridgeClient);
174
+ if (!connValidation.valid) {
175
+ return StateValidator.formatValidationError(connValidation);
176
+ }
177
+
178
+ try {
179
+ // Queue execution request via bridge daemon
180
+ const requestId = `js-${Date.now()}`;
181
+ await bridgeClient.queueRequest('execute-js', {
182
+ code,
183
+ timeout: validTimeout,
184
+ id: requestId,
185
+ timestamp: new Date().toISOString(),
186
+ context,
187
+ cspBypass,
188
+ ...(tabId ? { tabId } : {})
189
+ });
190
+
191
+ // If not waiting, just return the queue message
192
+ if (!waitResult) {
193
+ return {
194
+ content: [{
195
+ type: 'text',
196
+ text: `## JS Execution Sent\n\nRequest ID: ${requestId}\nCommand sent to browser (Fire-and-forget). No result will be returned.`
197
+ }]
198
+ };
199
+ }
200
+
201
+ // BLOCKING WAIT: Poll bridge daemon for result
202
+ // Wait longer than code timeout to allow for overhead (max 15s or code timeout + 2s)
203
+ const waitTimeout = Math.min(validTimeout + 2000, 15000);
204
+ const record = await bridgeClient.waitForResult('js-execution', requestId, waitTimeout);
205
+
206
+ if (record) {
207
+ // CSP bypass banner: when cspBypass:true is used, the page is reloaded
208
+ // to strip CSP headers. SPA state (modals, form inputs, navigation
209
+ // history, React/Vue component state) is LOST. Make this explicit so
210
+ // users don't wonder why the DOM looks "different" after execution.
211
+ let cspBanner = '';
212
+ if (record._cspBypassed || cspBypass) {
213
+ cspBanner = `⚠️ **CSP bypass active for \`${record._cspBypassed || domain || 'this domain'}\`** β€” the tab was reloaded to strip CSP headers. SPA state (modals, form inputs, navigation history, component state) was lost.\n\n`;
214
+ }
215
+
216
+ let output = cspBanner + '## JS Execution Success\n\n';
217
+ if (record.result?.error) {
218
+ output = cspBanner + '## JS Execution Error\n\n';
219
+ output += `\`\`\`\n${record.result.error}\n\`\`\`\n`;
220
+ } else {
221
+ const resultStr = JSON.stringify(record.result, null, 2);
222
+ if (record.result === null && resultStr === 'null') {
223
+ const typeHint = record.__typeHint;
224
+ output += `Result:\n\`\`\`json\nnull\n\`\`\`\n`;
225
+ if (typeHint) {
226
+ output += `\nπŸ” **Type diagnostic:** The code returned a \`${typeHint}\` value. `;
227
+ if (typeHint === 'undefined') {
228
+ output += `Add an explicit \`return\` statement to return a value.\n`;
229
+ } else if (typeHint === 'function') {
230
+ output += `Functions cannot be serialized. Call the function or return its result instead.\n`;
231
+ } else if (typeHint === 'Promise (await or return the resolved value)') {
232
+ output += `Use \`await\` or \`.then()\` to resolve the Promise, or mark the code as async.\n`;
233
+ } else {
234
+ output += `This type cannot be serialized to JSON. Use \`JSON.stringify()\` or extract primitive values.\n`;
235
+ }
236
+ } else {
237
+ output += `\nπŸ’‘ Tip: \`null\` means the code returned \`undefined\` or a non-serializable value. `;
238
+ output += `Add an explicit \`return\` or wrap in \`JSON.stringify()\` to inspect complex objects.\n`;
239
+ }
240
+ } else if (resultStr.length > 3000) {
241
+ output += `Result (truncated):\n\`\`\`json\n${resultStr.substring(0, 3000)}...\n\`\`\`\n`;
242
+ } else {
243
+ output += `Result:\n\`\`\`json\n${resultStr}\n\`\`\`\n`;
244
+ }
245
+ }
246
+ // Insight prompt: disambiguation pattern detected β†’ save_site_profile reminder
247
+ const domain = bridgeClient.activeTabUrl
248
+ ? (() => { try { return new URL(bridgeClient.activeTabUrl).hostname; } catch { return null; } })()
249
+ : null;
250
+ const insight = analyzeCodeForInsights(code, domain);
251
+ if (insight && insight.hasDisambiguation) {
252
+ await bridgeClient.incrementInsight(insight.domain);
253
+ output += `\n\nπŸ’‘ **Insight fΔ±rsatΔ±** β€” \`${insight.domain}\` ΓΌzerinde Γ§alıştΔ±n.\n`;
254
+ output += `Keşfedilen selector'lar: ${insight.selectors.map(s => `\`${s}\``).join(', ')}\n`;
255
+ output += `Disambiguation pattern tespit edildi (.filter / .closest / .find kullanΔ±ldΔ±)\n`;
256
+ output += `β†’ Γ–ΔŸrendiğin varsa kaydet: \`manage_site_profile({ action: 'save', domain: "${insight.domain}", notes: "...", stableSelectors: [...] })\``;
257
+ }
258
+ return { content: [{ type: 'text', text: output }] };
259
+ } else {
260
+ // FALLBACK: Timeout β€” the request has been consumed by waitForResult
261
+ return {
262
+ isError: true,
263
+ content: [{
264
+ type: 'text',
265
+ text: `## JS Execution Timeout\n\nNo response from Chrome within ${waitTimeout}ms. The request has been cleared.\nRequest ID: ${requestId}\n\nπŸ’‘ Your code may be slow on this heavy DOM page. Alternatives:\n1. Lower timeout: execute_js({ code: "...", timeout: 3000 })\n2. Use background context: execute_js({ code: "...", context: "background" })\n3. Simplify code: break complex queries into steps`
266
+ }]
267
+ };
268
+ }
269
+ } catch (error) {
270
+ return {
271
+ isError: true,
272
+ content: [{
273
+ type: 'text',
274
+ text: `Error during execution: ${error.message}\n\nREQUIRED STEPS:\n1. Verify browser connection in debug_mcp_state()\n2. Check code syntax and context compatibility`
275
+ }]
276
+ };
277
+ }
278
+ },
279
+ {
280
+ maxRetries: 2
281
+ }
282
+ );
283
+ }
284
+ };
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Export Session Tool
3
+ * Aktif browser'daki cookie'leri, localStorage/sessionStorage token'larΔ±nΔ± export eder.
4
+ * Playwright instance'Δ±na aktarmak iΓ§in kullanΔ±lΔ±r.
5
+ */
6
+
7
+ export const exportSessionTool = {
8
+ name: 'export_session',
9
+ description: `This is a tool from the dombridge MCP server.
10
+ Exports the active browser session: cookies, auth tokens, and storage data from the current page's domain.
11
+
12
+ WORKFLOW POSITION: πŸ” Auth Export β€” call after login, before Playwright handoff. Requires active browser session.
13
+
14
+ USE CASE: Export session once β†’ hand it to a Playwright instance β†’ headless scraping without re-login.
15
+
16
+ RETURNS:
17
+ - cookies: All cookies for the domain (name, value, domain, path, secure, httpOnly, expiry)
18
+ - localStorage: Auth/token related keys (filtered for security)
19
+ - sessionStorage: Auth/token related keys (filtered for security)
20
+ - domain: The domain the session was exported from
21
+ - exportedAt: ISO timestamp
22
+
23
+ SECURITY NOTE:
24
+ - Only reads the current active tab's domain
25
+ - localStorage/sessionStorage filtered to auth-related keys only (token, auth, session, jwt, etc.)
26
+ - Data is returned directly to you β€” never stored on disk by this tool
27
+
28
+ MULTI-TAB: Call debug_mcp_state() first to get tab IDs, then pass tabId to export a specific tab's session.
29
+ Example: export_session({ tabId: 142 })
30
+
31
+ WORKFLOW:
32
+ 1. Log in to target site in browser (Trendyol, etc.)
33
+ 2. Navigate to any page on that site
34
+ 3. Call export_session() β†’ get cookies + tokens
35
+ 4. Pass the exported data to Playwright to continue headless
36
+ `,
37
+ inputSchema: {
38
+ type: 'object',
39
+ properties: {
40
+ tabId: {
41
+ type: 'number',
42
+ description: 'Target tab ID (optional). Omit to use active tab. Get IDs from debug_mcp_state().'
43
+ }
44
+ }
45
+ },
46
+ handler: async (args, bridgeClient) => {
47
+ if (!bridgeClient.isConnected) {
48
+ return {
49
+ content: [{
50
+ type: 'text',
51
+ text: `❌ Error: Extension not connected.\nREQUIRED STEPS:\n1. Reload webpage\n2. Ensure the Chrome extension is active`
52
+ }],
53
+ isError: true
54
+ };
55
+ }
56
+
57
+ const { tabId } = args || {};
58
+ const requestId = `export-session-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
59
+
60
+ try {
61
+ await bridgeClient.queueRequest('export-session', { id: requestId, ...(tabId ? { tabId } : {}) });
62
+
63
+ const resultItem = await bridgeClient.waitForResult('export-session', requestId, 15000);
64
+
65
+ if (!resultItem) {
66
+ return {
67
+ content: [{ type: 'text', text: `❌ Timeout: export_session did not complete within 15000ms.` }],
68
+ isError: true
69
+ };
70
+ }
71
+
72
+ if (resultItem.error) {
73
+ return {
74
+ content: [{ type: 'text', text: `❌ export_session failed: ${resultItem.error}` }],
75
+ isError: true
76
+ };
77
+ }
78
+
79
+ const r = resultItem.result;
80
+ if (!r) {
81
+ return {
82
+ content: [{ type: 'text', text: `❌ export_session returned no data.` }],
83
+ isError: true
84
+ };
85
+ }
86
+
87
+ const lines = [
88
+ `πŸ” **Session Export: ${r.domain}**`,
89
+ `Exported at: ${r.exportedAt}`,
90
+ `URL: ${r.url}`,
91
+ ''
92
+ ];
93
+
94
+ // Cookies
95
+ lines.push(`**πŸͺ Cookies: ${r.cookieCount} found**`);
96
+ if (r.cookies.length > 0) {
97
+ // Auth cookie'leri vurgula (analytics cookie'leri hariΓ§)
98
+ const ANALYTICS_COOKIE_PREFIXES = ['_ym_', '_ga', '_gid', '_fbp', '_fbc', '_clck', '_clsk', '_hjid'];
99
+ const isAnalyticsCookie = (name) => ANALYTICS_COOKIE_PREFIXES.some(p => name.startsWith(p));
100
+ const authCookies = r.cookies.filter(c =>
101
+ !isAnalyticsCookie(c.name) &&
102
+ /token|auth|session|jwt|bearer|access|refresh|login|sid|uid/i.test(c.name)
103
+ );
104
+ const otherCookies = r.cookies.filter(c =>
105
+ isAnalyticsCookie(c.name) ||
106
+ !/token|auth|session|jwt|bearer|access|refresh|login|sid|uid/i.test(c.name)
107
+ );
108
+
109
+ if (authCookies.length > 0) {
110
+ lines.push(`\n_Auth cookies (${authCookies.length}):_`);
111
+ authCookies.forEach(c => {
112
+ const expiry = c.expirationDate
113
+ ? ` | expires: ${new Date(c.expirationDate * 1000).toISOString().split('T')[0]}`
114
+ : ' | session';
115
+ lines.push(` - \`${c.name}\` = \`${c.value.substring(0, 40)}${c.value.length > 40 ? '…' : ''}\`${expiry}`);
116
+ });
117
+ }
118
+
119
+ if (otherCookies.length > 0) {
120
+ lines.push(`\n_Other cookies (${otherCookies.length}):_`);
121
+ otherCookies.slice(0, 5).forEach(c => {
122
+ lines.push(` - \`${c.name}\``);
123
+ });
124
+ if (otherCookies.length > 5) {
125
+ lines.push(` - _... ${otherCookies.length - 5} more_`);
126
+ }
127
+ }
128
+ } else {
129
+ lines.push(' _(no cookies found β€” are you logged in?)_');
130
+ }
131
+
132
+ // localStorage
133
+ const lsKeys = Object.keys(r.localStorage);
134
+ if (lsKeys.length > 0) {
135
+ lines.push(`\n**πŸ—„οΈ localStorage (${lsKeys.length} auth keys):**`);
136
+ lsKeys.forEach(k => {
137
+ const val = r.localStorage[k];
138
+ lines.push(` - \`${k}\` = \`${val.substring(0, 60)}${val.length > 60 ? '…' : ''}\``);
139
+ });
140
+ }
141
+
142
+ // sessionStorage
143
+ const ssKeys = Object.keys(r.sessionStorage);
144
+ if (ssKeys.length > 0) {
145
+ lines.push(`\n**πŸ—ƒοΈ sessionStorage (${ssKeys.length} auth keys):**`);
146
+ ssKeys.forEach(k => {
147
+ const val = r.sessionStorage[k];
148
+ lines.push(` - \`${k}\` = \`${val.substring(0, 60)}${val.length > 60 ? '…' : ''}\``);
149
+ });
150
+ }
151
+
152
+ lines.push('');
153
+ lines.push('---');
154
+ lines.push('πŸ’‘ **Playwright usage:**');
155
+ lines.push('```js');
156
+ lines.push('await context.addCookies(session.cookies);');
157
+ lines.push('await page.evaluate(ls => Object.entries(ls).forEach(([k,v]) => localStorage.setItem(k,v)), session.localStorage);');
158
+ lines.push('```');
159
+
160
+ return {
161
+ content: [{ type: 'text', text: lines.join('\n') }]
162
+ };
163
+
164
+ } catch (e) {
165
+ return {
166
+ content: [{ type: 'text', text: `❌ Error: ${e.message}` }],
167
+ isError: true
168
+ };
169
+ }
170
+ }
171
+ };