@skyramp/mcp 0.2.0 → 0.2.1
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/build/playwright/registerPlaywrightTools.js +1 -0
- package/build/utils/docker.test.js +1 -1
- package/build/utils/versions.js +1 -1
- package/node_modules/playwright/lib/mcp/skyramp/assertApiRequestTool.js +46 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +298 -51
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +5 -0
- package/package.json +2 -2
- package/node_modules/playwright/lib/mcp/browser/tools/domAnalyzer.js +0 -261
- package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.3.tgz +0 -0
- package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.4.tgz +0 -0
- package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.5.tgz +0 -0
- package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.6.tgz +0 -0
|
@@ -54,7 +54,7 @@ describe("dockerImageExistsLocally", () => {
|
|
|
54
54
|
});
|
|
55
55
|
});
|
|
56
56
|
describe("pullDockerImage", () => {
|
|
57
|
-
const IMAGE = "skyramp/executor:v1.3.
|
|
57
|
+
const IMAGE = "skyramp/executor:v1.3.26";
|
|
58
58
|
beforeEach(() => jest.clearAllMocks());
|
|
59
59
|
describe("on amd64 host", () => {
|
|
60
60
|
const originalArch = process.arch;
|
package/build/utils/versions.js
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
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 assertApiRequestTool_exports = {};
|
|
20
|
+
__export(assertApiRequestTool_exports, {
|
|
21
|
+
assertApiRequestMcpTool: () => assertApiRequestMcpTool,
|
|
22
|
+
assertApiRequestSchema: () => assertApiRequestSchema
|
|
23
|
+
});
|
|
24
|
+
module.exports = __toCommonJS(assertApiRequestTool_exports);
|
|
25
|
+
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
|
|
26
|
+
var import_tool = require("../sdk/tool");
|
|
27
|
+
const assertApiRequestSchema = {
|
|
28
|
+
name: "browser_assert_api_request",
|
|
29
|
+
title: "Assert API request payload",
|
|
30
|
+
description: [
|
|
31
|
+
"Mark the NEXT user action for API request payload assertion.",
|
|
32
|
+
"Call this tool IMMEDIATELY BEFORE the click or action that triggers an API request you want to validate.",
|
|
33
|
+
"The generated test will capture the request payload and assert it matches on replay.",
|
|
34
|
+
'Example: call this tool, then call browser_click on a "Save" button that sends a POST request.'
|
|
35
|
+
].join(" "),
|
|
36
|
+
inputSchema: import_mcpBundle.z.object({}),
|
|
37
|
+
type: "readOnly"
|
|
38
|
+
};
|
|
39
|
+
function assertApiRequestMcpTool() {
|
|
40
|
+
return (0, import_tool.toMcpTool)(assertApiRequestSchema);
|
|
41
|
+
}
|
|
42
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
43
|
+
0 && (module.exports = {
|
|
44
|
+
assertApiRequestMcpTool,
|
|
45
|
+
assertApiRequestSchema
|
|
46
|
+
});
|
|
@@ -43,6 +43,7 @@ var import_log = require("../log");
|
|
|
43
43
|
var import_skyRampExport = require("../test/skyRampExport");
|
|
44
44
|
var import_exportTool = require("./exportTool");
|
|
45
45
|
var import_assertTool = require("./assertTool");
|
|
46
|
+
var import_assertApiRequestTool = require("./assertApiRequestTool");
|
|
46
47
|
var import_types = require("./types");
|
|
47
48
|
const traceDebug = (0, import_utilsBundle.debug)("pw:mcp:trace");
|
|
48
49
|
class TraceRecordingBackend {
|
|
@@ -115,7 +116,7 @@ class TraceRecordingBackend {
|
|
|
115
116
|
}
|
|
116
117
|
async listTools() {
|
|
117
118
|
const browserTools = await this._browserBackend.listTools();
|
|
118
|
-
return [...browserTools, (0, import_exportTool.exportZipMcpTool)(), (0, import_assertTool.assertMcpTool)()];
|
|
119
|
+
return [...browserTools, (0, import_exportTool.exportZipMcpTool)(), (0, import_assertTool.assertMcpTool)(), (0, import_assertApiRequestTool.assertApiRequestMcpTool)()];
|
|
119
120
|
}
|
|
120
121
|
async callTool(name, args, progress) {
|
|
121
122
|
if (!this._initialized)
|
|
@@ -142,6 +143,19 @@ class TraceRecordingBackend {
|
|
|
142
143
|
const parsed = import_assertTool.assertToolSchema.inputSchema.parse(args || {});
|
|
143
144
|
return this._handleAssert(parsed);
|
|
144
145
|
}
|
|
146
|
+
if (name === import_assertApiRequestTool.assertApiRequestSchema.name) {
|
|
147
|
+
this._trackedActions.push({
|
|
148
|
+
toolName: "browser_assert_api_request",
|
|
149
|
+
args: {},
|
|
150
|
+
code: "",
|
|
151
|
+
timestamp: Date.now(),
|
|
152
|
+
pageAlias: this._currentPageAlias
|
|
153
|
+
});
|
|
154
|
+
traceDebug("Assert API request marker recorded");
|
|
155
|
+
return {
|
|
156
|
+
content: [{ type: "text", text: "### Result\nAPI request assertion marker recorded. The next action that triggers a network request will have its payload captured for validation in the generated test." }]
|
|
157
|
+
};
|
|
158
|
+
}
|
|
145
159
|
if (name === "browser_select_option") {
|
|
146
160
|
const result2 = await this._handleSelectOption(args || {});
|
|
147
161
|
return result2;
|
|
@@ -178,6 +192,7 @@ class TraceRecordingBackend {
|
|
|
178
192
|
timestamp: ts,
|
|
179
193
|
pageAlias: this._currentPageAlias
|
|
180
194
|
});
|
|
195
|
+
await this._enableFlutterAccessibility();
|
|
181
196
|
const snapResult = await this._browserBackend.callTool("browser_snapshot", {});
|
|
182
197
|
return {
|
|
183
198
|
content: [
|
|
@@ -191,17 +206,88 @@ Reloaded current page: ${currentUrl}
|
|
|
191
206
|
}
|
|
192
207
|
const timestampBeforeAction = Date.now();
|
|
193
208
|
const pageAliasBeforeAction = this._currentPageAlias;
|
|
209
|
+
const stripRestore = await this._maybeStripWrapperRoles(name, args);
|
|
194
210
|
let preClickHoverCode = null;
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (
|
|
201
|
-
|
|
211
|
+
let preClickSnapshot = null;
|
|
212
|
+
let preClickLocatorCount = 0;
|
|
213
|
+
let preClickParentChain = null;
|
|
214
|
+
let result;
|
|
215
|
+
try {
|
|
216
|
+
if (name === "browser_click" && args?.ref) {
|
|
217
|
+
const hoverResult = await this._browserBackend.callTool("browser_hover", {
|
|
218
|
+
element: args.element || "element",
|
|
219
|
+
ref: args.ref
|
|
220
|
+
});
|
|
221
|
+
if (!hoverResult.isError) {
|
|
222
|
+
const parsed = (0, import_response.parseResponse)(hoverResult);
|
|
223
|
+
preClickHoverCode = parsed?.code ?? null;
|
|
224
|
+
preClickSnapshot = parsed?.snapshot ?? null;
|
|
225
|
+
if (preClickHoverCode) {
|
|
226
|
+
const roleCountMatch = preClickHoverCode.match(/getByRole\(\s*['"](\w+)['"]\s*(?:,\s*\{[^}]*name:\s*(?:'([^']*)'|"([^"]*)")\s*[^}]*\})?\s*\)/);
|
|
227
|
+
if (roleCountMatch) {
|
|
228
|
+
try {
|
|
229
|
+
const page = this._browserBackend.context?.currentTab()?.page;
|
|
230
|
+
if (page) {
|
|
231
|
+
const r = roleCountMatch[1];
|
|
232
|
+
const n = roleCountMatch[2] ?? roleCountMatch[3];
|
|
233
|
+
preClickLocatorCount = n ? await page.getByRole(r, { name: n }).count() : await page.getByRole(r).count();
|
|
234
|
+
traceDebug(`[pre-click count] getByRole("${r}"${n ? `, { name: "${n}" }` : ""}) = ${preClickLocatorCount}`);
|
|
235
|
+
}
|
|
236
|
+
} catch {
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
const page = this._browserBackend.context?.currentTab()?.page;
|
|
242
|
+
if (page) {
|
|
243
|
+
preClickParentChain = await page.locator(`aria-ref=${args.ref}`).evaluate((el) => {
|
|
244
|
+
const ARIA_LANDMARKS = /* @__PURE__ */ new Set(["navigation", "main", "banner", "contentinfo", "complementary", "search", "form"]);
|
|
245
|
+
const SEMANTIC_TAGS_UNIQUE_CANDIDATES = ["nav", "main", "header", "footer", "aside"];
|
|
246
|
+
const SKIP_TESTID = /^(container|wrapper|content|main|root|app)$/i;
|
|
247
|
+
const SKIP_ID = /^(root|app|main|container|wrapper|content)$/i;
|
|
248
|
+
const isDynamic = (s) => /[-_]\d+$/.test(s) || /^[0-9a-f]{8,}/.test(s);
|
|
249
|
+
let cur = el.parentElement;
|
|
250
|
+
while (cur && cur !== el.ownerDocument.body) {
|
|
251
|
+
const tid = cur.getAttribute("data-testid") || cur.getAttribute("data-test-id");
|
|
252
|
+
if (tid && !isDynamic(tid) && !SKIP_TESTID.test(tid))
|
|
253
|
+
return `getByTestId(${JSON.stringify(tid)})`;
|
|
254
|
+
const id = cur.id;
|
|
255
|
+
if (id && !isDynamic(id) && !SKIP_ID.test(id)) {
|
|
256
|
+
const escapedId = id.replace(/(["\\])/g, "\\$1");
|
|
257
|
+
return `locator(${JSON.stringify("#" + escapedId)})`;
|
|
258
|
+
}
|
|
259
|
+
const role = cur.getAttribute("role");
|
|
260
|
+
if (role && ARIA_LANDMARKS.has(role)) {
|
|
261
|
+
const ariaLabel = cur.getAttribute("aria-label");
|
|
262
|
+
if (ariaLabel)
|
|
263
|
+
return `getByRole(${JSON.stringify(role)}, { name: ${JSON.stringify(ariaLabel)} })`;
|
|
264
|
+
if (el.ownerDocument.querySelectorAll(`[role="${role}"]`).length === 1)
|
|
265
|
+
return `getByRole(${JSON.stringify(role)})`;
|
|
266
|
+
}
|
|
267
|
+
const tag = cur.tagName.toLowerCase();
|
|
268
|
+
if (SEMANTIC_TAGS_UNIQUE_CANDIDATES.includes(tag)) {
|
|
269
|
+
if (el.ownerDocument.querySelectorAll(tag).length === 1)
|
|
270
|
+
return `locator(${JSON.stringify(tag)})`;
|
|
271
|
+
}
|
|
272
|
+
cur = cur.parentElement;
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
}).catch(() => null);
|
|
276
|
+
traceDebug(`[pre-click parent chain] ${preClickParentChain}`);
|
|
277
|
+
}
|
|
278
|
+
} catch {
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
result = await this._browserBackend.callTool(name, args);
|
|
283
|
+
} finally {
|
|
284
|
+
if (stripRestore)
|
|
285
|
+
await stripRestore();
|
|
286
|
+
}
|
|
287
|
+
if (name === "browser_navigate" && !result.isError) {
|
|
288
|
+
await this._enableFlutterAccessibility();
|
|
202
289
|
}
|
|
203
|
-
|
|
204
|
-
result = await this._improveActionSelector(name, args || {}, result, preClickHoverCode);
|
|
290
|
+
result = await this._improveActionSelector(name, args || {}, result, preClickHoverCode, preClickSnapshot, preClickLocatorCount, preClickParentChain);
|
|
205
291
|
if (name === "browser_fill_form" && result.isError) {
|
|
206
292
|
const resultText = result.content?.[0]?.type === "text" ? result.content[0].text : "";
|
|
207
293
|
if (resultText.includes("not a <select> element") || resultText.includes("selectOption") && resultText.includes("Timeout")) {
|
|
@@ -563,7 +649,7 @@ ${details}` }]
|
|
|
563
649
|
* preClickHoverCode: hover result taken BEFORE the click (needed for navigation clicks
|
|
564
650
|
* where the element ref is no longer valid after navigation).
|
|
565
651
|
*/
|
|
566
|
-
async _improveActionSelector(toolName, args, result, preClickHoverCode = null) {
|
|
652
|
+
async _improveActionSelector(toolName, args, result, preClickHoverCode = null, preClickSnapshot = null, preClickLocatorCount = 0, preClickParentChain = null) {
|
|
567
653
|
if (result.isError)
|
|
568
654
|
return result;
|
|
569
655
|
const selectorTools = /* @__PURE__ */ new Set(["browser_click", "browser_hover", "browser_type", "browser_fill"]);
|
|
@@ -606,43 +692,64 @@ ${details}` }]
|
|
|
606
692
|
}
|
|
607
693
|
return result;
|
|
608
694
|
}
|
|
609
|
-
|
|
610
|
-
if (!roleMatch)
|
|
695
|
+
if (code.includes(".filter(") || code.includes(".nth(") || code.includes(".first(") || code.includes(".last("))
|
|
611
696
|
return result;
|
|
612
|
-
|
|
697
|
+
if (/page\.(getByTestId|locator|getByRole)\([^)]*\)\.getByRole/.test(code))
|
|
698
|
+
return result;
|
|
699
|
+
const bareRoleMatch = code.match(/getByRole\(\s*['"](\w+)['"]\s*\)/);
|
|
700
|
+
const namedRoleMatch = code.match(/getByRole\(\s*['"](\w+)['"]\s*,\s*\{[^}]*name:\s*(?:'([^']*)'|"([^"]*)")/);
|
|
701
|
+
if (!bareRoleMatch && !namedRoleMatch)
|
|
702
|
+
return result;
|
|
703
|
+
const role = bareRoleMatch?.[1] ?? namedRoleMatch[1];
|
|
704
|
+
const roleName = namedRoleMatch ? namedRoleMatch[2] ?? namedRoleMatch[3] : void 0;
|
|
613
705
|
const ref = args.ref;
|
|
614
706
|
if (!ref)
|
|
615
707
|
return result;
|
|
616
|
-
const snapText = parsed?.snapshot
|
|
617
|
-
|
|
618
|
-
return result;
|
|
619
|
-
const snapLines = snapText.split("\n");
|
|
708
|
+
const snapText = preClickSnapshot || parsed?.snapshot || "";
|
|
709
|
+
const snapLines = (snapText || "").split("\n");
|
|
620
710
|
const rolePattern = new RegExp(`^\\s*-\\s*${role}\\s+`, "m");
|
|
621
|
-
|
|
622
|
-
if (
|
|
623
|
-
|
|
624
|
-
|
|
711
|
+
let snapshotMatchCount = 0;
|
|
712
|
+
if (snapText) {
|
|
713
|
+
if (roleName) {
|
|
714
|
+
const escapedName = roleName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
715
|
+
const namedPattern = new RegExp(`^\\s*-\\s*${role}\\s+"[^"]*${escapedName}`, "i");
|
|
716
|
+
snapshotMatchCount = snapLines.filter((l) => namedPattern.test(l)).length;
|
|
717
|
+
} else {
|
|
718
|
+
snapshotMatchCount = snapLines.filter((l) => rolePattern.test(l)).length;
|
|
719
|
+
}
|
|
625
720
|
}
|
|
626
|
-
|
|
721
|
+
const ambiguousCount = Math.max(preClickLocatorCount, snapshotMatchCount);
|
|
722
|
+
traceDebug(`[disambig] role="${role}" name="${roleName ?? ""}" preClickCount=${preClickLocatorCount} snapshotCount=${snapshotMatchCount} \u2192 ambiguousCount=${ambiguousCount}`);
|
|
723
|
+
traceDebug(`Role "${role}"${roleName ? ` name="${roleName}"` : ""} matches ${ambiguousCount} elements \u2014 attempting to disambiguate`);
|
|
627
724
|
const refPattern = `[ref=${ref}]`;
|
|
628
725
|
const refIdx = snapLines.findIndex((l) => l.includes(refPattern));
|
|
629
726
|
if (refIdx < 0)
|
|
630
727
|
return result;
|
|
631
728
|
const refLine = snapLines[refIdx];
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
729
|
+
if (!roleName && ambiguousCount > 1) {
|
|
730
|
+
const textMatch = refLine.match(/^\s*-\s*\w+\s+"([^"]*)"/);
|
|
731
|
+
if (textMatch && textMatch[1]) {
|
|
732
|
+
const visibleText = textMatch[1];
|
|
733
|
+
const textPattern = new RegExp(`^\\s*-\\s*${role}\\s+"[^"]*${visibleText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "i");
|
|
734
|
+
const textCount = snapLines.filter((l) => textPattern.test(l)).length;
|
|
735
|
+
if (textCount === 1) {
|
|
736
|
+
const improvedCode = code.replace(
|
|
737
|
+
/getByRole\((\s*['"](\w+)['"]\s*)\)/,
|
|
738
|
+
`getByRole($1).filter({ hasText: "${visibleText}" })`
|
|
739
|
+
);
|
|
740
|
+
traceDebug(`Improved selector with text filter: ${visibleText}`);
|
|
741
|
+
return this._modifyResultCode(result, improvedCode);
|
|
742
|
+
}
|
|
644
743
|
}
|
|
645
744
|
}
|
|
745
|
+
if (preClickParentChain) {
|
|
746
|
+
const improvedCode = code.replace(
|
|
747
|
+
/await\s+page\./,
|
|
748
|
+
`await page.${preClickParentChain}.`
|
|
749
|
+
);
|
|
750
|
+
traceDebug(`Improved selector by chaining from parent: ${preClickParentChain}`);
|
|
751
|
+
return this._modifyResultCode(result, improvedCode);
|
|
752
|
+
}
|
|
646
753
|
const refIndent = refLine.match(/^(\s*)/)?.[1]?.length ?? 0;
|
|
647
754
|
for (let i = refIdx - 1; i >= 0; i--) {
|
|
648
755
|
const line = snapLines[i];
|
|
@@ -667,7 +774,7 @@ ${details}` }]
|
|
|
667
774
|
/await\s+page\./,
|
|
668
775
|
`await page.getByTestId("${testid}").`
|
|
669
776
|
);
|
|
670
|
-
traceDebug(`Improved selector by chaining from test-id: ${testid}`);
|
|
777
|
+
traceDebug(`Improved selector by chaining from test-id (snapshot walk): ${testid}`);
|
|
671
778
|
return this._modifyResultCode(result, improvedCode);
|
|
672
779
|
}
|
|
673
780
|
}
|
|
@@ -676,22 +783,24 @@ ${details}` }]
|
|
|
676
783
|
if (indent === 0)
|
|
677
784
|
break;
|
|
678
785
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
786
|
+
if (ambiguousCount > 1) {
|
|
787
|
+
const roleRefs = [];
|
|
788
|
+
for (const line of snapLines) {
|
|
789
|
+
if (rolePattern.test(line)) {
|
|
790
|
+
const m = line.match(/\[ref=(\w+)\]/);
|
|
791
|
+
if (m)
|
|
792
|
+
roleRefs.push(m[1]);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
const index = roleRefs.indexOf(ref);
|
|
796
|
+
if (index >= 0) {
|
|
797
|
+
const improvedCode = code.replace(
|
|
798
|
+
/getByRole\((\s*['"][^'"]+['"](?:\s*,\s*\{[^}]*\})?)\s*\)/,
|
|
799
|
+
`getByRole($1).nth(${index})`
|
|
800
|
+
);
|
|
801
|
+
traceDebug(`Improved selector with .nth(${index})`);
|
|
802
|
+
return this._modifyResultCode(result, improvedCode);
|
|
685
803
|
}
|
|
686
|
-
}
|
|
687
|
-
const index = roleRefs.indexOf(ref);
|
|
688
|
-
if (index >= 0) {
|
|
689
|
-
const improvedCode = code.replace(
|
|
690
|
-
/getByRole\((\s*['"](\w+)['"]\s*)\)/,
|
|
691
|
-
`getByRole($1).nth(${index})`
|
|
692
|
-
);
|
|
693
|
-
traceDebug(`Improved selector with .nth(${index})`);
|
|
694
|
-
return this._modifyResultCode(result, improvedCode);
|
|
695
804
|
}
|
|
696
805
|
return result;
|
|
697
806
|
}
|
|
@@ -779,6 +888,51 @@ ${details}` }]
|
|
|
779
888
|
});
|
|
780
889
|
});
|
|
781
890
|
}
|
|
891
|
+
/**
|
|
892
|
+
* SKYR-3728: detect a wrapper-button around args.ref and, if found, strip
|
|
893
|
+
* role="button"/role="link" from every wrapper in the document so that
|
|
894
|
+
* upstream's closestCrossShadow can't retarget UP during selector
|
|
895
|
+
* regeneration. Returns a restore function the caller MUST invoke (via
|
|
896
|
+
* try/finally) once the click has been recorded.
|
|
897
|
+
*
|
|
898
|
+
* Works for browser_click, browser_type, browser_fill, browser_hover —
|
|
899
|
+
* anything that goes through tab.refLocator → upstream generateSelector.
|
|
900
|
+
*/
|
|
901
|
+
async _maybeStripWrapperRoles(toolName, args) {
|
|
902
|
+
const interactiveTools = /* @__PURE__ */ new Set(["browser_click", "browser_type", "browser_fill", "browser_hover"]);
|
|
903
|
+
if (!interactiveTools.has(toolName))
|
|
904
|
+
return null;
|
|
905
|
+
const ref = args?.ref;
|
|
906
|
+
if (!ref)
|
|
907
|
+
return null;
|
|
908
|
+
const tab = this._browserBackend.context?.currentTab();
|
|
909
|
+
const page = tab?.page;
|
|
910
|
+
if (!page)
|
|
911
|
+
return null;
|
|
912
|
+
let stripped = false;
|
|
913
|
+
try {
|
|
914
|
+
const handle = await page.locator(`aria-ref=${ref}`).elementHandle({ timeout: 1e3 });
|
|
915
|
+
if (!handle)
|
|
916
|
+
return null;
|
|
917
|
+
try {
|
|
918
|
+
stripped = await page.evaluate(pageEvaluateStripWrapperRoles, handle);
|
|
919
|
+
} finally {
|
|
920
|
+
await handle.dispose();
|
|
921
|
+
}
|
|
922
|
+
} catch {
|
|
923
|
+
return null;
|
|
924
|
+
}
|
|
925
|
+
if (!stripped)
|
|
926
|
+
return null;
|
|
927
|
+
traceDebug(`Wrapper-leak detected for ref=${ref}; stripped role=button/link document-wide for the action`);
|
|
928
|
+
return async () => {
|
|
929
|
+
try {
|
|
930
|
+
await page.evaluate(pageEvaluateRestoreWrapperRoles);
|
|
931
|
+
traceDebug("Restored stripped roles");
|
|
932
|
+
} catch {
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
}
|
|
782
936
|
_maybeTrackAction(toolName, args, result, timestamp, pageAliasBeforeAction) {
|
|
783
937
|
if (result.isError)
|
|
784
938
|
return;
|
|
@@ -809,6 +963,54 @@ ${details}` }]
|
|
|
809
963
|
return url.replace(/\/$/, "");
|
|
810
964
|
}
|
|
811
965
|
}
|
|
966
|
+
/**
|
|
967
|
+
* Enable Flutter web accessibility mode by clicking the hidden
|
|
968
|
+
* flt-semantics-placeholder button. Flutter web apps render to canvas by
|
|
969
|
+
* default, making DOM automation impossible. This creates a parallel ARIA
|
|
970
|
+
* tree that Playwright can interact with.
|
|
971
|
+
*/
|
|
972
|
+
async _enableFlutterAccessibility() {
|
|
973
|
+
const page = this._browserBackend.context?.currentTab()?.page;
|
|
974
|
+
if (!page) return;
|
|
975
|
+
const url = page.url();
|
|
976
|
+
if (url.startsWith("chrome-extension://") || url.startsWith("about:") || url.startsWith("file://"))
|
|
977
|
+
return;
|
|
978
|
+
const alreadyEnabled = await page.evaluate(() => {
|
|
979
|
+
const host = document.querySelector("flt-semantics-host");
|
|
980
|
+
return host && host.children.length > 0;
|
|
981
|
+
}).catch(() => false);
|
|
982
|
+
if (alreadyEnabled) {
|
|
983
|
+
traceDebug("Flutter accessibility already enabled");
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
let isFlutter = await page.evaluate(
|
|
987
|
+
() => !!document.querySelector("flt-glass-pane, flutter-view, flt-semantics-placeholder")
|
|
988
|
+
).catch(() => false);
|
|
989
|
+
if (!isFlutter) {
|
|
990
|
+
const hasCanvas = await page.evaluate(() => !!document.querySelector("canvas")).catch(() => false);
|
|
991
|
+
if (hasCanvas) {
|
|
992
|
+
isFlutter = await page.waitForFunction(
|
|
993
|
+
() => !!document.querySelector("flt-glass-pane, flutter-view, flt-semantics-placeholder"),
|
|
994
|
+
{ timeout: 5e3 }
|
|
995
|
+
).then(() => true).catch(() => false);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
if (!isFlutter) return;
|
|
999
|
+
const placeholder = page.locator("flt-semantics-placeholder").first();
|
|
1000
|
+
const clicked = await placeholder.click({ force: true }).then(() => true).catch(() => false);
|
|
1001
|
+
if (!clicked) {
|
|
1002
|
+
traceDebug("Flutter detected but placeholder click failed");
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
const enabled = await page.waitForFunction(() => {
|
|
1006
|
+
const host = document.querySelector("flt-semantics-host");
|
|
1007
|
+
return host && host.children.length > 0;
|
|
1008
|
+
}, { timeout: 5e3 }).then(() => true).catch(() => false);
|
|
1009
|
+
if (enabled)
|
|
1010
|
+
traceDebug("Flutter accessibility mode enabled");
|
|
1011
|
+
else
|
|
1012
|
+
traceDebug("Flutter placeholder clicked but semantics host did not appear");
|
|
1013
|
+
}
|
|
812
1014
|
/** Clean up temp directory and optionally recreate it for next session. */
|
|
813
1015
|
_cleanupTempDir(recreate = false) {
|
|
814
1016
|
try {
|
|
@@ -838,6 +1040,51 @@ ${details}` }]
|
|
|
838
1040
|
this._cleanupTempDir(false);
|
|
839
1041
|
}
|
|
840
1042
|
}
|
|
1043
|
+
function pageEvaluateStripWrapperRoles(target) {
|
|
1044
|
+
const TOKEN = "__skyr_3728_stripped__";
|
|
1045
|
+
const w = window;
|
|
1046
|
+
if (w[TOKEN])
|
|
1047
|
+
return false;
|
|
1048
|
+
if (!target || !(target instanceof Element))
|
|
1049
|
+
return false;
|
|
1050
|
+
const labelSelector = 'input, textarea, select, label, [role="checkbox"], [role="radio"], [role="switch"], [role="textbox"], [role="combobox"]';
|
|
1051
|
+
let wrapper = null;
|
|
1052
|
+
for (let el = target; el; el = el.parentElement) {
|
|
1053
|
+
const role = el.getAttribute && el.getAttribute("role");
|
|
1054
|
+
if (role !== "button" && role !== "link")
|
|
1055
|
+
continue;
|
|
1056
|
+
if (el.children.length === 0)
|
|
1057
|
+
continue;
|
|
1058
|
+
if (!el.querySelector(labelSelector))
|
|
1059
|
+
continue;
|
|
1060
|
+
wrapper = el;
|
|
1061
|
+
break;
|
|
1062
|
+
}
|
|
1063
|
+
if (!wrapper)
|
|
1064
|
+
return false;
|
|
1065
|
+
const stripped = [];
|
|
1066
|
+
document.querySelectorAll('[role="button"], [role="link"]').forEach((el) => {
|
|
1067
|
+
if (el.children.length === 0)
|
|
1068
|
+
return;
|
|
1069
|
+
const role = el.getAttribute("role");
|
|
1070
|
+
stripped.push({ el, role });
|
|
1071
|
+
el.removeAttribute("role");
|
|
1072
|
+
});
|
|
1073
|
+
w[TOKEN] = stripped;
|
|
1074
|
+
return true;
|
|
1075
|
+
}
|
|
1076
|
+
function pageEvaluateRestoreWrapperRoles() {
|
|
1077
|
+
const TOKEN = "__skyr_3728_stripped__";
|
|
1078
|
+
const w = window;
|
|
1079
|
+
const stripped = w[TOKEN];
|
|
1080
|
+
if (!stripped)
|
|
1081
|
+
return;
|
|
1082
|
+
for (const { el, role } of stripped) {
|
|
1083
|
+
if (el.isConnected)
|
|
1084
|
+
el.setAttribute("role", role);
|
|
1085
|
+
}
|
|
1086
|
+
delete w[TOKEN];
|
|
1087
|
+
}
|
|
841
1088
|
function splitExtensionPaths(value) {
|
|
842
1089
|
if (!value)
|
|
843
1090
|
return void 0;
|
|
@@ -495,6 +495,11 @@ function buildJsonlContent(actions, browserName, harPath) {
|
|
|
495
495
|
}
|
|
496
496
|
continue;
|
|
497
497
|
}
|
|
498
|
+
if (action.toolName === "browser_assert_api_request") {
|
|
499
|
+
lines.push(JSON.stringify({ name: "assertApiRequest", signals: [], timestamp: String(action.timestamp), pageGuid, pageAlias: alias, framePath: DEFAULT_FRAME_PATH }));
|
|
500
|
+
actionCount++;
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
498
503
|
if ((action.toolName === "browser_type" || action.toolName === "browser_press_sequentially") && action.args.submit) {
|
|
499
504
|
const fillLine = trackedActionToJsonl(action, pageGuid, action.timestamp);
|
|
500
505
|
if (fillLine) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyramp/mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"main": "build/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./build/index.js",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@modelcontextprotocol/sdk": "^1.24.3",
|
|
57
57
|
"@playwright/test": "^1.55.0",
|
|
58
|
-
"@skyramp/skyramp": "1.3.
|
|
58
|
+
"@skyramp/skyramp": "1.3.26",
|
|
59
59
|
"dockerode": "^5.0.0",
|
|
60
60
|
"fast-glob": "^3.3.3",
|
|
61
61
|
"js-yaml": "^4.1.1",
|
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __export = (target, all) => {
|
|
9
|
-
for (var name in all)
|
|
10
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
-
};
|
|
12
|
-
var __copyProps = (to, from, except, desc) => {
|
|
13
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
-
for (let key of __getOwnPropNames(from))
|
|
15
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
-
}
|
|
18
|
-
return to;
|
|
19
|
-
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
-
var domAnalyzer_exports = {};
|
|
30
|
-
__export(domAnalyzer_exports, {
|
|
31
|
-
default: () => domAnalyzer_default
|
|
32
|
-
});
|
|
33
|
-
module.exports = __toCommonJS(domAnalyzer_exports);
|
|
34
|
-
var crypto = __toESM(require("crypto"));
|
|
35
|
-
var fs = __toESM(require("fs"));
|
|
36
|
-
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
|
|
37
|
-
var import_tool = require("./tool");
|
|
38
|
-
var import_crawler = require("../../../dom-analyzer/crawler");
|
|
39
|
-
var import_blueprint = require("../../../dom-analyzer/blueprint");
|
|
40
|
-
var import_serialization = require("../../../dom-analyzer/serialization");
|
|
41
|
-
function hashStorageState(path) {
|
|
42
|
-
if (!path)
|
|
43
|
-
return null;
|
|
44
|
-
try {
|
|
45
|
-
const contents = fs.readFileSync(path, "utf-8");
|
|
46
|
-
return crypto.createHash("sha256").update(contents).digest("hex").slice(0, 16);
|
|
47
|
-
} catch {
|
|
48
|
-
const pathHash = crypto.createHash("sha256").update(path).digest("hex").slice(0, 16);
|
|
49
|
-
return `unreadable:${pathHash}`;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
function cacheKey(entryUrl, storageStateHash, probeButtons) {
|
|
53
|
-
return `${entryUrl}::${storageStateHash ?? "no-auth"}::${probeButtons}`;
|
|
54
|
-
}
|
|
55
|
-
function humanDuration(ms) {
|
|
56
|
-
if (ms < 6e4)
|
|
57
|
-
return `${Math.round(ms / 1e3)}s`;
|
|
58
|
-
return `${Math.round(ms / 6e4)}m`;
|
|
59
|
-
}
|
|
60
|
-
const sitemapBuild = (0, import_tool.defineTool)({
|
|
61
|
-
capability: "core",
|
|
62
|
-
schema: {
|
|
63
|
-
name: "browser_sitemap_build",
|
|
64
|
-
title: "Build Sitemap (graph of PageBlueprints)",
|
|
65
|
-
description: [
|
|
66
|
-
"Crawl an application starting from a URL and build a Sitemap \u2014 a graph of PageBlueprints (one per URL)",
|
|
67
|
-
"connected by navigation edges. The Sitemap is cached in the session.",
|
|
68
|
-
"",
|
|
69
|
-
"Call once at the start of a session. Subsequent calls within the TTL (~30 minutes) reuse the cached",
|
|
70
|
-
"Sitemap unless `refresh: true` is passed. Use `browser_sitemap_query` to read already-crawled pages",
|
|
71
|
-
"rather than re-calling this tool.",
|
|
72
|
-
"",
|
|
73
|
-
"Depth defaults to 5, maxPages to 50 (the real bound in practice).",
|
|
74
|
-
"",
|
|
75
|
-
'\u26A0\uFE0F SAFETY: By default, `probeButtons: "immutable-only"` skips destructive-looking buttons',
|
|
76
|
-
"(Delete / Submit / Place Order / etc.) and buttons inside forms to prevent side effects during",
|
|
77
|
-
'crawling. Use `probeButtons: "all"` only against dev / staging environments \u2014 it will click every',
|
|
78
|
-
"unique button, which can submit forms, create records, send notifications, or mutate server state."
|
|
79
|
-
].join("\n"),
|
|
80
|
-
inputSchema: import_mcpBundle.z.object({
|
|
81
|
-
url: import_mcpBundle.z.string().describe("Entry URL to start crawling from"),
|
|
82
|
-
depth: import_mcpBundle.z.number().optional().default(5).describe("Max crawl depth (default: 5)"),
|
|
83
|
-
maxPages: import_mcpBundle.z.number().optional().default(50).describe("Max pages to visit (default: 50)"),
|
|
84
|
-
sameOriginOnly: import_mcpBundle.z.boolean().optional().default(true).describe("Only follow same-origin links (default: true)"),
|
|
85
|
-
probeButtons: import_mcpBundle.z.enum(["immutable-only", "all", "none"]).optional().default("immutable-only").describe('Button-probing safety mode. Default: immutable-only (safe; skips destructive-verb buttons and buttons inside forms). Use "all" only against dev/staging \u2014 probes every button. "none" disables button probing entirely.'),
|
|
86
|
-
playwrightStoragePath: import_mcpBundle.z.string().optional().describe("Path to a Playwright storageState.json file \u2014 cookies, localStorage, sessionStorage per origin. Use this to crawl apps behind a login."),
|
|
87
|
-
refresh: import_mcpBundle.z.boolean().optional().default(false).describe("Force a full re-crawl, bypassing the cache")
|
|
88
|
-
}),
|
|
89
|
-
type: "readOnly"
|
|
90
|
-
},
|
|
91
|
-
handle: async (context, params, response) => {
|
|
92
|
-
const { url, depth, maxPages, sameOriginOnly, probeButtons, playwrightStoragePath, refresh } = params;
|
|
93
|
-
const normalizedUrl = (0, import_serialization.normalizeUrl)(url);
|
|
94
|
-
const storageStateHash = hashStorageState(playwrightStoragePath);
|
|
95
|
-
const probeMode = probeButtons;
|
|
96
|
-
const key = cacheKey(normalizedUrl, storageStateHash, probeMode);
|
|
97
|
-
const cached = context.sitemapCache.get(key);
|
|
98
|
-
if (cached && !refresh) {
|
|
99
|
-
const age = Date.now() - new Date(cached.sitemap.cachedAt).getTime();
|
|
100
|
-
if (age < import_serialization.CRAWL_TTL_MS) {
|
|
101
|
-
const pageCount2 = Object.keys(cached.sitemap.pages).length;
|
|
102
|
-
response.addTextResult(
|
|
103
|
-
`Reusing cached Sitemap from ${humanDuration(age)} ago.
|
|
104
|
-
Entry: ${cached.sitemap.entryUrl}
|
|
105
|
-
Pages: ${pageCount2}
|
|
106
|
-
Edges: ${cached.sitemap.edges.length}
|
|
107
|
-
Storage state: ${storageStateHash ? `auth hash ${storageStateHash}` : "none"}
|
|
108
|
-
Probe mode: ${probeMode}
|
|
109
|
-
|
|
110
|
-
Use browser_sitemap_query to read any page. Pass refresh=true to recrawl.`
|
|
111
|
-
);
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
let sitemap;
|
|
116
|
-
if (depth > 0) {
|
|
117
|
-
sitemap = await (0, import_crawler.crawl)(url, {
|
|
118
|
-
depth,
|
|
119
|
-
maxPages,
|
|
120
|
-
sameOriginOnly,
|
|
121
|
-
probeButtons: probeMode,
|
|
122
|
-
playwrightStoragePath
|
|
123
|
-
});
|
|
124
|
-
} else {
|
|
125
|
-
sitemap = await (0, import_crawler.crawlSinglePage)(url, { playwrightStoragePath });
|
|
126
|
-
}
|
|
127
|
-
context.sitemapCache.set(key, { sitemap, storageStateHash, probeButtonsMode: probeMode });
|
|
128
|
-
const pageCount = Object.keys(sitemap.pages).length;
|
|
129
|
-
const pageList = Object.keys(sitemap.pages).map((u) => {
|
|
130
|
-
const bp = sitemap.pages[u];
|
|
131
|
-
const elCount = bp.sections.reduce((sum, s) => sum + s.elements.length + s.repeatingElements.length, 0);
|
|
132
|
-
return ` - ${u} (${elCount} elements across ${bp.sections.length} sections)`;
|
|
133
|
-
}).join("\n");
|
|
134
|
-
response.addTextResult(
|
|
135
|
-
`Sitemap built for ${sitemap.entryUrl}
|
|
136
|
-
Pages: ${pageCount}
|
|
137
|
-
Edges: ${sitemap.edges.length}
|
|
138
|
-
Storage state: ${storageStateHash ? `auth hash ${storageStateHash}` : "none"}
|
|
139
|
-
Probe mode: ${probeMode}
|
|
140
|
-
|
|
141
|
-
Pages:
|
|
142
|
-
${pageList}
|
|
143
|
-
|
|
144
|
-
Use browser_sitemap_query to read page blueprints, edges, or derived views (mapJson, outline).`
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
const sitemapQuery = (0, import_tool.defineTool)({
|
|
149
|
-
capability: "core",
|
|
150
|
-
schema: {
|
|
151
|
-
name: "browser_sitemap_query",
|
|
152
|
-
title: "Query the cached Sitemap",
|
|
153
|
-
description: [
|
|
154
|
-
"Query the Sitemap cached by browser_sitemap_build. Use this instead of re-calling sitemap_build.",
|
|
155
|
-
"",
|
|
156
|
-
"Modes:",
|
|
157
|
-
" page \u2014 returns the full canonical PageBlueprint for the given URL",
|
|
158
|
-
" (sections with enrichment, logical names, XPaths)",
|
|
159
|
-
" edges \u2014 returns navigation edges originating from the given URL",
|
|
160
|
-
" mapJson \u2014 returns the derived flat logicalName \u2192 xpath map for the URL",
|
|
161
|
-
" outline \u2014 returns the derived textual section-to-element hierarchy for the URL",
|
|
162
|
-
" overview \u2014 (when url is omitted) returns page list + edge list summary"
|
|
163
|
-
].join("\n"),
|
|
164
|
-
inputSchema: import_mcpBundle.z.object({
|
|
165
|
-
type: import_mcpBundle.z.enum(["page", "edges", "mapJson", "outline"]).optional().describe("Query mode. Omit with url to get overview."),
|
|
166
|
-
url: import_mcpBundle.z.string().optional().describe("Page URL to query. Omit to get overview across all pages.")
|
|
167
|
-
}),
|
|
168
|
-
type: "readOnly"
|
|
169
|
-
},
|
|
170
|
-
handle: async (context, params, response) => {
|
|
171
|
-
if (context.sitemapCache.size === 0) {
|
|
172
|
-
response.addError("No Sitemap available. Run browser_sitemap_build first.");
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
const latest = [...context.sitemapCache.values()].sort(
|
|
176
|
-
(a, b) => new Date(b.sitemap.cachedAt).getTime() - new Date(a.sitemap.cachedAt).getTime()
|
|
177
|
-
)[0];
|
|
178
|
-
const sitemap = latest.sitemap;
|
|
179
|
-
const { type, url } = params;
|
|
180
|
-
if (!url || !type) {
|
|
181
|
-
const overview = {
|
|
182
|
-
schemaVersion: sitemap.schemaVersion,
|
|
183
|
-
entryUrl: sitemap.entryUrl,
|
|
184
|
-
cachedAt: sitemap.cachedAt,
|
|
185
|
-
pages: Object.keys(sitemap.pages).map((u) => ({
|
|
186
|
-
url: u,
|
|
187
|
-
sections: sitemap.pages[u].sections.length,
|
|
188
|
-
elements: sitemap.pages[u].sections.reduce(
|
|
189
|
-
(s, sec) => s + sec.elements.length + sec.repeatingElements.length,
|
|
190
|
-
0
|
|
191
|
-
),
|
|
192
|
-
pageHash: sitemap.pages[u].pageHash
|
|
193
|
-
})),
|
|
194
|
-
edges: sitemap.edges
|
|
195
|
-
};
|
|
196
|
-
response.addTextResult(JSON.stringify(overview, null, 2));
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
const normalizedQueryUrl = (0, import_serialization.normalizeUrl)(url);
|
|
200
|
-
const blueprint2 = sitemap.pages[normalizedQueryUrl];
|
|
201
|
-
if (!blueprint2) {
|
|
202
|
-
response.addError(
|
|
203
|
-
`Page not found in Sitemap: ${normalizedQueryUrl}. Available pages: ${Object.keys(sitemap.pages).join(", ")}`
|
|
204
|
-
);
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
if (type === "page") {
|
|
208
|
-
response.addTextResult(JSON.stringify(blueprint2, null, 2));
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
if (type === "edges") {
|
|
212
|
-
const edges = sitemap.edges.filter((e) => e.from === normalizedQueryUrl);
|
|
213
|
-
response.addTextResult(JSON.stringify({ url: normalizedQueryUrl, edges }, null, 2));
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
if (type === "mapJson") {
|
|
217
|
-
response.addTextResult(JSON.stringify((0, import_blueprint.buildMap)(blueprint2), null, 2));
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
if (type === "outline") {
|
|
221
|
-
response.addTextResult((0, import_blueprint.buildOutline)(blueprint2));
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
});
|
|
226
|
-
const blueprint = (0, import_tool.defineTabTool)({
|
|
227
|
-
capability: "core",
|
|
228
|
-
schema: {
|
|
229
|
-
name: "browser_blueprint",
|
|
230
|
-
title: "Build PageBlueprint for the current page",
|
|
231
|
-
description: [
|
|
232
|
-
"Build a PageBlueprint for the currently loaded page. Returns the canonical `sections` tree",
|
|
233
|
-
"containing singular elements and repeating-element shapes, each carrying enrichment fields",
|
|
234
|
-
"(mutability, widgetType, framePath, shadowRoot, stableId, testId).",
|
|
235
|
-
"",
|
|
236
|
-
"Call this only when the DOM has changed since the last known-good blueprint \u2014 e.g. after a",
|
|
237
|
-
"modal opens, a form submits, a filter changes a list, or any mutable action. For pure navigation",
|
|
238
|
-
"to an already-crawled URL, reuse the Sitemap cache via browser_sitemap_query.",
|
|
239
|
-
"",
|
|
240
|
-
"Logical names are stable \u2014 use them in generated test code. Refs (from browser_snapshot) are",
|
|
241
|
-
"ephemeral \u2014 use them only for dispatching the next interaction, never in generated code."
|
|
242
|
-
].join("\n"),
|
|
243
|
-
inputSchema: import_mcpBundle.z.object({}),
|
|
244
|
-
type: "readOnly"
|
|
245
|
-
},
|
|
246
|
-
handle: async (tab, _params, response) => {
|
|
247
|
-
try {
|
|
248
|
-
const bp = await (0, import_blueprint.buildPageBlueprint)(tab.page);
|
|
249
|
-
response.addTextResult(JSON.stringify(bp, null, 2));
|
|
250
|
-
} catch (err) {
|
|
251
|
-
if (err instanceof import_blueprint.BlueprintInvariantError) {
|
|
252
|
-
response.addError(
|
|
253
|
-
`Blueprint invariant violated: ${err.message}. This is a builder bug; please file an issue with the URL of the page that triggered it.`
|
|
254
|
-
);
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
throw err;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
});
|
|
261
|
-
var domAnalyzer_default = [sitemapBuild, sitemapQuery, blueprint];
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|