@skyramp/mcp 0.2.2 → 0.2.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.
@@ -56,6 +56,7 @@ export async function registerPlaywrightTools(server, options) {
56
56
  'browser_assert_table_cell',
57
57
  'skyramp_export_zip',
58
58
  'skyramp_load_trace',
59
+ 'browser_mouse_action',
59
60
  // DOM Analyzer tools (Phase C)
60
61
  'browser_blueprint',
61
62
  'browser_blueprint_diff',
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var mouseActions_exports = {};
20
+ __export(mouseActions_exports, {
21
+ DEFAULT_DRAG_STEPS: () => DEFAULT_DRAG_STEPS,
22
+ decodeModifierKeys: () => decodeModifierKeys,
23
+ decomposeDrag: () => decomposeDrag,
24
+ mouseActionToJsonl: () => mouseActionToJsonl,
25
+ mouseJsonlToCode: () => mouseJsonlToCode
26
+ });
27
+ module.exports = __toCommonJS(mouseActions_exports);
28
+ const DEFAULT_DRAG_STEPS = 10;
29
+ function decodeModifierKeys(mask, platform = typeof process !== "undefined" ? process.platform : "linux") {
30
+ if (!mask)
31
+ return [];
32
+ const keys = [];
33
+ if (mask & 1) keys.push("Alt");
34
+ if ((mask & 6) === 6) {
35
+ keys.push(platform === "darwin" ? "Meta" : "Control");
36
+ } else {
37
+ if (mask & 2) keys.push("Control");
38
+ if (mask & 4) keys.push("Meta");
39
+ }
40
+ if (mask & 8) keys.push("Shift");
41
+ return keys;
42
+ }
43
+ function decomposeDrag(start, end, steps = DEFAULT_DRAG_STEPS) {
44
+ return [
45
+ { name: "mouse.move", position: start },
46
+ { name: "mouse.down" },
47
+ { name: "mouse.move", position: end, steps },
48
+ { name: "mouse.up" }
49
+ ];
50
+ }
51
+ function mouseActionToJsonl(params) {
52
+ const button = params.button ?? "left";
53
+ const modifiers = params.modifiers ?? 0;
54
+ switch (params.action) {
55
+ case "move": {
56
+ if (params.x === void 0 || params.y === void 0)
57
+ return { error: 'mouse action "move" requires x and y.' };
58
+ const move = params.steps !== void 0 ? { name: "mouse.move", position: { x: params.x, y: params.y }, steps: params.steps } : { name: "mouse.move", position: { x: params.x, y: params.y } };
59
+ return { actions: [move] };
60
+ }
61
+ case "down":
62
+ return { actions: [button === "left" ? { name: "mouse.down" } : { name: "mouse.down", button }] };
63
+ case "up":
64
+ return { actions: [button === "left" ? { name: "mouse.up" } : { name: "mouse.up", button }] };
65
+ case "wheel": {
66
+ if (params.deltaX === void 0 && params.deltaY === void 0)
67
+ return { error: 'mouse action "wheel" requires deltaX and/or deltaY.' };
68
+ if (params.x === void 0 || params.y === void 0)
69
+ return { error: 'mouse action "wheel" requires x and y (the scroll target) so the pointer is moved there before scrolling.' };
70
+ const position = { x: params.x, y: params.y };
71
+ return { actions: [
72
+ { name: "mouse.move", position },
73
+ { name: "mouse.wheel", position, deltaX: params.deltaX ?? 0, deltaY: params.deltaY ?? 0, modifiers }
74
+ ] };
75
+ }
76
+ case "click": {
77
+ if (params.x === void 0 || params.y === void 0)
78
+ return { error: 'mouse action "click" requires x and y.' };
79
+ return { actions: [{ name: "click", selector: "body", position: { x: params.x, y: params.y }, button, modifiers, clickCount: params.clickCount ?? 1 }] };
80
+ }
81
+ case "drag": {
82
+ if (params.x === void 0 || params.y === void 0 || params.endX === void 0 || params.endY === void 0)
83
+ return { error: 'mouse action "drag" requires x, y, endX and endY.' };
84
+ return { actions: decomposeDrag({ x: params.x, y: params.y }, { x: params.endX, y: params.endY }, params.steps) };
85
+ }
86
+ default:
87
+ return { error: `Unknown mouse action: ${params.action}` };
88
+ }
89
+ }
90
+ function mouseJsonlToCode(a) {
91
+ switch (a.name) {
92
+ case "mouse.move":
93
+ return [a.steps !== void 0 ? `await page.mouse.move(${a.position.x}, ${a.position.y}, { steps: ${a.steps} });` : `await page.mouse.move(${a.position.x}, ${a.position.y});`];
94
+ case "mouse.down":
95
+ return [a.button && a.button !== "left" ? `await page.mouse.down({ button: '${a.button}' });` : `await page.mouse.down();`];
96
+ case "mouse.up":
97
+ return [a.button && a.button !== "left" ? `await page.mouse.up({ button: '${a.button}' });` : `await page.mouse.up();`];
98
+ case "mouse.wheel":
99
+ return withModifierLines(a.modifiers, `await page.mouse.wheel(${a.deltaX}, ${a.deltaY});`);
100
+ case "click": {
101
+ const opts = a.button !== "left" || a.clickCount !== 1 ? `, { button: '${a.button}', clickCount: ${a.clickCount} }` : "";
102
+ return withModifierLines(a.modifiers, `await page.mouse.click(${a.position.x}, ${a.position.y}${opts});`);
103
+ }
104
+ }
105
+ }
106
+ function withModifierLines(mask, line) {
107
+ const keys = decodeModifierKeys(mask);
108
+ if (!keys.length)
109
+ return [line];
110
+ return [
111
+ ...keys.map((k) => `await page.keyboard.down('${k}');`),
112
+ line,
113
+ ...[...keys].reverse().map((k) => `await page.keyboard.up('${k}');`)
114
+ ];
115
+ }
116
+ // Annotate the CommonJS export names for ESM import in node:
117
+ 0 && (module.exports = {
118
+ DEFAULT_DRAG_STEPS,
119
+ decodeModifierKeys,
120
+ decomposeDrag,
121
+ mouseActionToJsonl,
122
+ mouseJsonlToCode
123
+ });
@@ -31,6 +31,7 @@ module.exports = __toCommonJS(loadTraceTool_exports);
31
31
  var import_mcpBundle = require("playwright-core/lib/mcpBundle");
32
32
  var import_utils = require("playwright-core/lib/utils");
33
33
  var import_tool = require("../sdk/tool");
34
+ var import_mouseActions = require("./common/mouseActions");
34
35
  const loadTraceSchema = {
35
36
  name: "skyramp_load_trace",
36
37
  title: "Load and replay a Skyramp trace",
@@ -183,6 +184,17 @@ function urlMatchesPattern(currentUrl, pattern) {
183
184
  return (0, import_utils.urlMatches)(void 0, currentUrl, pattern);
184
185
  return currentUrl.toLowerCase().includes(pattern.toLowerCase());
185
186
  }
187
+ async function withMouseModifiers(page, mask, fn) {
188
+ const keys = (0, import_mouseActions.decodeModifierKeys)(mask);
189
+ for (const k of keys)
190
+ await page.keyboard.down(k);
191
+ try {
192
+ await fn();
193
+ } finally {
194
+ for (const k of [...keys].reverse())
195
+ await page.keyboard.up(k);
196
+ }
197
+ }
186
198
  const DEFAULT_ACTION_TIMEOUT = 15e3;
187
199
  async function performClientAction(page, actionInContext) {
188
200
  const action = actionInContext.action;
@@ -215,6 +227,40 @@ async function performClientAction(page, actionInContext) {
215
227
  await page.keyboard.press(shortcut);
216
228
  return;
217
229
  }
230
+ if (action.name === "mouse.move") {
231
+ if (!action.position)
232
+ return;
233
+ if (action.steps !== void 0)
234
+ await page.mouse.move(action.position.x, action.position.y, { steps: action.steps });
235
+ else
236
+ await page.mouse.move(action.position.x, action.position.y);
237
+ return;
238
+ }
239
+ if (action.name === "mouse.down") {
240
+ if (action.button && action.button !== "left")
241
+ await page.mouse.down({ button: action.button });
242
+ else
243
+ await page.mouse.down();
244
+ return;
245
+ }
246
+ if (action.name === "mouse.up") {
247
+ if (action.button && action.button !== "left")
248
+ await page.mouse.up({ button: action.button });
249
+ else
250
+ await page.mouse.up();
251
+ return;
252
+ }
253
+ if (action.name === "mouse.wheel") {
254
+ await withMouseModifiers(page, action.modifiers, () => page.mouse.wheel(action.deltaX ?? 0, action.deltaY ?? 0));
255
+ return;
256
+ }
257
+ if (action.name === "click" && action.position && (!action.selector || action.selector === "body")) {
258
+ const button = action.button ?? "left";
259
+ const clickCount = action.clickCount ?? 1;
260
+ const needsOpts = button !== "left" || clickCount !== 1;
261
+ await withMouseModifiers(page, action.modifiers, () => needsOpts ? page.mouse.click(action.position.x, action.position.y, { button, clickCount }) : page.mouse.click(action.position.x, action.position.y));
262
+ return;
263
+ }
218
264
  if (!action.selector)
219
265
  return;
220
266
  const selector = buildFullSelector(actionInContext.frame.framePath, action.selector);
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var mouseActionTool_exports = {};
20
+ __export(mouseActionTool_exports, {
21
+ _schema: () => mouseActionSchema,
22
+ executeMouseAction: () => executeMouseAction,
23
+ mouseActionMcpTool: () => mouseActionMcpTool,
24
+ mouseActionSchema: () => mouseActionSchema
25
+ });
26
+ module.exports = __toCommonJS(mouseActionTool_exports);
27
+ var import_mcpBundle = require("playwright-core/lib/mcpBundle");
28
+ var import_tool = require("../sdk/tool");
29
+ var import_mouseActions = require("./common/mouseActions");
30
+ const mouseActionSchema = {
31
+ name: "browser_mouse_action",
32
+ title: "Mouse action (coordinate-based)",
33
+ description: [
34
+ "Perform coordinate-based mouse interactions: drag-and-drop, slider adjustment, canvas/diagram manipulation, scrolling, and other gestures that operate on viewport pixel positions rather than element references.",
35
+ "Primary uses: drag an item between positions (drag), move a slider thumb (drag), pan/draw on a canvas (move/down/up), and scroll a region (wheel).",
36
+ "The `action` parameter selects the gesture:",
37
+ "- drag: press at (x, y), move to (endX, endY), release. Decomposed into low-level mouse events (move\u2192down\u2192move\u2192up) so it works for sliders, canvas, and custom drag-handles where high-level element drag fails.",
38
+ "- move / down / up: individual pointer steps for composing custom gestures.",
39
+ "- wheel: scroll by deltaX / deltaY at the pointer position.",
40
+ '- click: a single position click at (x, y). Use for canvas hotspots, or as a fallback for an interactive element that has no usable accessibility ref or is occluded by an overlay (where browser_click reports "intercepts pointer events"). For ordinary elements, prefer browser_click.',
41
+ "Coordinates are viewport pixels, obtained from a snapshot or screenshot."
42
+ ].join(" "),
43
+ inputSchema: import_mcpBundle.z.object({
44
+ action: import_mcpBundle.z.enum(["move", "down", "up", "click", "wheel", "drag"]).describe("The mouse action to perform."),
45
+ x: import_mcpBundle.z.number().optional().describe("X coordinate (viewport px). Required for move/click/wheel; the start X for drag."),
46
+ y: import_mcpBundle.z.number().optional().describe("Y coordinate (viewport px). Required for move/click/wheel; the start Y for drag."),
47
+ endX: import_mcpBundle.z.number().optional().describe("End X coordinate for drag."),
48
+ endY: import_mcpBundle.z.number().optional().describe("End Y coordinate for drag."),
49
+ button: import_mcpBundle.z.enum(["left", "right", "middle"]).optional().describe("Mouse button for down/up/click. Defaults to left."),
50
+ clickCount: import_mcpBundle.z.number().int().positive().optional().describe("Number of clicks for click (e.g. 2 for double-click). Defaults to 1."),
51
+ deltaX: import_mcpBundle.z.number().optional().describe("Horizontal scroll delta for wheel."),
52
+ deltaY: import_mcpBundle.z.number().optional().describe("Vertical scroll delta for wheel."),
53
+ steps: import_mcpBundle.z.number().int().positive().optional().describe("Intermediate move steps for move/drag (smoother movement)."),
54
+ modifiers: import_mcpBundle.z.number().int().min(0).max(15).optional().describe("Modifier bitmask (Alt=1, Control=2, Meta=4, Shift=8) for click/wheel. Range 0-15.")
55
+ }),
56
+ type: "action"
57
+ };
58
+ function mouseActionMcpTool() {
59
+ return (0, import_tool.toMcpTool)(mouseActionSchema);
60
+ }
61
+ async function executeMouseAction(page, params) {
62
+ const built = (0, import_mouseActions.mouseActionToJsonl)(params);
63
+ if ("error" in built)
64
+ return { result: { content: [{ type: "text", text: `### Error
65
+ ${built.error}` }], isError: true }, actions: [] };
66
+ if (!page)
67
+ return { result: { content: [{ type: "text", text: "### Error\nNo open page. Call browser_navigate first." }], isError: true }, actions: [] };
68
+ const code = [];
69
+ try {
70
+ for (const a of built.actions) {
71
+ code.push(...(0, import_mouseActions.mouseJsonlToCode)(a));
72
+ await runOne(page, a);
73
+ }
74
+ } catch (e) {
75
+ return { result: { content: [{ type: "text", text: `### Error
76
+ Mouse action failed: ${e.message}` }], isError: true }, actions: [] };
77
+ }
78
+ return {
79
+ result: { content: [{ type: "text", text: `### Ran Playwright code
80
+ ${code.join("\n")}` }] },
81
+ actions: built.actions
82
+ };
83
+ }
84
+ async function runOne(page, a) {
85
+ switch (a.name) {
86
+ case "mouse.move":
87
+ if (a.steps !== void 0)
88
+ await page.mouse.move(a.position.x, a.position.y, { steps: a.steps });
89
+ else
90
+ await page.mouse.move(a.position.x, a.position.y);
91
+ return;
92
+ case "mouse.down":
93
+ if (a.button && a.button !== "left")
94
+ await page.mouse.down({ button: a.button });
95
+ else
96
+ await page.mouse.down();
97
+ return;
98
+ case "mouse.up":
99
+ if (a.button && a.button !== "left")
100
+ await page.mouse.up({ button: a.button });
101
+ else
102
+ await page.mouse.up();
103
+ return;
104
+ case "mouse.wheel":
105
+ await withModifiers(page, a.modifiers, () => page.mouse.wheel(a.deltaX, a.deltaY));
106
+ return;
107
+ case "click": {
108
+ const needsOpts = a.button !== "left" || a.clickCount !== 1;
109
+ await withModifiers(page, a.modifiers, () => needsOpts ? page.mouse.click(a.position.x, a.position.y, { button: a.button, clickCount: a.clickCount }) : page.mouse.click(a.position.x, a.position.y));
110
+ return;
111
+ }
112
+ }
113
+ }
114
+ async function withModifiers(page, mask, fn) {
115
+ const keys = (0, import_mouseActions.decodeModifierKeys)(mask);
116
+ for (const k of keys)
117
+ await page.keyboard.down(k);
118
+ try {
119
+ await fn();
120
+ } finally {
121
+ for (const k of [...keys].reverse())
122
+ await page.keyboard.up(k);
123
+ }
124
+ }
125
+ // Annotate the CommonJS export names for ESM import in node:
126
+ 0 && (module.exports = {
127
+ _schema,
128
+ executeMouseAction,
129
+ mouseActionMcpTool,
130
+ mouseActionSchema
131
+ });
@@ -46,6 +46,7 @@ var import_assertTool = require("./assertTool");
46
46
  var import_assertApiRequestTool = require("./assertApiRequestTool");
47
47
  var import_loadTraceTool = require("./loadTraceTool");
48
48
  var import_skyRampImport = require("./skyRampImport");
49
+ var import_mouseActionTool = require("./mouseActionTool");
49
50
  var import_utils = require("playwright-core/lib/utils");
50
51
  var import_types = require("./types");
51
52
  const traceDebug = (0, import_utilsBundle.debug)("pw:mcp:trace");
@@ -119,7 +120,7 @@ class TraceRecordingBackend {
119
120
  }
120
121
  async listTools() {
121
122
  const browserTools = await this._browserBackend.listTools();
122
- return [...browserTools, (0, import_exportTool.exportZipMcpTool)(), (0, import_assertTool.assertMcpTool)(), (0, import_assertApiRequestTool.assertApiRequestMcpTool)(), (0, import_loadTraceTool.loadTraceMcpTool)()];
123
+ return [...browserTools, (0, import_exportTool.exportZipMcpTool)(), (0, import_assertTool.assertMcpTool)(), (0, import_assertApiRequestTool.assertApiRequestMcpTool)(), (0, import_loadTraceTool.loadTraceMcpTool)(), (0, import_mouseActionTool.mouseActionMcpTool)()];
123
124
  }
124
125
  async callTool(name, args, progress) {
125
126
  if (!this._initialized)
@@ -146,6 +147,10 @@ class TraceRecordingBackend {
146
147
  const parsed = import_loadTraceTool.loadTraceSchema.inputSchema.parse(args || {});
147
148
  return this._handleLoadTrace(parsed);
148
149
  }
150
+ if (name === import_mouseActionTool.mouseActionSchema.name) {
151
+ const parsed = import_mouseActionTool.mouseActionSchema.inputSchema.parse(args || {});
152
+ return this._handleMouseAction(parsed);
153
+ }
149
154
  if (name === import_assertTool.assertToolSchema.name) {
150
155
  const parsed = import_assertTool.assertToolSchema.inputSchema.parse(args || {});
151
156
  return this._handleAssert(parsed);
@@ -320,6 +325,33 @@ Reloaded current page: ${currentUrl}
320
325
  serverClosed() {
321
326
  void this._autoExportAndClose().catch(import_log.logUnhandledError);
322
327
  }
328
+ /**
329
+ * Execute a coordinate-based mouse action and track each decomposed sub-action
330
+ * (mouse.move/down/up/wheel or a position click) into _trackedActions so it is
331
+ * exported into the combined JSONL trace. The JSONL field shapes come straight
332
+ * from common/mouseActions, which is the single source of truth shared with
333
+ * the export and seeding paths.
334
+ */
335
+ async _handleMouseAction(params) {
336
+ const page = this._browserBackend.context?.currentTab()?.page;
337
+ const timestamp = Date.now();
338
+ const pageAlias = this._currentPageAlias;
339
+ const { result, actions } = await (0, import_mouseActionTool.executeMouseAction)(page, params);
340
+ if (result.isError)
341
+ return result;
342
+ actions.forEach((a, i) => {
343
+ this._trackedActions.push({
344
+ toolName: "browser_mouse_action",
345
+ args: a,
346
+ code: "",
347
+ // Stagger timestamps so a decomposed drag keeps its move<down<move<up order.
348
+ timestamp: timestamp + i,
349
+ pageAlias
350
+ });
351
+ });
352
+ traceDebug(`Tracked ${actions.length} mouse sub-action(s) for "${params.action}" on ${pageAlias}`);
353
+ return result;
354
+ }
323
355
  /**
324
356
  * Load a prior Skyramp trace and replay it against the live browser, honoring
325
357
  * an optional stop point, then seed _trackedActions with the replayed actions
@@ -455,6 +487,14 @@ Continue recording with browser_* tools, then call skyramp_export_zip to write t
455
487
  */
456
488
  _seedTrackedActionFields(a, locatorExpr) {
457
489
  const sq = (s) => `'${String(s).replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t")}'`;
490
+ if (a.name === "mouse.move" || a.name === "mouse.down" || a.name === "mouse.up" || a.name === "mouse.wheel") {
491
+ const args = this._seedMouseActionArgs(a);
492
+ if (!args)
493
+ return null;
494
+ return { toolName: "browser_mouse_action", code: "", args };
495
+ }
496
+ if (a.name === "click" && a.position && (!a.selector || a.selector === "body"))
497
+ return { toolName: "browser_mouse_action", code: "", args: { name: "click", selector: "body", position: a.position, button: a.button ?? "left", modifiers: a.modifiers ?? 0, clickCount: a.clickCount ?? 1 } };
458
498
  const SELECTOR_REQUIRED = /* @__PURE__ */ new Set(["click", "hover", "fill", "pressSequentially", "check", "uncheck", "select"]);
459
499
  if (SELECTOR_REQUIRED.has(a.name) && !locatorExpr)
460
500
  return null;
@@ -530,6 +570,35 @@ Continue recording with browser_* tools, then call skyramp_export_zip to write t
530
570
  return null;
531
571
  }
532
572
  }
573
+ /**
574
+ * Build the normalized JSONL args for a re-seeded coordinate mouse action
575
+ * (mouse.move/down/up/wheel), following the MouseJsonlAction contract in
576
+ * common/mouseActions.ts. Only known fields are emitted (so stray fields from
577
+ * the loaded trace line are not forwarded), wheel deltas/modifiers default to
578
+ * 0, and a move/wheel missing its position is rejected (returns null) so the
579
+ * caller skips it rather than exporting an invalid shape for the Go consumer.
580
+ */
581
+ _seedMouseActionArgs(a) {
582
+ switch (a.name) {
583
+ case "mouse.move": {
584
+ if (!a.position)
585
+ return null;
586
+ const args = { name: "mouse.move", position: a.position };
587
+ if (a.steps !== void 0)
588
+ args.steps = a.steps;
589
+ return args;
590
+ }
591
+ case "mouse.down":
592
+ case "mouse.up":
593
+ return a.button && a.button !== "left" ? { name: a.name, button: a.button } : { name: a.name };
594
+ case "mouse.wheel":
595
+ if (!a.position)
596
+ return null;
597
+ return { name: "mouse.wheel", position: a.position, deltaX: a.deltaX ?? 0, deltaY: a.deltaY ?? 0, modifiers: a.modifiers ?? 0 };
598
+ default:
599
+ return null;
600
+ }
601
+ }
533
602
  /**
534
603
  * Handle browser_select_option with automatic fallback for custom dropdowns.
535
604
  * 1. Try native selectOption first.
@@ -654,14 +723,26 @@ Could not extract selector from hover result.` }], isError: true };
654
723
  }
655
724
  const locatorExpr = locatorMatch[1].trim();
656
725
  let parsed = this._codeToLocator(locatorExpr);
657
- if (!parsed) {
726
+ const snapResult = await this._browserBackend.callTool("browser_snapshot", {});
727
+ if (snapResult.isError) {
728
+ const errText = snapResult.content?.[0]?.type === "text" ? snapResult.content[0].text : "";
658
729
  return { content: [{ type: "text", text: `### Assertion Failed
659
- Could not parse locator: ${locatorExpr}` }], isError: true };
730
+ Could not capture page snapshot to verify the assertion. ${errText}` }], isError: true };
660
731
  }
661
- const snapResult = await this._browserBackend.callTool("browser_snapshot", {});
662
732
  const snapText = snapResult.content?.map((c) => c.type === "text" ? c.text : "").join("") || "";
663
733
  const snapLines = snapText.split("\n");
664
734
  const refLine = snapLines.find((l) => l.includes(`[ref=${params.ref}]`)) || "";
735
+ if (!parsed || parsed.locator.kind === "css") {
736
+ const fromSnapshot = this._extractLocatorForRef(refLine);
737
+ if (fromSnapshot) {
738
+ traceDebug(`Resolved assert selector from snapshot accessible name (hover code was ${parsed ? "brittle CSS" : "unparseable"}: ${locatorExpr})`);
739
+ parsed = fromSnapshot;
740
+ }
741
+ }
742
+ if (!parsed) {
743
+ return { content: [{ type: "text", text: `### Assertion Failed
744
+ Could not parse locator: ${locatorExpr}` }], isError: true };
745
+ }
665
746
  const textMatch = refLine.match(/^\s*-\s*\w+\s+"([^"]*)"/);
666
747
  const elementText = textMatch?.[1] || "";
667
748
  const valueMatch = refLine.match(/\]:\s*(.+)$/);
@@ -739,6 +820,13 @@ ${details}` }]
739
820
  if (ariaRefMatch) {
740
821
  return null;
741
822
  }
823
+ const cssMatch = expr.match(/locator\(\s*['"]([#.][^'"]+)['"]\s*\)/);
824
+ if (cssMatch) {
825
+ return {
826
+ selector: cssMatch[1],
827
+ locator: { kind: "css", body: cssMatch[1], options: {} }
828
+ };
829
+ }
742
830
  return null;
743
831
  }
744
832
  /**
@@ -227,6 +227,11 @@ function trackedActionToJsonl(action, pageGuid, timestamp) {
227
227
  }
228
228
  if (toolName === "browser_press_key")
229
229
  return JSON.stringify({ name: "press", key: args.key, modifiers: modifiersArrayToMask(args.modifiers), selector: "", ...base });
230
+ if (toolName === "browser_mouse_action") {
231
+ if (!args.name)
232
+ return null;
233
+ return JSON.stringify({ ...args, ...base });
234
+ }
230
235
  if (!code)
231
236
  return null;
232
237
  const extracted = extractLocatorFromCode(code);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyramp/mcp",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "main": "build/index.js",
5
5
  "exports": {
6
6
  ".": "./build/index.js",