@tontoko/fast-playwright-mcp 0.1.1 → 0.1.3

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.
@@ -0,0 +1,514 @@
1
+ import { createRequire } from "node:module";
2
+ var __create = Object.create;
3
+ var __getProtoOf = Object.getPrototypeOf;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __toESM = (mod, isNodeMode, target) => {
8
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
9
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
10
+ for (let key of __getOwnPropNames(mod))
11
+ if (!__hasOwnProp.call(to, key))
12
+ __defProp(to, key, {
13
+ get: () => mod[key],
14
+ enumerable: true
15
+ });
16
+ return to;
17
+ };
18
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
19
+
20
+ // src/utilities/html-inspector.ts
21
+ import { SelectorResolver } from "../services/selector-resolver.js";
22
+ import {
23
+ generateHTMLInspectionSuggestions,
24
+ HTMLInspectionConstants,
25
+ HTMLInspectionUtils,
26
+ validateHTMLInspectionOptions
27
+ } from "../types/html-inspection.js";
28
+ import { tabDebug } from "../utils/log.js";
29
+
30
+ class HTMLInspector {
31
+ selectorResolver;
32
+ defaultOptions = {
33
+ depth: HTMLInspectionConstants.DEFAULT_DEPTH,
34
+ maxSize: HTMLInspectionConstants.DEFAULT_MAX_SIZE,
35
+ format: "html",
36
+ includeAttributes: true,
37
+ includeStyles: false,
38
+ preserveWhitespace: false
39
+ };
40
+ constructor(page) {
41
+ this.selectorResolver = new SelectorResolver(page);
42
+ }
43
+ async _processSelectorResult(index, selectorResult, selector, options, remainingSize) {
44
+ if (selectorResult.error || selectorResult.confidence === 0) {
45
+ tabDebug(`Selector ${index} failed to resolve: ${selectorResult.error}`);
46
+ return { error: true };
47
+ }
48
+ try {
49
+ const elementResult = await this._extractElementHTML(selectorResult.locator, selector, options, remainingSize);
50
+ return { element: elementResult ?? undefined };
51
+ } catch (error) {
52
+ tabDebug(`Failed to extract HTML for selector ${index}:`, error);
53
+ return { error: true };
54
+ }
55
+ }
56
+ async _processAllSelectors(selectorResults, selectors, options) {
57
+ const elements = {};
58
+ let totalSizeBytes = 0;
59
+ let truncated = false;
60
+ let elementsFound = 0;
61
+ let selectorsNotFound = 0;
62
+ let totalDepth = 0;
63
+ const maxSize = options.maxSize ?? 50000;
64
+ const allResults = await Promise.all(selectorResults.map((selectorResult, i) => this._processSelectorResult(i, selectorResult, selectors[i], options, maxSize).then((result) => ({ index: i, result }))));
65
+ for (const { index, result } of allResults) {
66
+ if (result.error) {
67
+ selectorsNotFound++;
68
+ continue;
69
+ }
70
+ if (result.element && totalSizeBytes + result.element.metadata.sizeBytes <= maxSize) {
71
+ elements[index] = result.element;
72
+ totalSizeBytes += result.element.metadata.sizeBytes;
73
+ elementsFound++;
74
+ totalDepth += this._calculateElementDepth(result.element);
75
+ if (result.element.html.includes("<!-- TRUNCATED -->")) {
76
+ truncated = true;
77
+ }
78
+ } else if (result.element) {
79
+ truncated = true;
80
+ tabDebug("Size limit reached, skipping remaining elements");
81
+ break;
82
+ }
83
+ }
84
+ return {
85
+ elements,
86
+ stats: {
87
+ elementsFound,
88
+ selectorsNotFound,
89
+ totalDepth,
90
+ totalSizeBytes,
91
+ truncated
92
+ }
93
+ };
94
+ }
95
+ async extractHTML(options) {
96
+ const startTime = Date.now();
97
+ const validatedOptions = validateHTMLInspectionOptions(options);
98
+ const mergedOptions = { ...this.defaultOptions, ...validatedOptions };
99
+ tabDebug(`Starting HTML extraction for ${mergedOptions.selectors.length} selectors`);
100
+ try {
101
+ const selectorStartTime = Date.now();
102
+ const selectorResults = await this.selectorResolver.resolveSelectors(mergedOptions.selectors, { continueOnError: true });
103
+ const selectorResolutionMs = Date.now() - selectorStartTime;
104
+ const extractionStartTime = Date.now();
105
+ const { elements, stats } = await this._processAllSelectors(selectorResults, mergedOptions.selectors, mergedOptions);
106
+ const extractionMs = Date.now() - extractionStartTime;
107
+ const totalMs = Date.now() - startTime;
108
+ const result = {
109
+ elements,
110
+ totalSizeBytes: stats.totalSizeBytes,
111
+ truncated: stats.truncated,
112
+ timing: {
113
+ totalMs,
114
+ selectorResolutionMs,
115
+ extractionMs
116
+ },
117
+ stats: {
118
+ elementsFound: stats.elementsFound,
119
+ selectorsNotFound: stats.selectorsNotFound,
120
+ averageDepth: stats.elementsFound > 0 ? stats.totalDepth / stats.elementsFound : 0
121
+ }
122
+ };
123
+ result.suggestions = generateHTMLInspectionSuggestions(result);
124
+ if (result.timing.totalMs > 5000) {
125
+ result.suggestions?.push("Extraction took longer than expected. Consider reducing scope or depth.");
126
+ }
127
+ if (result.totalSizeBytes > HTMLInspectionConstants.SIZE_THRESHOLDS.WARNING) {
128
+ result.suggestions?.push("Large content detected. Consider reducing depth or using more specific selectors.");
129
+ }
130
+ tabDebug(`HTML extraction completed in ${result.timing.totalMs}ms: ${result.stats.elementsFound} elements, ${result.totalSizeBytes} bytes`);
131
+ return result;
132
+ } catch (error) {
133
+ const errorMessage = error instanceof Error ? error.message : String(error);
134
+ tabDebug("HTML extraction failed:", errorMessage);
135
+ return {
136
+ elements: {},
137
+ totalSizeBytes: 0,
138
+ truncated: false,
139
+ suggestions: [`Extraction failed: ${errorMessage}`],
140
+ timing: {
141
+ totalMs: Date.now() - startTime,
142
+ selectorResolutionMs: 0,
143
+ extractionMs: 0
144
+ },
145
+ stats: {
146
+ elementsFound: 0,
147
+ selectorsNotFound: options.selectors.length,
148
+ averageDepth: 0
149
+ }
150
+ };
151
+ }
152
+ }
153
+ suggestCSSSelectors(inspectionResult) {
154
+ const suggestions = [];
155
+ for (const elementResult of Object.values(inspectionResult.elements)) {
156
+ const elementSuggestions = this._generateSelectorsForElement(elementResult);
157
+ suggestions.push(...elementSuggestions);
158
+ }
159
+ return [...suggestions].sort((a, b) => b.confidence - a.confidence).slice(0, 20);
160
+ }
161
+ async optimizeForLLM(inspectionResult) {
162
+ const optimizedElements = {};
163
+ let totalSizeBytes = 0;
164
+ const entries = Object.entries(inspectionResult.elements);
165
+ const optimizedResults = await Promise.all(entries.map(async ([index, element]) => ({
166
+ index: Number.parseInt(index, 10),
167
+ element: await this._optimizeElementForLLM(element)
168
+ })));
169
+ for (const { index, element } of optimizedResults) {
170
+ optimizedElements[index] = element;
171
+ totalSizeBytes += element.metadata.sizeBytes;
172
+ }
173
+ return {
174
+ ...inspectionResult,
175
+ elements: optimizedElements,
176
+ totalSizeBytes
177
+ };
178
+ }
179
+ async _extractElementHTML(locator, selector, options, remainingSizeBytes) {
180
+ try {
181
+ const count = await locator.count();
182
+ if (count === 0) {
183
+ return null;
184
+ }
185
+ const element = locator.first();
186
+ const metadata = await this._extractElementMetadata(element);
187
+ let html;
188
+ switch (options.format) {
189
+ case "text":
190
+ html = await this._extractTextContent(element);
191
+ break;
192
+ case "aria":
193
+ html = await this._extractAriaContent(element);
194
+ break;
195
+ default:
196
+ html = await this._extractHTMLContent(element, options);
197
+ break;
198
+ }
199
+ const sizeBytes = HTMLInspectionUtils.calculateHtmlSize(html);
200
+ if (sizeBytes > remainingSizeBytes && remainingSizeBytes > 0) {
201
+ const truncationResult = HTMLInspectionUtils.truncateHtml(html, remainingSizeBytes);
202
+ html = truncationResult.html;
203
+ metadata.sizeBytes = HTMLInspectionUtils.calculateHtmlSize(html);
204
+ } else {
205
+ metadata.sizeBytes = sizeBytes;
206
+ }
207
+ const result = {
208
+ html,
209
+ metadata,
210
+ matchedSelector: selector
211
+ };
212
+ if (options.depth && options.depth > 1) {
213
+ result.children = await this._extractChildElements(element, options, remainingSizeBytes - metadata.sizeBytes);
214
+ }
215
+ return result;
216
+ } catch (error) {
217
+ const errorMessage = error instanceof Error ? error.message : String(error);
218
+ return {
219
+ html: "",
220
+ metadata: {
221
+ tagName: "unknown",
222
+ attributes: {},
223
+ textContent: "",
224
+ sizeBytes: 0
225
+ },
226
+ matchedSelector: selector,
227
+ error: errorMessage
228
+ };
229
+ }
230
+ }
231
+ async _extractElementMetadata(locator) {
232
+ try {
233
+ const [
234
+ tagName,
235
+ attributes,
236
+ textContent,
237
+ boundingBox,
238
+ computedStyles,
239
+ ariaRole,
240
+ ariaProperties
241
+ ] = await Promise.all([
242
+ locator.evaluate((el) => el.tagName.toLowerCase()),
243
+ locator.evaluate((el) => {
244
+ const attrs = {};
245
+ for (const attr of el.attributes) {
246
+ attrs[attr.name] = attr.value;
247
+ }
248
+ return attrs;
249
+ }),
250
+ locator.textContent() || "",
251
+ locator.boundingBox().catch(() => null),
252
+ this._extractComputedStyles(locator),
253
+ locator.evaluate((el) => el.getAttribute("role")),
254
+ this._extractAriaProperties(locator)
255
+ ]);
256
+ return {
257
+ tagName,
258
+ attributes,
259
+ textContent: textContent || "",
260
+ sizeBytes: 0,
261
+ boundingBox: boundingBox || undefined,
262
+ computedStyles,
263
+ ariaRole: ariaRole || undefined,
264
+ ariaProperties
265
+ };
266
+ } catch (error) {
267
+ tabDebug("Failed to extract element metadata:", error);
268
+ return {
269
+ tagName: "unknown",
270
+ attributes: {},
271
+ textContent: "",
272
+ sizeBytes: 0
273
+ };
274
+ }
275
+ }
276
+ async _extractHTMLContent(locator, options) {
277
+ return await locator.evaluate((el, opts) => {
278
+ const removeExcludedElements = (element, selector) => {
279
+ if (!selector) {
280
+ return;
281
+ }
282
+ const excluded = element.querySelectorAll(selector);
283
+ for (const elem of excluded) {
284
+ elem.remove();
285
+ }
286
+ };
287
+ const cleanAttributes = (element) => {
288
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_ELEMENT, null);
289
+ const elements = [];
290
+ let node = walker.nextNode();
291
+ while (node) {
292
+ elements.push(node);
293
+ node = walker.nextNode();
294
+ }
295
+ const keepAttrs = [
296
+ "id",
297
+ "class",
298
+ "role",
299
+ "data-testid",
300
+ "href",
301
+ "src",
302
+ "alt",
303
+ "title"
304
+ ];
305
+ for (const elem of elements) {
306
+ const toRemove = Array.from(elem.attributes).filter((attr) => !keepAttrs.includes(attr.name)).map((attr) => attr.name);
307
+ for (const name of toRemove) {
308
+ elem.removeAttribute(name);
309
+ }
310
+ }
311
+ };
312
+ const normalizeWhitespace = (element) => {
313
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null);
314
+ const textNodes = [];
315
+ let node = walker.nextNode();
316
+ while (node) {
317
+ textNodes.push(node);
318
+ node = walker.nextNode();
319
+ }
320
+ for (const textNode of textNodes) {
321
+ textNode.nodeValue = (textNode.nodeValue || "").replace(/\s+/g, " ").trim();
322
+ }
323
+ };
324
+ const clone = el.cloneNode(true);
325
+ removeExcludedElements(clone, opts.excludeSelector);
326
+ removeExcludedElements(clone, "script, style");
327
+ if (!opts.includeAttributes) {
328
+ cleanAttributes(clone);
329
+ }
330
+ if (!opts.preserveWhitespace) {
331
+ normalizeWhitespace(clone);
332
+ }
333
+ return clone.outerHTML;
334
+ }, options);
335
+ }
336
+ async _extractTextContent(locator) {
337
+ const textContent = await locator.textContent();
338
+ return textContent || "";
339
+ }
340
+ async _extractAriaContent(locator) {
341
+ return await locator.evaluate((el) => {
342
+ const role = el.getAttribute("role") || el.tagName.toLowerCase();
343
+ const label = el.getAttribute("aria-label") || el.textContent?.trim() || "";
344
+ return `${role}: ${label}`;
345
+ });
346
+ }
347
+ async _processBatch(childElements, tagNames, startIdx, endIdx, childOptions, maxBytes) {
348
+ const promises = [];
349
+ for (let i = startIdx;i < endIdx && maxBytes > 0; i++) {
350
+ const childSelector = { css: tagNames[i] };
351
+ promises.push(this._extractElementHTML(childElements[i], childSelector, childOptions, maxBytes).then((result) => result ?? undefined));
352
+ }
353
+ const results = await Promise.all(promises);
354
+ return results.filter((r) => r !== undefined);
355
+ }
356
+ async _extractChildElements(parentLocator, options, remainingSizeBytes) {
357
+ try {
358
+ const children = [];
359
+ const childOptions = { ...options, depth: (options.depth || 1) - 1 };
360
+ const childElements = await parentLocator.locator("> *").all();
361
+ if (childElements.length === 0) {
362
+ return children;
363
+ }
364
+ const tagNames = await Promise.all(childElements.map((child) => child.evaluate((el) => el.tagName.toLowerCase())));
365
+ const allResults = await this._processBatch(childElements, tagNames, 0, childElements.length, childOptions, remainingSizeBytes);
366
+ let currentRemainingBytes = remainingSizeBytes;
367
+ for (const childResult of allResults) {
368
+ if (currentRemainingBytes <= 0) {
369
+ break;
370
+ }
371
+ if (childResult.metadata.sizeBytes <= currentRemainingBytes) {
372
+ children.push(childResult);
373
+ currentRemainingBytes -= childResult.metadata.sizeBytes;
374
+ }
375
+ }
376
+ return children;
377
+ } catch (error) {
378
+ tabDebug("Failed to extract child elements:", error);
379
+ return [];
380
+ }
381
+ }
382
+ _calculateElementDepth(element) {
383
+ if (!element.children || element.children.length === 0) {
384
+ return 1;
385
+ }
386
+ const maxChildDepth = Math.max(...element.children.map((child) => this._calculateElementDepth(child)));
387
+ return 1 + maxChildDepth;
388
+ }
389
+ _generateSelectorsForElement(element) {
390
+ const suggestions = [];
391
+ const { tagName, attributes } = element.metadata;
392
+ if (attributes.id) {
393
+ suggestions.push({
394
+ selector: `#${attributes.id}`,
395
+ confidence: 0.95,
396
+ element: `${tagName}#${attributes.id}`,
397
+ description: "Unique ID selector (most reliable)"
398
+ });
399
+ }
400
+ if (attributes["data-testid"]) {
401
+ suggestions.push({
402
+ selector: `[data-testid="${attributes["data-testid"]}"]`,
403
+ confidence: 0.9,
404
+ element: `${tagName}[data-testid="${attributes["data-testid"]}"]`,
405
+ description: "Test ID selector (very reliable)"
406
+ });
407
+ }
408
+ if (attributes.class) {
409
+ const classes = attributes.class.split(" ").filter(Boolean);
410
+ for (const className of classes) {
411
+ suggestions.push({
412
+ selector: `.${className}`,
413
+ confidence: 0.7,
414
+ element: `${tagName}.${className}`,
415
+ description: "Class selector (moderate reliability)"
416
+ });
417
+ }
418
+ }
419
+ if (attributes.role) {
420
+ suggestions.push({
421
+ selector: `[role="${attributes.role}"]`,
422
+ confidence: 0.8,
423
+ element: `${tagName}[role="${attributes.role}"]`,
424
+ description: "ARIA role selector (good for accessibility)"
425
+ });
426
+ }
427
+ for (const [attrName, attrValue] of Object.entries(attributes)) {
428
+ if (!["id", "class", "role", "data-testid"].includes(attrName) && attrValue) {
429
+ suggestions.push({
430
+ selector: `${tagName}[${attrName}="${attrValue}"]`,
431
+ confidence: 0.6,
432
+ element: `${tagName}[${attrName}="${attrValue}"]`,
433
+ description: `Attribute selector (${attrName})`
434
+ });
435
+ }
436
+ }
437
+ return suggestions;
438
+ }
439
+ async _optimizeElementForLLM(element) {
440
+ let optimizedHtml = element.html;
441
+ const noiseAttributes = ["style", "onclick", "onload", "onchange"];
442
+ for (const attr of noiseAttributes) {
443
+ optimizedHtml = optimizedHtml.replace(new RegExp(`\\s+${attr}="[^"]*"`, "g"), "");
444
+ }
445
+ optimizedHtml = optimizedHtml.replace(/\s+/g, " ").trim();
446
+ optimizedHtml = optimizedHtml.replace(/<(\w+)[^>]*>\s*<\/\1>/g, "");
447
+ const optimizedElement = {
448
+ ...element,
449
+ html: optimizedHtml,
450
+ metadata: {
451
+ ...element.metadata,
452
+ sizeBytes: HTMLInspectionUtils.calculateHtmlSize(optimizedHtml)
453
+ }
454
+ };
455
+ if (element.children) {
456
+ optimizedElement.children = await Promise.all(element.children.map((child) => this._optimizeElementForLLM(child)));
457
+ }
458
+ return optimizedElement;
459
+ }
460
+ async _extractComputedStyles(locator) {
461
+ try {
462
+ return await locator.evaluate((el) => {
463
+ const computed = window.getComputedStyle(el);
464
+ const styles = {};
465
+ const importantProps = [
466
+ "display",
467
+ "visibility",
468
+ "position",
469
+ "width",
470
+ "height",
471
+ "color",
472
+ "background-color",
473
+ "font-size",
474
+ "font-weight"
475
+ ];
476
+ for (const prop of importantProps) {
477
+ styles[prop] = computed.getPropertyValue(prop);
478
+ }
479
+ return styles;
480
+ });
481
+ } catch {
482
+ return;
483
+ }
484
+ }
485
+ async _extractAriaProperties(locator) {
486
+ try {
487
+ return await locator.evaluate((el) => {
488
+ const ariaProps = {};
489
+ const commonAriaProps = [
490
+ "aria-label",
491
+ "aria-describedby",
492
+ "aria-hidden",
493
+ "aria-expanded",
494
+ "aria-selected",
495
+ "aria-checked",
496
+ "aria-disabled",
497
+ "aria-required"
498
+ ];
499
+ for (const prop of commonAriaProps) {
500
+ const value = el.getAttribute(prop);
501
+ if (value) {
502
+ ariaProps[prop] = value;
503
+ }
504
+ }
505
+ return Object.keys(ariaProps).length > 0 ? ariaProps : undefined;
506
+ });
507
+ } catch {
508
+ return;
509
+ }
510
+ }
511
+ }
512
+ export {
513
+ HTMLInspector
514
+ };
@@ -0,0 +1,24 @@
1
+ import { createRequire } from "node:module";
2
+ var __create = Object.create;
3
+ var __getProtoOf = Object.getPrototypeOf;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __toESM = (mod, isNodeMode, target) => {
8
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
9
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
10
+ for (let key of __getOwnPropNames(mod))
11
+ if (!__hasOwnProp.call(to, key))
12
+ __defProp(to, key, {
13
+ get: () => mod[key],
14
+ enumerable: true
15
+ });
16
+ return to;
17
+ };
18
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
19
+
20
+ // src/utilities/index.ts
21
+ import { HTMLInspector } from "./html-inspector.js";
22
+ export {
23
+ HTMLInspector
24
+ };
@@ -18,38 +18,11 @@ var __toESM = (mod, isNodeMode, target) => {
18
18
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
19
19
 
20
20
  // src/utils/tool-patterns.ts
21
- import { BaseElementToolHandler } from "../tools/base-tool-handler.js";
22
21
  import { getErrorMessage } from "./common-formatters.js";
23
- var resolveElementLocator = async (tab, params) => {
24
-
25
- class TempHandler extends BaseElementToolHandler {
26
- constructor() {
27
- super("temp");
28
- }
29
- executeToolLogic() {
30
- return Promise.resolve();
31
- }
32
- resolveLocator(tabInstance, locatorParams) {
33
- return this.resolveElementLocator(tabInstance, locatorParams);
34
- }
35
- }
36
- return await new TempHandler().resolveLocator(tab, params);
37
- };
38
- var validateElementParams = (params) => {
39
-
40
- class TempHandler extends BaseElementToolHandler {
41
- constructor() {
42
- super("temp");
43
- }
44
- executeToolLogic() {
45
- return Promise.resolve();
46
- }
47
- validateParams(validationParams) {
48
- this.validateElementParams(validationParams);
49
- }
50
- }
51
- new TempHandler().validateParams(params);
22
+ var resolveElementLocator = (_tab, _params) => {
23
+ return Promise.resolve(undefined);
52
24
  };
25
+ var validateElementParams = (_params) => {};
53
26
  function addToolErrorContext(response, error, toolName, params) {
54
27
  const errorMessage = getErrorMessage(error);
55
28
  response.addError(`Error in ${toolName}: ${errorMessage}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tontoko/fast-playwright-mcp",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -51,8 +51,8 @@
51
51
  "dotenv": "^17.2.0",
52
52
  "fast-diff": "^1.3.0",
53
53
  "mime": "^4.0.7",
54
- "playwright": "1.55.0-alpha-2025-08-07",
55
- "playwright-core": "1.55.0-alpha-2025-08-07",
54
+ "playwright": "1.58.0-alpha-1766189059000",
55
+ "playwright-core": "1.58.0-alpha-1766189059000",
56
56
  "sharp": "^0.34.3",
57
57
  "ws": "^8.18.1",
58
58
  "zod": "^3.24.1",
@@ -61,7 +61,7 @@
61
61
  "devDependencies": {
62
62
  "@anthropic-ai/sdk": "^0.57.0",
63
63
  "@biomejs/biome": "2.1.2",
64
- "@playwright/test": "1.55.0-alpha-2025-08-07",
64
+ "@playwright/test": "1.58.0-alpha-1766189059000",
65
65
  "@types/debug": "^4.1.12",
66
66
  "@types/node": "^22.13.10",
67
67
  "@types/ws": "^8.18.1",