mcpbrowser 0.3.46 → 0.3.47
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcpbrowser",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.47",
|
|
4
4
|
"mcpName": "io.github.cherchyk/mcpbrowser",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "MCP browser server - fetch web pages using real Chrome/Edge/Brave browser. Handles authentication, SSO, CAPTCHAs, and anti-bot protection. Browser automation for AI assistants.",
|
|
@@ -29,6 +29,7 @@ import { extractAndProcessHtml, waitForPageReady } from '../core/page.js';
|
|
|
29
29
|
import { MCPResponse, InformationalResponse } from '../core/responses.js';
|
|
30
30
|
import logger from '../core/logger.js';
|
|
31
31
|
import { getPluginNextSteps, getRecommendedPlugins } from '../core/plugin-loader.js';
|
|
32
|
+
import { scanPageForms } from './detect-forms.js';
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* @typedef {import('@modelcontextprotocol/sdk/types.js').Tool} Tool
|
|
@@ -38,7 +39,7 @@ import { getPluginNextSteps, getRecommendedPlugins } from '../core/plugin-loader
|
|
|
38
39
|
* Structured response for browser_click_element with JS fallback metadata
|
|
39
40
|
*/
|
|
40
41
|
export class ClickWithFallbackResponse extends MCPResponse {
|
|
41
|
-
constructor({ status, fallbackUsed = false, nativeAttempt, fallbackAttempt, postClickWait, currentUrl, html = null, message, nextSteps = [], recommendedPlugins = [] }) {
|
|
42
|
+
constructor({ status, fallbackUsed = false, nativeAttempt, fallbackAttempt, postClickWait, currentUrl, html = null, message, nextSteps = [], recommendedPlugins = [], formData = null }) {
|
|
42
43
|
super(nextSteps);
|
|
43
44
|
this.status = status;
|
|
44
45
|
this.fallbackUsed = fallbackUsed;
|
|
@@ -49,6 +50,9 @@ export class ClickWithFallbackResponse extends MCPResponse {
|
|
|
49
50
|
this.html = html;
|
|
50
51
|
this.message = message;
|
|
51
52
|
this.recommendedPlugins = recommendedPlugins;
|
|
53
|
+
this.forms = formData?.forms || [];
|
|
54
|
+
this.orphanedFields = formData?.orphanedFields || [];
|
|
55
|
+
this.totalFieldCount = formData?.totalFieldCount || 0;
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
_getAdditionalFields() {
|
|
@@ -61,7 +65,10 @@ export class ClickWithFallbackResponse extends MCPResponse {
|
|
|
61
65
|
currentUrl: this.currentUrl,
|
|
62
66
|
html: this.html,
|
|
63
67
|
message: this.message,
|
|
64
|
-
recommendedPlugins: this.recommendedPlugins
|
|
68
|
+
recommendedPlugins: this.recommendedPlugins,
|
|
69
|
+
forms: this.forms,
|
|
70
|
+
orphanedFields: this.orphanedFields,
|
|
71
|
+
totalFieldCount: this.totalFieldCount
|
|
65
72
|
};
|
|
66
73
|
}
|
|
67
74
|
|
|
@@ -91,7 +98,8 @@ export const CLICK_ELEMENT_TOOL = {
|
|
|
91
98
|
waitForElementTimeout: { type: "number", description: "Maximum time to wait for element in milliseconds", default: 1000 },
|
|
92
99
|
returnHtml: { type: "boolean", description: "Whether to wait for stability and return HTML after clicking. Set to false for fast form interactions (checkboxes, radio buttons).", default: true },
|
|
93
100
|
removeUnnecessaryHTML: { type: "boolean", description: "Remove Unnecessary HTML for size reduction by 90%. Only used when returnHtml is true.", default: true },
|
|
94
|
-
postClickWait: { type: "number", description: "Milliseconds to wait after click for SPAs to render dynamic content.", default: 1000 }
|
|
101
|
+
postClickWait: { type: "number", description: "Milliseconds to wait after click for SPAs to render dynamic content.", default: 1000 },
|
|
102
|
+
detectForms: { type: "boolean", description: "Scan page for forms after click and return structured form data (fields, selectors, submit buttons, orphaned inputs). Only applies when returnHtml=true. Set to true when you need to fill or interact with forms after clicking.", default: false }
|
|
95
103
|
},
|
|
96
104
|
required: ["url"],
|
|
97
105
|
additionalProperties: false,
|
|
@@ -135,6 +143,9 @@ export const CLICK_ELEMENT_TOOL = {
|
|
|
135
143
|
type: ["string", "null"],
|
|
136
144
|
description: "Page HTML if returnHtml was true, null otherwise"
|
|
137
145
|
},
|
|
146
|
+
forms: { type: "array", items: { type: "object" }, description: "Detected forms with fields, selectors, and metadata (when returnHtml is true)" },
|
|
147
|
+
orphanedFields: { type: "array", items: { type: "object" }, description: "Input/select/textarea elements not inside any <form> (when returnHtml is true)" },
|
|
148
|
+
totalFieldCount: { type: "number", description: "Total number of form fields found on the page" },
|
|
138
149
|
nextSteps: {
|
|
139
150
|
type: "array",
|
|
140
151
|
items: { type: "string" },
|
|
@@ -181,7 +192,7 @@ export const CLICK_ELEMENT_TOOL = {
|
|
|
181
192
|
* returnHtml: false
|
|
182
193
|
* });
|
|
183
194
|
*/
|
|
184
|
-
export async function clickElement({ url, selector, text, waitForElementTimeout = 30000, returnHtml = true, removeUnnecessaryHTML = true, postClickWait = 1000 }) {
|
|
195
|
+
export async function clickElement({ url, selector, text, waitForElementTimeout = 30000, returnHtml = true, removeUnnecessaryHTML = true, postClickWait = 1000, detectForms = false }) {
|
|
185
196
|
logger.info(`browser_click_element called: ${selector || `text="${text}"`}`);
|
|
186
197
|
|
|
187
198
|
if (!url) {
|
|
@@ -327,6 +338,17 @@ export async function clickElement({ url, selector, text, waitForElementTimeout
|
|
|
327
338
|
|
|
328
339
|
const currentUrl = page.url();
|
|
329
340
|
const html = finalStatus === 'success' && returnHtml ? await extractAndProcessHtml(page, removeUnnecessaryHTML) : null;
|
|
341
|
+
|
|
342
|
+
// Scan for forms when requested and returning HTML (lightweight, ~50-100ms)
|
|
343
|
+
let formData = null;
|
|
344
|
+
if (detectForms && finalStatus === 'success' && returnHtml) {
|
|
345
|
+
try {
|
|
346
|
+
formData = await scanPageForms(page);
|
|
347
|
+
} catch (err) {
|
|
348
|
+
logger.debug(`Form scan failed (non-fatal): ${err.message}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
330
352
|
const baseMessage = selector ? `Clicked element: ${selector}` : `Clicked element with text: "${text}"`;
|
|
331
353
|
const message = finalStatus === 'success'
|
|
332
354
|
? baseMessage
|
|
@@ -362,7 +384,8 @@ export async function clickElement({ url, selector, text, waitForElementTimeout
|
|
|
362
384
|
html,
|
|
363
385
|
message,
|
|
364
386
|
nextSteps,
|
|
365
|
-
recommendedPlugins: html ? getRecommendedPlugins(currentUrl, html) : []
|
|
387
|
+
recommendedPlugins: html ? getRecommendedPlugins(currentUrl, html) : [],
|
|
388
|
+
formData
|
|
366
389
|
});
|
|
367
390
|
} catch (err) {
|
|
368
391
|
logger.error(`browser_click_element failed: ${err.message}`);
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* detect-forms.js - Auto Form Discovery
|
|
3
|
+
* Scans the current page and returns structured JSON of all forms,
|
|
4
|
+
* their fields, submit buttons, and orphaned inputs (common in SPAs).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getBrowser, getValidatedPage } from '../core/browser.js';
|
|
8
|
+
import { MCPResponse, InformationalResponse } from '../core/responses.js';
|
|
9
|
+
import logger from '../core/logger.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {import('@modelcontextprotocol/sdk/types.js').Tool} Tool
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// RESPONSE CLASS
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Response for successful detect_forms operations
|
|
21
|
+
*/
|
|
22
|
+
export class DetectFormsResponse extends MCPResponse {
|
|
23
|
+
/**
|
|
24
|
+
* @param {Object} params
|
|
25
|
+
* @param {Array} params.forms - Array of form objects
|
|
26
|
+
* @param {Array} params.orphanedFields - Fields not inside any <form>
|
|
27
|
+
* @param {number} params.totalFieldCount - Total number of fields found
|
|
28
|
+
* @param {string} params.summary - Human-readable summary
|
|
29
|
+
* @param {string[]} params.nextSteps - Suggested next actions
|
|
30
|
+
*/
|
|
31
|
+
constructor({ forms, orphanedFields, totalFieldCount, summary, nextSteps = [] }) {
|
|
32
|
+
super(nextSteps);
|
|
33
|
+
|
|
34
|
+
if (!Array.isArray(forms)) {
|
|
35
|
+
throw new TypeError('forms must be an array');
|
|
36
|
+
}
|
|
37
|
+
if (!Array.isArray(orphanedFields)) {
|
|
38
|
+
throw new TypeError('orphanedFields must be an array');
|
|
39
|
+
}
|
|
40
|
+
if (typeof totalFieldCount !== 'number') {
|
|
41
|
+
throw new TypeError('totalFieldCount must be a number');
|
|
42
|
+
}
|
|
43
|
+
if (typeof summary !== 'string') {
|
|
44
|
+
throw new TypeError('summary must be a string');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.forms = forms;
|
|
48
|
+
this.orphanedFields = orphanedFields;
|
|
49
|
+
this.totalFieldCount = totalFieldCount;
|
|
50
|
+
this.summary = summary;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_getAdditionalFields() {
|
|
54
|
+
return {
|
|
55
|
+
forms: this.forms,
|
|
56
|
+
orphanedFields: this.orphanedFields,
|
|
57
|
+
totalFieldCount: this.totalFieldCount,
|
|
58
|
+
summary: this.summary
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
getTextSummary() {
|
|
63
|
+
return this.summary;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// TOOL DEFINITION
|
|
69
|
+
// ============================================================================
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @type {Tool}
|
|
73
|
+
*/
|
|
74
|
+
export const DETECT_FORMS_TOOL = {
|
|
75
|
+
name: "browser_detect_forms",
|
|
76
|
+
title: "Detect Forms",
|
|
77
|
+
description: "**AUTO FORM DISCOVERY** - Scans the current page and returns structured JSON of all forms: fields (name, type, required, placeholder, current value, validation constraints), submit buttons, and orphaned inputs not inside any <form> (common in SPAs). Use this BEFORE filling forms to understand what fields exist and how to interact with them.\n\n**PREREQUISITE**: Page MUST be loaded with browser_fetch_webpage first.",
|
|
78
|
+
inputSchema: {
|
|
79
|
+
type: "object",
|
|
80
|
+
properties: {
|
|
81
|
+
url: { type: "string", description: "URL of the already-loaded page" },
|
|
82
|
+
includeHidden: { type: "boolean", default: false, description: "Include hidden fields (type=hidden). Useful for understanding form state." }
|
|
83
|
+
},
|
|
84
|
+
required: ["url"],
|
|
85
|
+
additionalProperties: false
|
|
86
|
+
},
|
|
87
|
+
outputSchema: {
|
|
88
|
+
type: "object",
|
|
89
|
+
properties: {
|
|
90
|
+
forms: {
|
|
91
|
+
type: "array",
|
|
92
|
+
items: {
|
|
93
|
+
type: "object",
|
|
94
|
+
properties: {
|
|
95
|
+
formSelector: { type: "string" },
|
|
96
|
+
action: { type: "string" },
|
|
97
|
+
method: { type: "string" },
|
|
98
|
+
formType: { type: "string" },
|
|
99
|
+
fields: {
|
|
100
|
+
type: "array",
|
|
101
|
+
items: {
|
|
102
|
+
type: "object",
|
|
103
|
+
properties: {
|
|
104
|
+
selector: { type: "string" },
|
|
105
|
+
name: { type: "string" },
|
|
106
|
+
id: { type: "string" },
|
|
107
|
+
tag: { type: "string" },
|
|
108
|
+
type: { type: "string" },
|
|
109
|
+
required: { type: "boolean" },
|
|
110
|
+
placeholder: { type: "string" },
|
|
111
|
+
currentValue: { type: "string" },
|
|
112
|
+
label: { type: "string" },
|
|
113
|
+
validation: {
|
|
114
|
+
type: "object",
|
|
115
|
+
properties: {
|
|
116
|
+
min: { type: "string" },
|
|
117
|
+
max: { type: "string" },
|
|
118
|
+
pattern: { type: "string" },
|
|
119
|
+
maxLength: { type: "number" }
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
submitButton: {
|
|
126
|
+
type: ["object", "null"],
|
|
127
|
+
properties: {
|
|
128
|
+
selector: { type: "string" },
|
|
129
|
+
text: { type: "string" },
|
|
130
|
+
type: { type: "string" }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
description: "Array of detected forms with fields and metadata"
|
|
136
|
+
},
|
|
137
|
+
orphanedFields: {
|
|
138
|
+
type: "array",
|
|
139
|
+
items: { type: "object" },
|
|
140
|
+
description: "Input/select/textarea elements not inside any <form>"
|
|
141
|
+
},
|
|
142
|
+
totalFieldCount: { type: "number", description: "Total number of fields found" },
|
|
143
|
+
summary: { type: "string", description: "Human-readable summary of detected forms" },
|
|
144
|
+
nextSteps: {
|
|
145
|
+
type: "array",
|
|
146
|
+
items: { type: "string" },
|
|
147
|
+
description: "Suggested next actions"
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
required: ["forms", "orphanedFields", "totalFieldCount", "summary", "nextSteps"],
|
|
151
|
+
additionalProperties: false
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// ============================================================================
|
|
156
|
+
// PAGE EVALUATION FUNCTION
|
|
157
|
+
// ============================================================================
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Runs inside the browser context via page.evaluate().
|
|
161
|
+
* Scans all forms and orphaned fields, resolves labels, classifies form types.
|
|
162
|
+
* @param {boolean} includeHidden - Whether to include hidden fields
|
|
163
|
+
* @returns {Object} Raw form data
|
|
164
|
+
*/
|
|
165
|
+
function buildScanFunction() {
|
|
166
|
+
return (includeHidden) => {
|
|
167
|
+
/**
|
|
168
|
+
* Build a CSS selector for an element
|
|
169
|
+
*/
|
|
170
|
+
function buildSelector(el) {
|
|
171
|
+
if (el.id) return `#${CSS.escape(el.id)}`;
|
|
172
|
+
if (el.name) {
|
|
173
|
+
const tag = el.tagName.toLowerCase();
|
|
174
|
+
const sel = `${tag}[name="${CSS.escape(el.name)}"]`;
|
|
175
|
+
if (document.querySelectorAll(sel).length === 1) return sel;
|
|
176
|
+
}
|
|
177
|
+
// Fallback: nth-of-type relative to parent
|
|
178
|
+
const parent = el.parentElement;
|
|
179
|
+
if (!parent) return el.tagName.toLowerCase();
|
|
180
|
+
const tag = el.tagName.toLowerCase();
|
|
181
|
+
const siblings = Array.from(parent.children).filter(c => c.tagName === el.tagName);
|
|
182
|
+
if (siblings.length === 1) return `${buildSelector(parent)} > ${tag}`;
|
|
183
|
+
const idx = siblings.indexOf(el) + 1;
|
|
184
|
+
return `${buildSelector(parent)} > ${tag}:nth-of-type(${idx})`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Resolve label text for a field element
|
|
189
|
+
* Priority: <label for> → parent <label> → aria-label → aria-labelledby → placeholder
|
|
190
|
+
*/
|
|
191
|
+
function resolveLabel(el) {
|
|
192
|
+
// 1. Explicit <label for="id">
|
|
193
|
+
if (el.id) {
|
|
194
|
+
const label = document.querySelector(`label[for="${CSS.escape(el.id)}"]`);
|
|
195
|
+
if (label) return label.textContent.trim();
|
|
196
|
+
}
|
|
197
|
+
// 2. Parent <label> wrapping the input
|
|
198
|
+
const parentLabel = el.closest('label');
|
|
199
|
+
if (parentLabel) {
|
|
200
|
+
// Get text content excluding the input itself
|
|
201
|
+
const clone = parentLabel.cloneNode(true);
|
|
202
|
+
clone.querySelectorAll('input, select, textarea').forEach(c => c.remove());
|
|
203
|
+
const text = clone.textContent.trim();
|
|
204
|
+
if (text) return text;
|
|
205
|
+
}
|
|
206
|
+
// 3. aria-label
|
|
207
|
+
const ariaLabel = el.getAttribute('aria-label');
|
|
208
|
+
if (ariaLabel) return ariaLabel.trim();
|
|
209
|
+
// 4. aria-labelledby
|
|
210
|
+
const ariaLabelledBy = el.getAttribute('aria-labelledby');
|
|
211
|
+
if (ariaLabelledBy) {
|
|
212
|
+
const refEl = document.getElementById(ariaLabelledBy);
|
|
213
|
+
if (refEl) return refEl.textContent.trim();
|
|
214
|
+
}
|
|
215
|
+
// 5. placeholder
|
|
216
|
+
const placeholder = el.getAttribute('placeholder');
|
|
217
|
+
if (placeholder) return placeholder.trim();
|
|
218
|
+
return '';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Extract field info from an input/select/textarea element
|
|
223
|
+
*/
|
|
224
|
+
function extractField(el) {
|
|
225
|
+
const tag = el.tagName.toLowerCase();
|
|
226
|
+
const type = el.getAttribute('type') || (tag === 'select' ? 'select' : tag === 'textarea' ? 'textarea' : 'text');
|
|
227
|
+
return {
|
|
228
|
+
selector: buildSelector(el),
|
|
229
|
+
name: el.name || '',
|
|
230
|
+
id: el.id || '',
|
|
231
|
+
tag,
|
|
232
|
+
type,
|
|
233
|
+
required: el.required || el.getAttribute('aria-required') === 'true',
|
|
234
|
+
placeholder: el.getAttribute('placeholder') || '',
|
|
235
|
+
currentValue: el.value || '',
|
|
236
|
+
label: resolveLabel(el),
|
|
237
|
+
validation: {
|
|
238
|
+
min: el.getAttribute('min') || '',
|
|
239
|
+
max: el.getAttribute('max') || '',
|
|
240
|
+
pattern: el.getAttribute('pattern') || '',
|
|
241
|
+
maxLength: el.maxLength >= 0 ? el.maxLength : null
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Find the submit button for a form
|
|
248
|
+
*/
|
|
249
|
+
function findSubmitButton(form) {
|
|
250
|
+
// Explicit submit button
|
|
251
|
+
const submit = form.querySelector('button[type="submit"], input[type="submit"]');
|
|
252
|
+
if (submit) {
|
|
253
|
+
return {
|
|
254
|
+
selector: buildSelector(submit),
|
|
255
|
+
text: (submit.textContent || submit.value || '').trim(),
|
|
256
|
+
type: submit.getAttribute('type') || 'submit'
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
// Fallback: first <button> without type (default is submit)
|
|
260
|
+
const btn = form.querySelector('button:not([type])');
|
|
261
|
+
if (btn) {
|
|
262
|
+
return {
|
|
263
|
+
selector: buildSelector(btn),
|
|
264
|
+
text: btn.textContent.trim(),
|
|
265
|
+
type: 'submit'
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Classify form type via heuristics
|
|
273
|
+
*/
|
|
274
|
+
function classifyForm(fields, form) {
|
|
275
|
+
const visibleFields = fields.filter(f => f.type !== 'hidden');
|
|
276
|
+
const hasPassword = visibleFields.some(f => f.type === 'password');
|
|
277
|
+
const hasEmail = visibleFields.some(f => f.type === 'email' || f.name.includes('email') || f.id.includes('email'));
|
|
278
|
+
const hasTextarea = visibleFields.some(f => f.tag === 'textarea');
|
|
279
|
+
const hasSearch = visibleFields.some(f => f.type === 'search');
|
|
280
|
+
|
|
281
|
+
// Check for credit card patterns
|
|
282
|
+
const cardPatterns = /card|cc[-_]?num|cvv|cvc|expir|ccv/i;
|
|
283
|
+
const hasCardFields = visibleFields.some(f =>
|
|
284
|
+
cardPatterns.test(f.name) || cardPatterns.test(f.id) || cardPatterns.test(f.label)
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
if (hasCardFields) return 'checkout';
|
|
288
|
+
if (hasPassword && visibleFields.length <= 3) return 'login';
|
|
289
|
+
if (hasPassword && hasEmail && visibleFields.length > 3) return 'registration';
|
|
290
|
+
if (hasSearch) return 'search';
|
|
291
|
+
if (hasTextarea && hasEmail && !hasPassword) return 'contact';
|
|
292
|
+
|
|
293
|
+
// Check form action/class for search hints
|
|
294
|
+
const formAction = (form.getAttribute('action') || '').toLowerCase();
|
|
295
|
+
const formClass = (form.getAttribute('class') || '').toLowerCase();
|
|
296
|
+
const formRole = (form.getAttribute('role') || '').toLowerCase();
|
|
297
|
+
if (formRole === 'search' || formAction.includes('search') || formClass.includes('search')) return 'search';
|
|
298
|
+
|
|
299
|
+
// Single text input with submit = likely search
|
|
300
|
+
if (visibleFields.length === 1 && (visibleFields[0].type === 'text' || visibleFields[0].type === 'search')) return 'search';
|
|
301
|
+
|
|
302
|
+
return 'other';
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const fieldSelector = 'input, select, textarea';
|
|
306
|
+
const forms = [];
|
|
307
|
+
|
|
308
|
+
// Process each <form> element
|
|
309
|
+
document.querySelectorAll('form').forEach((form, index) => {
|
|
310
|
+
const fieldElements = Array.from(form.querySelectorAll(fieldSelector));
|
|
311
|
+
let fields = fieldElements.map(extractField);
|
|
312
|
+
|
|
313
|
+
// Filter hidden fields unless includeHidden
|
|
314
|
+
if (!includeHidden) {
|
|
315
|
+
fields = fields.filter(f => f.type !== 'hidden');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
forms.push({
|
|
319
|
+
formSelector: buildSelector(form),
|
|
320
|
+
action: form.getAttribute('action') || '',
|
|
321
|
+
method: (form.getAttribute('method') || 'GET').toUpperCase(),
|
|
322
|
+
formType: classifyForm(fields, form),
|
|
323
|
+
fields,
|
|
324
|
+
submitButton: findSubmitButton(form)
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// Collect orphaned fields (not inside any <form>)
|
|
329
|
+
const allFields = Array.from(document.querySelectorAll(fieldSelector));
|
|
330
|
+
let orphanedFields = allFields
|
|
331
|
+
.filter(el => !el.closest('form'))
|
|
332
|
+
.map(extractField);
|
|
333
|
+
|
|
334
|
+
if (!includeHidden) {
|
|
335
|
+
orphanedFields = orphanedFields.filter(f => f.type !== 'hidden');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const totalFieldCount = forms.reduce((sum, f) => sum + f.fields.length, 0) + orphanedFields.length;
|
|
339
|
+
|
|
340
|
+
return { forms, orphanedFields, totalFieldCount };
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ============================================================================
|
|
345
|
+
// SHARED SCAN FUNCTION (used by fetch-page, get-current-html, click-element)
|
|
346
|
+
// ============================================================================
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Scan a page for forms and return structured data.
|
|
350
|
+
* Lightweight (~50-100ms) - safe to call on every page load.
|
|
351
|
+
* @param {Object} page - Puppeteer page object
|
|
352
|
+
* @param {boolean} [includeHidden=false] - Whether to include hidden fields
|
|
353
|
+
* @returns {Promise<{forms: Array, orphanedFields: Array, totalFieldCount: number}>}
|
|
354
|
+
*/
|
|
355
|
+
export async function scanPageForms(page, includeHidden = false) {
|
|
356
|
+
const scanFn = buildScanFunction();
|
|
357
|
+
return await page.evaluate(scanFn, includeHidden);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ============================================================================
|
|
361
|
+
// ACTION FUNCTION
|
|
362
|
+
// ============================================================================
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Detect all forms on the current page
|
|
366
|
+
* @param {Object} params - Parameters
|
|
367
|
+
* @param {string} params.url - The URL of the page to scan
|
|
368
|
+
* @param {boolean} [params.includeHidden=false] - Whether to include hidden fields
|
|
369
|
+
* @returns {Promise<DetectFormsResponse|InformationalResponse>}
|
|
370
|
+
*/
|
|
371
|
+
export async function detectForms({ url, includeHidden = false }) {
|
|
372
|
+
logger.info(`browser_detect_forms called: url=${url}, includeHidden=${includeHidden}`);
|
|
373
|
+
|
|
374
|
+
if (!url) {
|
|
375
|
+
throw new Error("url parameter is required");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
let hostname;
|
|
379
|
+
try {
|
|
380
|
+
hostname = new URL(url).hostname;
|
|
381
|
+
} catch {
|
|
382
|
+
throw new Error(`Invalid URL: ${url}`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Ensure browser connection
|
|
386
|
+
try {
|
|
387
|
+
await getBrowser();
|
|
388
|
+
} catch (err) {
|
|
389
|
+
logger.error(`browser_detect_forms: Failed to connect to browser: ${err.message}`);
|
|
390
|
+
return new InformationalResponse(
|
|
391
|
+
`Browser connection failed: ${err.message}`,
|
|
392
|
+
'The browser must be running with remote debugging enabled.',
|
|
393
|
+
[
|
|
394
|
+
'Ensure the browser is installed and running',
|
|
395
|
+
'Check that remote debugging is enabled (--remote-debugging-port)',
|
|
396
|
+
'Try restarting the MCP server'
|
|
397
|
+
]
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Validate page exists and is usable
|
|
402
|
+
const { page, error: pageError } = await getValidatedPage(hostname);
|
|
403
|
+
|
|
404
|
+
if (!page) {
|
|
405
|
+
const isConnectionLost = pageError && pageError.includes('connection');
|
|
406
|
+
logger.debug(`browser_detect_forms: ${pageError || 'No page found for ' + hostname}`);
|
|
407
|
+
return new InformationalResponse(
|
|
408
|
+
isConnectionLost ? `Page connection lost for ${hostname}` : `No open page found for ${hostname}`,
|
|
409
|
+
isConnectionLost
|
|
410
|
+
? 'The browser tab was closed or the connection was lost. The page needs to be reloaded.'
|
|
411
|
+
: 'The page must be loaded before you can detect forms',
|
|
412
|
+
[
|
|
413
|
+
"Use MCPBrowser's browser_fetch_webpage tool to load the page first",
|
|
414
|
+
"Then retry MCPBrowser's browser_detect_forms with the same URL"
|
|
415
|
+
]
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
const raw = await scanPageForms(page, includeHidden);
|
|
421
|
+
|
|
422
|
+
// Build summary
|
|
423
|
+
const summary = buildSummary(raw.forms, raw.orphanedFields, raw.totalFieldCount);
|
|
424
|
+
|
|
425
|
+
logger.info(`browser_detect_forms completed: ${summary}`);
|
|
426
|
+
|
|
427
|
+
// Build next steps based on discovered forms
|
|
428
|
+
const nextSteps = buildNextSteps(raw.forms, raw.orphanedFields);
|
|
429
|
+
|
|
430
|
+
return new DetectFormsResponse({
|
|
431
|
+
forms: raw.forms,
|
|
432
|
+
orphanedFields: raw.orphanedFields,
|
|
433
|
+
totalFieldCount: raw.totalFieldCount,
|
|
434
|
+
summary,
|
|
435
|
+
nextSteps
|
|
436
|
+
});
|
|
437
|
+
} catch (err) {
|
|
438
|
+
logger.error(`browser_detect_forms failed: ${err.message}`);
|
|
439
|
+
return new InformationalResponse(
|
|
440
|
+
`Failed to detect forms: ${err.message}`,
|
|
441
|
+
'Could not scan the page for forms. The page may have navigated away or the connection was lost.',
|
|
442
|
+
[
|
|
443
|
+
"Try MCPBrowser's browser_fetch_webpage to reload the page",
|
|
444
|
+
"Use MCPBrowser's browser_close_tab and start fresh if needed"
|
|
445
|
+
]
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ============================================================================
|
|
451
|
+
// HELPERS
|
|
452
|
+
// ============================================================================
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Build a human-readable summary of detected forms
|
|
456
|
+
*/
|
|
457
|
+
function buildSummary(forms, orphanedFields, totalFieldCount) {
|
|
458
|
+
if (forms.length === 0 && orphanedFields.length === 0) {
|
|
459
|
+
return 'No forms or input fields found on this page';
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const parts = [];
|
|
463
|
+
if (forms.length > 0) {
|
|
464
|
+
const formDescriptions = forms.map(f => {
|
|
465
|
+
const fieldCount = f.fields.length;
|
|
466
|
+
return `1 ${f.formType} form (${fieldCount} field${fieldCount !== 1 ? 's' : ''})`;
|
|
467
|
+
});
|
|
468
|
+
parts.push(formDescriptions.join(', '));
|
|
469
|
+
}
|
|
470
|
+
if (orphanedFields.length > 0) {
|
|
471
|
+
parts.push(`${orphanedFields.length} orphaned field${orphanedFields.length !== 1 ? 's' : ''} (not in any form)`);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return `Found ${forms.length} form${forms.length !== 1 ? 's' : ''}: ${parts.join('; ')}. Total fields: ${totalFieldCount}`;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Build contextual next steps based on what was found
|
|
479
|
+
*/
|
|
480
|
+
function buildNextSteps(forms, orphanedFields) {
|
|
481
|
+
const steps = [];
|
|
482
|
+
|
|
483
|
+
if (forms.length > 0) {
|
|
484
|
+
const primaryForm = forms[0];
|
|
485
|
+
if (primaryForm.fields.length > 0) {
|
|
486
|
+
const firstField = primaryForm.fields[0];
|
|
487
|
+
steps.push(`Use MCPBrowser's browser_type_text to fill form fields (e.g., selector: '${firstField.selector}')`);
|
|
488
|
+
}
|
|
489
|
+
if (primaryForm.submitButton) {
|
|
490
|
+
steps.push(`Use MCPBrowser's browser_click_element to submit the form (selector: '${primaryForm.submitButton.selector}')`);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (orphanedFields.length > 0) {
|
|
495
|
+
steps.push("Use MCPBrowser's browser_type_text for orphaned fields (SPA inputs not inside a <form>)");
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
steps.push("Use MCPBrowser's browser_take_screenshot if form layout is unclear from the data");
|
|
499
|
+
steps.push("Use MCPBrowser's browser_get_current_html to see full page HTML");
|
|
500
|
+
|
|
501
|
+
return steps;
|
|
502
|
+
}
|
|
@@ -9,6 +9,7 @@ import { isLikelyAuthUrl, waitForAuth } from '../core/auth.js';
|
|
|
9
9
|
import { MCPResponse, ErrorResponse, HttpStatusResponse, InformationalResponse } from '../core/responses.js';
|
|
10
10
|
import logger from '../core/logger.js';
|
|
11
11
|
import { getPluginNextSteps, getRecommendedPlugins } from '../core/plugin-loader.js';
|
|
12
|
+
import { scanPageForms } from './detect-forms.js';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* @typedef {import('@modelcontextprotocol/sdk/types.js').Tool} Tool
|
|
@@ -27,8 +28,9 @@ export class FetchPageSuccessResponse extends MCPResponse {
|
|
|
27
28
|
* @param {string} html - Page HTML content
|
|
28
29
|
* @param {string[]} nextSteps - Suggested next actions
|
|
29
30
|
* @param {Array} [recommendedPlugins] - Detected plugin metadata
|
|
31
|
+
* @param {Object} [formData] - Detected forms data
|
|
30
32
|
*/
|
|
31
|
-
constructor(currentUrl, html, nextSteps, recommendedPlugins = []) {
|
|
33
|
+
constructor(currentUrl, html, nextSteps, recommendedPlugins = [], formData = null) {
|
|
32
34
|
super(nextSteps);
|
|
33
35
|
|
|
34
36
|
if (typeof currentUrl !== 'string') {
|
|
@@ -41,13 +43,19 @@ export class FetchPageSuccessResponse extends MCPResponse {
|
|
|
41
43
|
this.currentUrl = currentUrl;
|
|
42
44
|
this.html = html;
|
|
43
45
|
this.recommendedPlugins = recommendedPlugins;
|
|
46
|
+
this.forms = formData?.forms || [];
|
|
47
|
+
this.orphanedFields = formData?.orphanedFields || [];
|
|
48
|
+
this.totalFieldCount = formData?.totalFieldCount || 0;
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
_getAdditionalFields() {
|
|
47
52
|
return {
|
|
48
53
|
currentUrl: this.currentUrl,
|
|
49
54
|
html: this.html,
|
|
50
|
-
recommendedPlugins: this.recommendedPlugins
|
|
55
|
+
recommendedPlugins: this.recommendedPlugins,
|
|
56
|
+
forms: this.forms,
|
|
57
|
+
orphanedFields: this.orphanedFields,
|
|
58
|
+
totalFieldCount: this.totalFieldCount
|
|
51
59
|
};
|
|
52
60
|
}
|
|
53
61
|
|
|
@@ -78,7 +86,8 @@ export const FETCH_WEBPAGE_TOOL = {
|
|
|
78
86
|
},
|
|
79
87
|
removeUnnecessaryHTML: { type: "boolean", description: "Remove Unnecessary HTML for size reduction by 90%.", default: true },
|
|
80
88
|
selector: { type: "string", description: "CSS selector to extract a specific DOM subtree instead of the full page. Use to scope extraction and reduce response size (e.g., 'main', '[role=\"main\"]', 'body > div:first-child'). If no elements match, falls back to full page with a note." },
|
|
81
|
-
postLoadWait: { type: "number", description: "Additional milliseconds to wait after page load before extracting HTML. Use for pages that need extra time to render. Default: 0 (no extra wait, SPA detection handles most cases automatically).", default: 0 }
|
|
89
|
+
postLoadWait: { type: "number", description: "Additional milliseconds to wait after page load before extracting HTML. Use for pages that need extra time to render. Default: 0 (no extra wait, SPA detection handles most cases automatically).", default: 0 },
|
|
90
|
+
detectForms: { type: "boolean", description: "Scan page for forms and return structured form data (fields, selectors, submit buttons, orphaned inputs). Set to true when you need to fill or interact with forms.", default: false }
|
|
82
91
|
},
|
|
83
92
|
required: ["url"],
|
|
84
93
|
additionalProperties: false
|
|
@@ -88,6 +97,9 @@ export const FETCH_WEBPAGE_TOOL = {
|
|
|
88
97
|
properties: {
|
|
89
98
|
currentUrl: { type: "string", description: "Final URL after any redirects" },
|
|
90
99
|
html: { type: "string", description: "Page HTML content" },
|
|
100
|
+
forms: { type: "array", items: { type: "object" }, description: "Detected forms with fields, selectors, and metadata" },
|
|
101
|
+
orphanedFields: { type: "array", items: { type: "object" }, description: "Input/select/textarea elements not inside any <form>" },
|
|
102
|
+
totalFieldCount: { type: "number", description: "Total number of form fields found on the page" },
|
|
91
103
|
nextSteps: {
|
|
92
104
|
type: "array",
|
|
93
105
|
items: { type: "string" },
|
|
@@ -123,7 +135,7 @@ export const FETCH_WEBPAGE_TOOL = {
|
|
|
123
135
|
* @param {number} [params.postLoadWait=0] - Additional milliseconds to wait after page load before extracting HTML
|
|
124
136
|
* @returns {Promise<Object>} Result object with success status, URL, HTML content, or error details
|
|
125
137
|
*/
|
|
126
|
-
export async function fetchPage({ url, browser = '', removeUnnecessaryHTML = true, selector = null, postLoadWait = 0 }) {
|
|
138
|
+
export async function fetchPage({ url, browser = '', removeUnnecessaryHTML = true, selector = null, postLoadWait = 0, detectForms = false }) {
|
|
127
139
|
logger.info(`browser_fetch_webpage called: url=${url}`);
|
|
128
140
|
|
|
129
141
|
// Handle missing URL with environment variable fallback
|
|
@@ -151,7 +163,7 @@ export async function fetchPage({ url, browser = '', removeUnnecessaryHTML = tru
|
|
|
151
163
|
|
|
152
164
|
// Queue this request - processed sequentially, one at a time
|
|
153
165
|
return queueRequest(async () => {
|
|
154
|
-
return await doFetchPage({ url, browser, removeUnnecessaryHTML, selector, postLoadWait });
|
|
166
|
+
return await doFetchPage({ url, browser, removeUnnecessaryHTML, selector, postLoadWait, detectForms });
|
|
155
167
|
});
|
|
156
168
|
}
|
|
157
169
|
|
|
@@ -159,7 +171,7 @@ export async function fetchPage({ url, browser = '', removeUnnecessaryHTML = tru
|
|
|
159
171
|
* Internal function that does the actual page fetching.
|
|
160
172
|
* Called by the queue processor - only one runs at a time.
|
|
161
173
|
*/
|
|
162
|
-
async function doFetchPage({ url, browser, removeUnnecessaryHTML, selector, postLoadWait }) {
|
|
174
|
+
async function doFetchPage({ url, browser, removeUnnecessaryHTML, selector, postLoadWait, detectForms }) {
|
|
163
175
|
const originalHostname = new URL(url).hostname;
|
|
164
176
|
|
|
165
177
|
// Ensure browser connection
|
|
@@ -218,6 +230,16 @@ async function doFetchPage({ url, browser, removeUnnecessaryHTML, selector, post
|
|
|
218
230
|
// Extract and process HTML
|
|
219
231
|
const processedHtml = await extractAndProcessHtml(page, removeUnnecessaryHTML, selector);
|
|
220
232
|
|
|
233
|
+
// Scan for forms when requested (lightweight, ~50-100ms)
|
|
234
|
+
let formData = null;
|
|
235
|
+
if (detectForms) {
|
|
236
|
+
try {
|
|
237
|
+
formData = await scanPageForms(page);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
logger.debug(`Form scan failed (non-fatal): ${err.message}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
221
243
|
logger.info(`browser_fetch_webpage completed: ${page.url()}`);
|
|
222
244
|
|
|
223
245
|
// Check for non-2xx HTTP status codes
|
|
@@ -237,7 +259,8 @@ async function doFetchPage({ url, browser, removeUnnecessaryHTML, selector, post
|
|
|
237
259
|
"Use MCPBrowser's browser_take_screenshot if page has charts, images, or complex visual layout that's hard to understand from HTML",
|
|
238
260
|
"Use MCPBrowser's browser_close_tab when finished to free browser resources"
|
|
239
261
|
],
|
|
240
|
-
getRecommendedPlugins(page.url(), processedHtml)
|
|
262
|
+
getRecommendedPlugins(page.url(), processedHtml),
|
|
263
|
+
formData
|
|
241
264
|
);
|
|
242
265
|
} catch (err) {
|
|
243
266
|
logger.error(`browser_fetch_webpage failed: ${err.message || String(err)}`);
|
|
@@ -7,6 +7,7 @@ import { extractAndProcessHtml } from '../core/page.js';
|
|
|
7
7
|
import { MCPResponse, InformationalResponse } from '../core/responses.js';
|
|
8
8
|
import logger from '../core/logger.js';
|
|
9
9
|
import { getPluginNextSteps, getRecommendedPlugins } from '../core/plugin-loader.js';
|
|
10
|
+
import { scanPageForms } from './detect-forms.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* @typedef {import('@modelcontextprotocol/sdk/types.js').Tool} Tool
|
|
@@ -25,8 +26,9 @@ export class GetCurrentHtmlSuccessResponse extends MCPResponse {
|
|
|
25
26
|
* @param {string} html - Page HTML content
|
|
26
27
|
* @param {string[]} nextSteps - Suggested next actions
|
|
27
28
|
* @param {Array} [recommendedPlugins] - Detected plugin metadata
|
|
29
|
+
* @param {Object} [formData] - Detected forms data
|
|
28
30
|
*/
|
|
29
|
-
constructor(currentUrl, html, nextSteps, recommendedPlugins = []) {
|
|
31
|
+
constructor(currentUrl, html, nextSteps, recommendedPlugins = [], formData = null) {
|
|
30
32
|
super(nextSteps);
|
|
31
33
|
|
|
32
34
|
if (typeof currentUrl !== 'string') {
|
|
@@ -39,13 +41,19 @@ export class GetCurrentHtmlSuccessResponse extends MCPResponse {
|
|
|
39
41
|
this.currentUrl = currentUrl;
|
|
40
42
|
this.html = html;
|
|
41
43
|
this.recommendedPlugins = recommendedPlugins;
|
|
44
|
+
this.forms = formData?.forms || [];
|
|
45
|
+
this.orphanedFields = formData?.orphanedFields || [];
|
|
46
|
+
this.totalFieldCount = formData?.totalFieldCount || 0;
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
_getAdditionalFields() {
|
|
45
50
|
return {
|
|
46
51
|
currentUrl: this.currentUrl,
|
|
47
52
|
html: this.html,
|
|
48
|
-
recommendedPlugins: this.recommendedPlugins
|
|
53
|
+
recommendedPlugins: this.recommendedPlugins,
|
|
54
|
+
forms: this.forms,
|
|
55
|
+
orphanedFields: this.orphanedFields,
|
|
56
|
+
totalFieldCount: this.totalFieldCount
|
|
49
57
|
};
|
|
50
58
|
}
|
|
51
59
|
|
|
@@ -70,7 +78,8 @@ export const GET_CURRENT_HTML_TOOL = {
|
|
|
70
78
|
properties: {
|
|
71
79
|
url: { type: "string", description: "The URL of the page (must match a previously fetched page)" },
|
|
72
80
|
removeUnnecessaryHTML: { type: "boolean", description: "Remove Unnecessary HTML for size reduction by 90%.", default: true },
|
|
73
|
-
selector: { type: "string", description: "CSS selector to extract a specific DOM subtree instead of the full page. Use to scope extraction and reduce response size (e.g., 'main', '[role=\"main\"]', 'body > div:first-child'). If no elements match, falls back to full page with a note." }
|
|
81
|
+
selector: { type: "string", description: "CSS selector to extract a specific DOM subtree instead of the full page. Use to scope extraction and reduce response size (e.g., 'main', '[role=\"main\"]', 'body > div:first-child'). If no elements match, falls back to full page with a note." },
|
|
82
|
+
detectForms: { type: "boolean", description: "Scan page for forms and return structured form data (fields, selectors, submit buttons, orphaned inputs). Set to true when you need to fill or interact with forms.", default: false }
|
|
74
83
|
},
|
|
75
84
|
required: ["url"],
|
|
76
85
|
additionalProperties: false
|
|
@@ -80,6 +89,9 @@ export const GET_CURRENT_HTML_TOOL = {
|
|
|
80
89
|
properties: {
|
|
81
90
|
currentUrl: { type: "string", description: "Current page URL" },
|
|
82
91
|
html: { type: "string", description: "Page HTML content" },
|
|
92
|
+
forms: { type: "array", items: { type: "object" }, description: "Detected forms with fields, selectors, and metadata" },
|
|
93
|
+
orphanedFields: { type: "array", items: { type: "object" }, description: "Input/select/textarea elements not inside any <form>" },
|
|
94
|
+
totalFieldCount: { type: "number", description: "Total number of form fields found on the page" },
|
|
83
95
|
nextSteps: {
|
|
84
96
|
type: "array",
|
|
85
97
|
items: { type: "string" },
|
|
@@ -108,7 +120,7 @@ export const GET_CURRENT_HTML_TOOL = {
|
|
|
108
120
|
* @param {boolean} [params.removeUnnecessaryHTML=true] - Whether to clean HTML
|
|
109
121
|
* @returns {Promise<Object>} Result object with current HTML
|
|
110
122
|
*/
|
|
111
|
-
export async function getCurrentHtml({ url, removeUnnecessaryHTML = true, selector = null }) {
|
|
123
|
+
export async function getCurrentHtml({ url, removeUnnecessaryHTML = true, selector = null, detectForms = false }) {
|
|
112
124
|
const startTime = Date.now();
|
|
113
125
|
logger.info(`browser_get_current_html called: url=${url}${selector ? ` selector=${selector}` : ''}`);
|
|
114
126
|
|
|
@@ -161,6 +173,16 @@ export async function getCurrentHtml({ url, removeUnnecessaryHTML = true, select
|
|
|
161
173
|
const currentUrl = page.url();
|
|
162
174
|
const html = await extractAndProcessHtml(page, removeUnnecessaryHTML, selector);
|
|
163
175
|
|
|
176
|
+
// Scan for forms when requested (lightweight, ~50-100ms)
|
|
177
|
+
let formData = null;
|
|
178
|
+
if (detectForms) {
|
|
179
|
+
try {
|
|
180
|
+
formData = await scanPageForms(page);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
logger.debug(`Form scan failed (non-fatal): ${err.message}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
164
186
|
// Detect empty/near-empty HTML extraction (e.g., CSP blocking page.evaluate)
|
|
165
187
|
if (!html || html.trim().length < 100) {
|
|
166
188
|
logger.warn(`browser_get_current_html: HTML extraction returned empty/minimal content from ${currentUrl} (${html ? html.trim().length : 0} chars)`);
|
|
@@ -188,7 +210,8 @@ export async function getCurrentHtml({ url, removeUnnecessaryHTML = true, select
|
|
|
188
210
|
"Use MCPBrowser's browser_take_screenshot if page layout or visual content is hard to understand from HTML",
|
|
189
211
|
"Use MCPBrowser's browser_close_tab to free resources when done"
|
|
190
212
|
],
|
|
191
|
-
getRecommendedPlugins(currentUrl, html)
|
|
213
|
+
getRecommendedPlugins(currentUrl, html),
|
|
214
|
+
formData
|
|
192
215
|
);
|
|
193
216
|
} catch (err) {
|
|
194
217
|
logger.error(`browser_get_current_html failed: ${err.message}`);
|
package/src/mcp-browser.js
CHANGED
|
@@ -32,6 +32,7 @@ import { takeScreenshot, TAKE_SCREENSHOT_TOOL } from './actions/take-screenshot.
|
|
|
32
32
|
import { scrollPage, SCROLL_PAGE_TOOL } from './actions/scroll-page.js';
|
|
33
33
|
import { executeJavascript, EXECUTE_JAVASCRIPT_TOOL } from './actions/execute-javascript.js';
|
|
34
34
|
import { navigateHistory, NAVIGATE_HISTORY_TOOL } from './actions/navigate-history.js';
|
|
35
|
+
import { detectForms, DETECT_FORMS_TOOL } from './actions/detect-forms.js';
|
|
35
36
|
|
|
36
37
|
// Import plugin dispatch tools
|
|
37
38
|
import { pluginAction, PLUGIN_ACTION_TOOL } from './actions/plugin-action.js';
|
|
@@ -80,6 +81,7 @@ async function main() {
|
|
|
80
81
|
TAKE_SCREENSHOT_TOOL,
|
|
81
82
|
SCROLL_PAGE_TOOL,
|
|
82
83
|
NAVIGATE_HISTORY_TOOL,
|
|
84
|
+
DETECT_FORMS_TOOL,
|
|
83
85
|
PLUGIN_INFO_TOOL,
|
|
84
86
|
PLUGIN_ACTION_TOOL
|
|
85
87
|
];
|
|
@@ -146,6 +148,10 @@ async function main() {
|
|
|
146
148
|
result = await navigateHistory(safeArgs);
|
|
147
149
|
break;
|
|
148
150
|
|
|
151
|
+
case "browser_detect_forms":
|
|
152
|
+
result = await detectForms(safeArgs);
|
|
153
|
+
break;
|
|
154
|
+
|
|
149
155
|
case "browser_plugin_info":
|
|
150
156
|
result = pluginInfo(safeArgs);
|
|
151
157
|
break;
|
|
@@ -213,6 +219,7 @@ export {
|
|
|
213
219
|
takeScreenshot,
|
|
214
220
|
scrollPage,
|
|
215
221
|
navigateHistory,
|
|
222
|
+
detectForms,
|
|
216
223
|
handleAcceptEula,
|
|
217
224
|
// CLI exports
|
|
218
225
|
isCliMode,
|