@tontoko/fast-playwright-mcp 0.0.4 → 0.0.6

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
@@ -69,7 +69,7 @@ First, install the Playwright MCP server with your client.
69
69
  }
70
70
  ```
71
71
 
72
- [<img src="https://img.shields.io/badge/VS_Code-VS_Code?style=flat-square&label=Install%20Server&color=0098FF" alt="Install in VS Code">](https://insiders.vscode.dev/redirect?url=vscode%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522playwright%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522%2540playwright%252Fmcp%2540latest%2522%255D%257D) [<img alt="Install in VS Code Insiders" src="https://img.shields.io/badge/VS_Code_Insiders-VS_Code_Insiders?style=flat-square&label=Install%20Server&color=24bfa5">](https://insiders.vscode.dev/redirect?url=vscode-insiders%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522playwright%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522%2540playwright%252Fmcp%2540latest%2522%255D%257D)
72
+ [<img src="https://img.shields.io/badge/VS_Code-VS_Code?style=flat-square&label=Install%20Server&color=0098FF" alt="Install in VS Code">](https://insiders.vscode.dev/redirect?url=vscode%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522fast-playwright%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522%2540tontoko%252Ffast-playwright-mcp%2540latest%2522%255D%257D) [<img alt="Install in VS Code Insiders" src="https://img.shields.io/badge/VS_Code_Insiders-VS_Code_Insiders?style=flat-square&label=Install%20Server&color=24bfa5">](https://insiders.vscode.dev/redirect?url=vscode-insiders%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522fast-playwright%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522%2540tontoko%252Ffast-playwright-mcp%2540latest%2522%255D%257D)
73
73
 
74
74
 
75
75
  <details>
@@ -94,11 +94,11 @@ Follow the MCP install [guide](https://modelcontextprotocol.io/quickstart/user),
94
94
 
95
95
  #### Click the button to install:
96
96
 
97
- [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](cursor://anysphere.cursor-deeplink/mcp/install?name=Playwright&config=eyJjb21tYW5kIjoibnB4IEBwbGF5d3JpZ2h0L21jcEBsYXRlc3QifQ%3D%3D)
97
+ [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](cursor://anysphere.cursor-deeplink/mcp/install?name=fast-playwright&config=eyJjb21tYW5kIjoibnB4IEB0b250b2tvL2Zhc3QtcGxheXdyaWdodC1tY3BAbGF0ZXN0In0K)
98
98
 
99
99
  #### Or install manually:
100
100
 
101
- Go to `Cursor Settings` -> `MCP` -> `Add new MCP Server`. Name to your liking, use `command` type with the command `npx @tontoko/fast-playwright-mcp`. You can also verify config or add command like arguments via clicking `Edit`.
101
+ Go to `Cursor Settings` -> `MCP` -> `Add new MCP Server`. Name to your liking, use `command` type with the command `npx @tontoko/fast-playwright-mcp@latest`. You can also verify config or add command like arguments via clicking `Edit`.
102
102
 
103
103
  </details>
104
104
 
@@ -146,7 +146,7 @@ Click <code>Save</code>.
146
146
 
147
147
  #### Click the button to install:
148
148
 
149
- [<img src="https://img.shields.io/badge/VS_Code-VS_Code?style=flat-square&label=Install%20Server&color=0098FF" alt="Install in VS Code">](https://insiders.vscode.dev/redirect?url=vscode%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522playwright%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522%2540playwright%252Fmcp%2540latest%2522%255D%257D) [<img alt="Install in VS Code Insiders" src="https://img.shields.io/badge/VS_Code_Insiders-VS_Code_Insiders?style=flat-square&label=Install%20Server&color=24bfa5">](https://insiders.vscode.dev/redirect?url=vscode-insiders%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522playwright%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522%2540playwright%252Fmcp%2540latest%2522%255D%257D)
149
+ [<img src="https://img.shields.io/badge/VS_Code-VS_Code?style=flat-square&label=Install%20Server&color=0098FF" alt="Install in VS Code">](https://insiders.vscode.dev/redirect?url=vscode%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522fast-playwright%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522%2540tontoko%252Ffast-playwright-mcp%2540latest%2522%255D%257D) [<img alt="Install in VS Code Insiders" src="https://img.shields.io/badge/VS_Code_Insiders-VS_Code_Insiders?style=flat-square&label=Install%20Server&color=24bfa5">](https://insiders.vscode.dev/redirect?url=vscode-insiders%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522fast-playwright%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522%2540tontoko%252Ffast-playwright-mcp%2540latest%2522%255D%257D)
150
150
 
151
151
  #### Or install manually:
152
152
 
@@ -18,17 +18,26 @@ var __toESM = (mod, isNodeMode, target) => {
18
18
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
19
19
 
20
20
  // src/batch/batch-executor.ts
21
+ import { randomBytes } from "node:crypto";
22
+ import debug from "debug";
21
23
  import { Response } from "../response.js";
22
24
  import { mergeExpectations } from "../schemas/expectation.js";
23
25
  import { getErrorMessage } from "../utils/common-formatters.js";
26
+ var batchDebug = debug("pw:mcp:batch");
24
27
 
25
28
  class BatchExecutor {
26
29
  toolRegistry;
27
30
  context;
31
+ currentBatchContext;
28
32
  constructor(context, toolRegistry) {
29
33
  this.context = context;
30
34
  this.toolRegistry = toolRegistry;
31
35
  }
36
+ generateBatchId() {
37
+ const timestamp = Date.now();
38
+ const random = randomBytes(4).toString("hex");
39
+ return `batch_${timestamp}_${random}`;
40
+ }
32
41
  validateAllSteps(steps) {
33
42
  for (const [index, step] of steps.entries()) {
34
43
  const tool = this.toolRegistry.get(step.tool);
@@ -52,6 +61,11 @@ class BatchExecutor {
52
61
  const results = [];
53
62
  const startTime = Date.now();
54
63
  let stopReason = "completed";
64
+ this.currentBatchContext = {
65
+ batchId: this.generateBatchId(),
66
+ startTime
67
+ };
68
+ batchDebug(`Starting batch execution ${this.currentBatchContext.batchId} with ${options.steps.length} steps`);
55
69
  this.validateAllSteps(options.steps);
56
70
  const executeSequentially = async (index) => {
57
71
  if (index >= options.steps.length) {
@@ -60,7 +74,10 @@ class BatchExecutor {
60
74
  const step = options.steps[index];
61
75
  const stepStartTime = Date.now();
62
76
  try {
63
- const result = await this.executeStep(step, options.globalExpectation);
77
+ if (this.currentBatchContext) {
78
+ this.currentBatchContext.currentStepIndex = index;
79
+ }
80
+ const result = await this.executeStep(step, options.globalExpectation, this.currentBatchContext);
64
81
  const stepEndTime = Date.now();
65
82
  results.push({
66
83
  stepIndex: index,
@@ -100,7 +117,7 @@ class BatchExecutor {
100
117
  stopReason
101
118
  };
102
119
  }
103
- async executeStep(step, globalExpectation) {
120
+ async executeStep(step, globalExpectation, batchContext) {
104
121
  const tool = this.toolRegistry.get(step.tool);
105
122
  if (!tool) {
106
123
  throw new Error(`Unknown tool: ${step.tool}`);
@@ -110,10 +127,18 @@ class BatchExecutor {
110
127
  ...step.arguments,
111
128
  expectation: mergedExpectation
112
129
  };
113
- const response = new Response(this.context, step.tool, argsWithExpectation, mergedExpectation);
114
- await tool.handle(this.context, argsWithExpectation, response);
115
- await response.finish();
116
- return response.serialize();
130
+ const previousBatchContext = this.context.batchContext;
131
+ this.context.batchContext = batchContext;
132
+ try {
133
+ const response = new Response(this.context, step.tool, argsWithExpectation, mergedExpectation);
134
+ batchDebug(`Executing batch step: ${step.tool}`);
135
+ await tool.handle(this.context, argsWithExpectation, response);
136
+ await response.finish();
137
+ batchDebug(`Batch step ${step.tool} completed`);
138
+ return response.serialize();
139
+ } finally {
140
+ this.context.batchContext = previousBatchContext;
141
+ }
117
142
  }
118
143
  mergeStepExpectations(toolName, globalExpectation, stepExpectation) {
119
144
  let merged = mergeExpectations(toolName);
@@ -79,10 +79,12 @@ class BrowserServerBackend {
79
79
  throw new Error(`Tool not found: ${schema.name}`);
80
80
  }
81
81
  context.setRunningTool(true);
82
+ backendDebug(`Executing tool: ${schema.name}`);
82
83
  try {
83
84
  await matchedTool.handle(context, parsedArguments, response);
84
85
  await response.finish();
85
86
  this._sessionLog?.logResponse(response);
87
+ backendDebug(`Tool ${schema.name} completed successfully`);
86
88
  } catch (error) {
87
89
  backendDebug(`Error executing tool ${schema.name}:`, error);
88
90
  response.addError(String(error));
package/lib/context.js CHANGED
@@ -24,6 +24,7 @@ import { outputFile } from "./config.js";
24
24
  import { logUnhandledError } from "./log.js";
25
25
  import { Tab } from "./tab.js";
26
26
  var testDebug = debug("pw:mcp:test");
27
+ var contextDebug = debug("pw:mcp:context");
27
28
 
28
29
  class Context {
29
30
  tools;
@@ -40,6 +41,7 @@ class Context {
40
41
  _closeBrowserContextPromise;
41
42
  _isRunningTool = false;
42
43
  _abortController = new AbortController;
44
+ batchContext;
43
45
  constructor(options) {
44
46
  this.tools = options.tools;
45
47
  this.config = options.config;
@@ -66,6 +68,7 @@ class Context {
66
68
  return this._currentTab;
67
69
  }
68
70
  async newTab() {
71
+ contextDebug("Creating new tab");
69
72
  const { browserContext } = await this._ensureBrowserContext();
70
73
  const page = await browserContext.newPage();
71
74
  const tab = this._tabs.find((t) => t.page === page);
@@ -73,6 +76,7 @@ class Context {
73
76
  throw new Error("Failed to create tab: tab not found after creation");
74
77
  }
75
78
  this._currentTab = tab;
79
+ contextDebug("New tab created successfully");
76
80
  return this._currentTab;
77
81
  }
78
82
  async selectTab(index) {
@@ -121,13 +125,21 @@ class Context {
121
125
  this._currentTab = this._tabs[Math.min(index, this._tabs.length - 1)];
122
126
  }
123
127
  if (!this._tabs.length) {
124
- this.closeBrowserContext().catch(() => {});
128
+ contextDebug("No tabs remaining, closing browser context");
129
+ this.closeBrowserContext().catch((error) => {
130
+ contextDebug("Error closing browser context:", error);
131
+ });
125
132
  }
126
133
  }
127
134
  async closeBrowserContext() {
128
- this._closeBrowserContextPromise ??= this._closeBrowserContextImpl().catch(logUnhandledError);
135
+ contextDebug("Closing browser context");
136
+ this._closeBrowserContextPromise ??= this._closeBrowserContextImpl().catch((error) => {
137
+ contextDebug("Failed to close browser context:", error);
138
+ logUnhandledError(error);
139
+ });
129
140
  await this._closeBrowserContextPromise;
130
141
  this._closeBrowserContextPromise = undefined;
142
+ contextDebug("Browser context closed");
131
143
  }
132
144
  isRunningTool() {
133
145
  return this._isRunningTool;
@@ -175,8 +187,10 @@ class Context {
175
187
  }
176
188
  _ensureBrowserContext() {
177
189
  this._browserContextPromise ??= (() => {
190
+ contextDebug("Ensuring browser context exists");
178
191
  const promise = this._setupBrowserContext();
179
- promise.catch(() => {
192
+ promise.catch((error) => {
193
+ contextDebug("Failed to setup browser context:", error);
180
194
  this._browserContextPromise = undefined;
181
195
  });
182
196
  return promise;
@@ -187,6 +201,7 @@ class Context {
187
201
  if (this._closeBrowserContextPromise) {
188
202
  throw new Error("Another browser context is being closed.");
189
203
  }
204
+ contextDebug("Setting up new browser context");
190
205
  const result = await this._browserContextFactory.createContext(this._clientInfo, this._abortController.signal);
191
206
  const { browserContext } = result;
192
207
  await this._setupRequestInterception(browserContext);
@@ -23,7 +23,6 @@ import { DiagnosticBase } from "./common/diagnostic-base.js";
23
23
  import { safeDispose } from "./common/error-enrichment-utils.js";
24
24
  import { SmartHandleBatch } from "./smart-handle.js";
25
25
  var elementDiscoveryDebug = debug("pw:mcp:element-discovery");
26
-
27
26
  class ElementDiscovery extends DiagnosticBase {
28
27
  smartHandleBatch;
29
28
  maxBatchSize = 100;
@@ -358,23 +357,134 @@ class ElementDiscovery extends DiagnosticBase {
358
357
  }
359
358
  async generateSelector(element) {
360
359
  return await element.evaluate((el) => {
360
+ const CLASS_SPLIT_PATTERN = "\\s+";
361
+ const classSplitRegex = new RegExp(CLASS_SPLIT_PATTERN);
362
+ const isUnique = (selector) => {
363
+ return document.querySelectorAll(selector).length === 1;
364
+ };
365
+ const getClasses = (elem) => {
366
+ return elem.className ? `.${elem.className.trim().split(classSplitRegex).join(".")}` : "";
367
+ };
368
+ const tryIdSelector = (elem, elemTag) => {
369
+ if (!elem.id) {
370
+ return null;
371
+ }
372
+ const selector = `${elemTag}#${elem.id}`;
373
+ return isUnique(selector) ? selector : null;
374
+ };
375
+ const tryDataAttributes = (elem, elemTag) => {
376
+ const dataAttrs = Array.from(elem.attributes).filter((attr) => attr.name.startsWith("data-")).map((attr) => `[${attr.name}="${attr.value}"]`).join("");
377
+ if (!dataAttrs) {
378
+ return null;
379
+ }
380
+ const selector = `${elemTag}${dataAttrs}`;
381
+ return isUnique(selector) ? selector : null;
382
+ };
383
+ const tryMeaningfulAttributes = (elem, elemTag) => {
384
+ const attrs = [
385
+ "name",
386
+ "type",
387
+ "aria-label",
388
+ "placeholder",
389
+ "value",
390
+ "role"
391
+ ];
392
+ for (const attrName of attrs) {
393
+ const attrValue = elem.getAttribute(attrName);
394
+ if (attrValue) {
395
+ const selector = `${elemTag}[${attrName}="${attrValue}"]`;
396
+ if (isUnique(selector)) {
397
+ return selector;
398
+ }
399
+ }
400
+ }
401
+ return null;
402
+ };
403
+ const tryClassSelector = (elem, elemTag) => {
404
+ const classes = getClasses(elem);
405
+ if (!classes) {
406
+ return null;
407
+ }
408
+ const selector = `${elemTag}${classes}`;
409
+ return isUnique(selector) ? selector : null;
410
+ };
411
+ const tryParentContext = (elem, elemTag) => {
412
+ const parent = elem.parentElement;
413
+ if (!parent) {
414
+ return null;
415
+ }
416
+ const parentSelector = buildParentSelector(parent);
417
+ const selector = `${parentSelector} ${elemTag}`;
418
+ return isUnique(selector) ? selector : null;
419
+ };
420
+ const buildParentSelector = (parent) => {
421
+ const tag2 = parent.tagName.toLowerCase();
422
+ const id = parent.id ? `#${parent.id}` : "";
423
+ const classes = getClasses(parent);
424
+ return `${tag2}${id}${classes}`;
425
+ };
426
+ const tryNthOfType = (elem, elemTag, siblings, parentSelector, elemClasses) => {
427
+ const sameTagSiblings = siblings.filter((s) => s.tagName.toLowerCase() === elemTag);
428
+ if (sameTagSiblings.length > 1) {
429
+ const tagIndex = sameTagSiblings.indexOf(elem) + 1;
430
+ const selector = `${parentSelector} > ${elemTag}${elemClasses}:nth-of-type(${tagIndex})`;
431
+ if (isUnique(selector)) {
432
+ return selector;
433
+ }
434
+ }
435
+ return null;
436
+ };
437
+ const tryGrandparentContext = (parent, parentTag, elemTag, index) => {
438
+ const grandParent = parent.parentElement;
439
+ if (!grandParent) {
440
+ return null;
441
+ }
442
+ const gpSelector = buildParentSelector(grandParent);
443
+ const parentIndex = Array.from(grandParent.children).filter((c) => c.tagName.toLowerCase() === parentTag).indexOf(parent) + 1;
444
+ const selector = `${gpSelector} ${parentTag}:nth-of-type(${parentIndex}) > ${elemTag}:nth-child(${index})`;
445
+ return isUnique(selector) ? selector : null;
446
+ };
447
+ const tryNthSelectors = (elem, elemTag) => {
448
+ const parent = elem.parentElement;
449
+ if (!parent) {
450
+ return null;
451
+ }
452
+ const siblings = Array.from(parent.children);
453
+ const index = siblings.indexOf(elem) + 1;
454
+ const parentSelector = buildParentSelector(parent);
455
+ const elemClasses = getClasses(elem);
456
+ const nthOfTypeResult = tryNthOfType(elem, elemTag, siblings, parentSelector, elemClasses);
457
+ if (nthOfTypeResult) {
458
+ return nthOfTypeResult;
459
+ }
460
+ const nthChildSelector = `${parentSelector} > ${elemTag}${elemClasses}:nth-child(${index})`;
461
+ if (isUnique(nthChildSelector)) {
462
+ return nthChildSelector;
463
+ }
464
+ const parentTag = parent.tagName.toLowerCase();
465
+ const gpResult = tryGrandparentContext(parent, parentTag, elemTag, index);
466
+ if (gpResult) {
467
+ return gpResult;
468
+ }
469
+ return nthChildSelector;
470
+ };
361
471
  if (!(el instanceof Element)) {
362
472
  return "unknown";
363
473
  }
364
474
  const tag = el.tagName.toLowerCase();
365
- const id = el.id ? `#${el.id}` : "";
366
- const classes = el.className ? `.${el.className.split(" ").join(".")}` : "";
367
- if (id) {
368
- return `${tag}${id}`;
369
- }
370
- if (classes) {
371
- return `${tag}${classes}`;
372
- }
373
- const parent = el.parentElement;
374
- if (parent) {
375
- const siblings = Array.from(parent.children);
376
- const index = siblings.indexOf(el) + 1;
377
- return `${parent.tagName.toLowerCase()} > ${tag}:nth-child(${index})`;
475
+ const strategies = [
476
+ () => tryIdSelector(el, tag),
477
+ () => tryDataAttributes(el, tag),
478
+ () => tryMeaningfulAttributes(el, tag),
479
+ () => tryClassSelector(el, tag),
480
+ () => tryParentContext(el, tag),
481
+ () => tryNthSelectors(el, tag)
482
+ ];
483
+ for (const strategy of strategies) {
484
+ const result = strategy();
485
+ if (result) {
486
+ return result;
487
+ }
378
488
  }
379
489
  return tag;
380
490
  });
@@ -87,7 +87,9 @@ class ResourceManager {
87
87
  }
88
88
  startCleanupTimer() {
89
89
  this.cleanupInterval = setInterval(() => {
90
- this.cleanupExpiredResources().catch(() => {});
90
+ this.cleanupExpiredResources().catch((error) => {
91
+ resourceDebug("Resource cleanup error (handled internally):", error);
92
+ });
91
93
  }, this.disposeTimeout / 2);
92
94
  }
93
95
  async cleanupExpiredResources() {
package/lib/response.js CHANGED
@@ -434,7 +434,9 @@ ${this._code.join(`
434
434
  await tab.page.waitForLoadState("load", {
435
435
  timeout: TIMEOUTS.MEDIUM_DELAY
436
436
  });
437
- await tab.page.waitForLoadState("networkidle", { timeout: TIMEOUTS.SHORT_DELAY }).catch(() => {});
437
+ await tab.page.waitForLoadState("networkidle", { timeout: TIMEOUTS.SHORT_DELAY }).catch((error) => {
438
+ responseDebug("networkidle timeout ignored during stability check:", error);
439
+ });
438
440
  return await tab.page.evaluate(() => document.readyState === "complete").catch(() => false);
439
441
  } catch (error) {
440
442
  responseDebug("Page stability check failed (retrying):", error);
package/lib/tab.js CHANGED
@@ -29,6 +29,7 @@ var TabEvents = {
29
29
  modalState: "modalState"
30
30
  };
31
31
  var snapshotDebug = debug("pw:mcp:snapshot");
32
+ var tabDebug = debug("pw:mcp:tab");
32
33
 
33
34
  class Tab extends EventEmitter {
34
35
  context;
@@ -40,6 +41,8 @@ class Tab extends EventEmitter {
40
41
  _onPageClose;
41
42
  _modalStates = [];
42
43
  _downloads = [];
44
+ _customRefMappings = new Map;
45
+ _customRefCounter = 0;
43
46
  _navigationState = {
44
47
  isNavigating: false,
45
48
  lastNavigationStart: 0
@@ -63,7 +66,9 @@ class Tab extends EventEmitter {
63
66
  });
64
67
  page.on("dialog", (dialog) => this._dialogShown(dialog));
65
68
  page.on("download", (download) => {
66
- this._downloadStarted(download).catch(() => {});
69
+ this._downloadStarted(download).catch((error) => {
70
+ tabDebug("Download error ignored:", error);
71
+ });
67
72
  });
68
73
  page.on("framenavigated", (frame) => {
69
74
  if (frame === page.mainFrame()) {
@@ -138,7 +143,11 @@ class Tab extends EventEmitter {
138
143
  return this === this.context.currentTab();
139
144
  }
140
145
  async waitForLoadState(state, options) {
141
- await callOnPageNoTrace(this.page, (page) => page.waitForLoadState(state, options).catch(logUnhandledError));
146
+ tabDebug(`Waiting for load state: ${state}`);
147
+ await callOnPageNoTrace(this.page, (page) => page.waitForLoadState(state, options).catch((error) => {
148
+ tabDebug(`Failed to wait for load state ${state}:`, error);
149
+ logUnhandledError(error);
150
+ }));
142
151
  }
143
152
  _handleNavigationStart() {
144
153
  this._navigationState.isNavigating = true;
@@ -178,6 +187,7 @@ class Tab extends EventEmitter {
178
187
  }
179
188
  }
180
189
  async navigate(url) {
190
+ tabDebug(`Navigating to: ${url}`);
181
191
  this._clearCollectedArtifacts();
182
192
  const downloadEvent = callOnPageNoTrace(this.page, (page) => page.waitForEvent("download").catch(logUnhandledError));
183
193
  try {
@@ -344,12 +354,34 @@ class Tab extends EventEmitter {
344
354
  async waitForCompletion(callback) {
345
355
  await this._raceAgainstModalStates(() => waitForCompletion(this, callback));
346
356
  }
357
+ registerCustomRef(ref, selector) {
358
+ this._customRefMappings.set(ref, selector);
359
+ }
360
+ unregisterCustomRef(ref) {
361
+ this._customRefMappings.delete(ref);
362
+ }
363
+ clearCustomRefs() {
364
+ this._customRefMappings.clear();
365
+ }
366
+ getNextCustomRefId(options) {
367
+ this._customRefCounter++;
368
+ if (options?.batchId && options.batchId.length > 0) {
369
+ return `batch_${options.batchId}_element_${this._customRefCounter}`;
370
+ }
371
+ return `element_${this._customRefCounter}`;
372
+ }
347
373
  async refLocator(params) {
348
374
  return (await this.refLocators([params]))[0];
349
375
  }
350
376
  async refLocators(params) {
351
377
  const snapshot = await this.page._snapshotForAI();
352
378
  return params.map((param) => {
379
+ if (this._customRefMappings.has(param.ref)) {
380
+ const selector = this._customRefMappings.get(param.ref);
381
+ if (selector) {
382
+ return this.page.locator(selector).describe(param.element);
383
+ }
384
+ }
353
385
  if (!snapshot.includes(`[ref=${param.ref}]`)) {
354
386
  const availableRefs = this._getAvailableRefs(snapshot);
355
387
  throw new Error(`Ref ${param.ref} not found. Available refs: [${availableRefs.join(", ")}]. Element: ${param.element}. Consider capturing a new snapshot if the page has changed.`);
@@ -62,6 +62,24 @@ var browserFindElements = defineTabTool({
62
62
  response.addResult("No elements found matching the specified criteria.");
63
63
  return;
64
64
  }
65
+ const batchId = tab.context.batchContext?.batchId;
66
+ const usesBatchRefs = Boolean(batchId);
67
+ if (usesBatchRefs) {
68
+ for (const alt of alternatives) {
69
+ const customRef = tab.getNextCustomRefId({ batchId });
70
+ tab.registerCustomRef(customRef, alt.selector);
71
+ alt.ref = customRef;
72
+ }
73
+ } else {
74
+ for (let i = 1;i <= 100; i++) {
75
+ tab.unregisterCustomRef(`found_${i}`);
76
+ }
77
+ for (const [index, alt] of alternatives.entries()) {
78
+ const customRef = `found_${index + 1}`;
79
+ tab.registerCustomRef(customRef, alt.selector);
80
+ alt.ref = customRef;
81
+ }
82
+ }
65
83
  const resultsText = formatElementResults(alternatives);
66
84
  await addDiagnosticInfoIfRequested(tab, context, resultsText);
67
85
  addPerformanceInfoIfAvailable(context, resultsText);
@@ -215,7 +233,8 @@ function formatElementResults(alternatives) {
215
233
  builder.addLine(`Found ${alternatives.length} elements matching the criteria:`);
216
234
  builder.addEmptyLine();
217
235
  for (const [index, alt] of alternatives.entries()) {
218
- builder.addLine(`${index + 1}. Selector: ${alt.selector}`);
236
+ builder.addLine(`${index + 1}. Ref: ${alt.ref}`);
237
+ builder.addLine(` Selector: ${alt.selector}`);
219
238
  builder.addLine(` Confidence: ${formatConfidencePercentage(alt.confidence)}`);
220
239
  builder.addLine(` Reason: ${alt.reason ?? "No reason provided"}`);
221
240
  if (index < alternatives.length - 1) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tontoko/fast-playwright-mcp",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },