mcp-web-inspector 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -962,10 +962,10 @@ Drag an element to a target location
962
962
  - targetSelector (string, required): CSS selector for the target location
963
963
 
964
964
  #### `fill`
965
- fill out an input field
965
+ fill an input/textarea/contenteditable; if the selector matches a wrapper, descends up to 4 levels to a unique fillable descendant (errors if zero or multiple)
966
966
 
967
967
  - Parameters:
968
- - selector (string, required): CSS selector for input field
968
+ - selector (string, required): CSS selector for input field or its wrapper
969
969
  - value (string, required): Value to fill
970
970
 
971
971
  #### `hover`
@@ -1,8 +1,6 @@
1
1
  import { BrowserToolBase } from '../base.js';
2
2
  import { ToolContext, ToolResponse, ToolMetadata, SessionConfig } from '../../common/types.js';
3
- /**
4
- * Tool for filling form fields
5
- */
3
+ export declare const BOUNDED_FILLABLE_DESCENDANT_SELECTOR: string;
6
4
  export declare class FillTool extends BrowserToolBase {
7
5
  static getMetadata(sessionConfig?: SessionConfig): ToolMetadata;
8
6
  execute(args: any, context: ToolContext): Promise<ToolResponse>;
@@ -1,18 +1,33 @@
1
1
  import { BrowserToolBase } from '../base.js';
2
2
  import { ANNOTATIONS, createSuccessResponse } from '../../common/types.js';
3
- /**
4
- * Tool for filling form fields
5
- */
3
+ const FILLABLE_BASE_SELECTOR = 'input:not([type="button"]):not([type="submit"]):not([type="reset"])' +
4
+ ':not([type="checkbox"]):not([type="radio"]):not([type="file"])' +
5
+ ':not([type="image"]):not([type="hidden"]),' +
6
+ 'textarea,' +
7
+ '[contenteditable=""],' +
8
+ '[contenteditable="true"]';
9
+ const MAX_DESCENT_DEPTH = 4;
10
+ export const BOUNDED_FILLABLE_DESCENDANT_SELECTOR = (() => {
11
+ const parts = FILLABLE_BASE_SELECTOR.split(',').map((s) => s.trim());
12
+ const groups = [];
13
+ for (let depth = 1; depth <= MAX_DESCENT_DEPTH; depth++) {
14
+ const ancestors = '* > '.repeat(depth - 1);
15
+ for (const p of parts) {
16
+ groups.push(`:scope > ${ancestors}${p}`);
17
+ }
18
+ }
19
+ return groups.join(', ');
20
+ })();
6
21
  export class FillTool extends BrowserToolBase {
7
22
  static getMetadata(sessionConfig) {
8
23
  return {
9
24
  name: "fill",
10
- description: "fill out an input field",
25
+ description: "fill an input/textarea/contenteditable; if the selector matches a wrapper, descends up to 4 levels to a unique fillable descendant (errors if zero or multiple)",
11
26
  annotations: ANNOTATIONS.interaction,
12
27
  inputSchema: {
13
28
  type: "object",
14
29
  properties: {
15
- selector: { type: "string", description: "CSS selector for input field" },
30
+ selector: { type: "string", description: "CSS selector for input field or its wrapper" },
16
31
  value: { type: "string", description: "Value to fill" },
17
32
  },
18
33
  required: ["selector", "value"],
@@ -23,14 +38,97 @@ export class FillTool extends BrowserToolBase {
23
38
  this.recordInteraction();
24
39
  return this.safeExecute(context, async (page) => {
25
40
  const normalizedSelector = this.normalizeSelector(args.selector);
26
- // Use standard element selection with error on multiple matches
27
41
  const locator = page.locator(normalizedSelector);
28
42
  const { element } = await this.selectPreferredLocator(locator, {
29
43
  errorOnMultiple: true,
30
44
  originalSelector: args.selector,
31
45
  });
32
- await element.fill(args.value);
33
- return createSuccessResponse(`Filled ${args.selector} with: ${args.value}`);
46
+ const wrapperInfo = await element.evaluate((el) => {
47
+ const fillableMatch = (node) => {
48
+ if (!node || node.nodeType !== 1)
49
+ return false;
50
+ const tag = (node.tagName || '').toLowerCase();
51
+ if (tag === 'textarea')
52
+ return true;
53
+ if (tag === 'input') {
54
+ const type = (node.getAttribute('type') || '').toLowerCase();
55
+ return !['button', 'submit', 'reset', 'checkbox', 'radio', 'file', 'image', 'hidden'].includes(type);
56
+ }
57
+ const ce = node.getAttribute('contenteditable');
58
+ return ce === '' || ce === 'true';
59
+ };
60
+ return {
61
+ isFillable: fillableMatch(el),
62
+ tag: (el.tagName || '').toLowerCase(),
63
+ };
64
+ });
65
+ if (wrapperInfo.isFillable) {
66
+ await element.fill(args.value);
67
+ return createSuccessResponse(`Filled ${args.selector} with: ${args.value}`);
68
+ }
69
+ const descendants = element.locator(BOUNDED_FILLABLE_DESCENDANT_SELECTOR);
70
+ const count = await descendants.count();
71
+ if (count === 0) {
72
+ throw new Error(`Selector "${args.selector}" matched a <${wrapperInfo.tag}> wrapper with no fillable descendants within ${MAX_DESCENT_DEPTH} levels (input, textarea, contenteditable).`);
73
+ }
74
+ if (count > 1) {
75
+ const candidates = await describeFillableCandidates(descendants, count);
76
+ throw new Error(`Selector "${args.selector}" matched a <${wrapperInfo.tag}> wrapper with ${count} fillable descendants. ` +
77
+ `Use a more specific selector or add data-testid to the input itself.\n\nCandidates:\n${candidates}`);
78
+ }
79
+ const target = descendants.first();
80
+ const targetTag = await target.evaluate((el) => {
81
+ const tag = (el.tagName || '').toLowerCase();
82
+ const type = el.getAttribute?.('type');
83
+ return type ? `<${tag} type="${type}">` : `<${tag}>`;
84
+ });
85
+ await target.fill(args.value);
86
+ return createSuccessResponse(`Filled ${args.selector} with: ${args.value} (descended into ${targetTag})`);
34
87
  });
35
88
  }
36
89
  }
90
+ async function describeFillableCandidates(locator, count) {
91
+ const max = Math.min(count, 5);
92
+ const lines = [];
93
+ for (let i = 0; i < max; i++) {
94
+ const nth = locator.nth(i);
95
+ try {
96
+ const info = await nth.evaluate((el) => {
97
+ const tag = (el.tagName || '').toLowerCase();
98
+ const type = el.getAttribute?.('type') || null;
99
+ const name = el.getAttribute?.('name') || null;
100
+ const id = el.id || null;
101
+ const placeholder = el.getAttribute?.('placeholder') || null;
102
+ const ariaLabel = el.getAttribute?.('aria-label') || null;
103
+ const testid = el.getAttribute?.('data-testid') || el.getAttribute?.('data-test') || el.getAttribute?.('data-cy') || null;
104
+ return { tag, type, name, id, placeholder, ariaLabel, testid };
105
+ });
106
+ const attrs = [];
107
+ if (info.type)
108
+ attrs.push(`type="${info.type}"`);
109
+ if (info.name)
110
+ attrs.push(`name="${info.name}"`);
111
+ if (info.id)
112
+ attrs.push(`id="${info.id}"`);
113
+ if (info.placeholder)
114
+ attrs.push(`placeholder="${truncate(info.placeholder)}"`);
115
+ if (info.ariaLabel)
116
+ attrs.push(`aria-label="${truncate(info.ariaLabel)}"`);
117
+ if (info.testid)
118
+ attrs.push(`data-testid="${info.testid}"`);
119
+ const head = `[${i}] <${info.tag}${attrs.length ? ' ' + attrs.join(' ') : ''}>`;
120
+ const suggestion = info.testid ? `testid:${info.testid}` : info.id ? `id=${info.id}` : null;
121
+ lines.push(suggestion ? `${head}\n selector: ${suggestion}` : head);
122
+ }
123
+ catch {
124
+ lines.push(`[${i}] (element)`);
125
+ }
126
+ }
127
+ if (count > max) {
128
+ lines.push(`… and ${count - max} more.`);
129
+ }
130
+ return lines.join('\n');
131
+ }
132
+ function truncate(s) {
133
+ return s.length > 60 ? `${s.slice(0, 57)}...` : s;
134
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-web-inspector",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Web Inspector MCP: Give LLMs visual superpowers to see, debug, and test any web page.",
5
5
  "license": "MIT",
6
6
  "author": "Anton",