@skyramp/mcp 0.0.64 → 0.1.0-rc.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/traceRecordingPrompt.js +30 -36
- package/build/prompts/architectPersona.js +19 -0
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +11 -6
- package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +49 -0
- package/build/prompts/test-maintenance/driftAnalysisSections.js +4 -2
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +32 -17
- package/build/prompts/testbot/testbot-prompts.js +87 -97
- package/build/prompts/testbot/testbot-prompts.test.js +142 -0
- package/build/services/ScenarioGenerationService.js +2 -2
- package/build/services/ScenarioGenerationService.test.js +35 -0
- package/build/services/TestExecutionService.js +1 -1
- package/build/services/TestGenerationService.js +1 -0
- package/build/tools/code-refactor/modularizationTool.js +2 -2
- package/build/tools/executeSkyrampTestTool.js +4 -3
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +49 -20
- package/build/tools/generate-tests/generateContractRestTool.js +26 -4
- package/build/tools/generate-tests/generateIntegrationRestTool.js +44 -13
- package/build/tools/generate-tests/generateMockRestTool.js +1 -0
- package/build/tools/generate-tests/generateScenarioRestTool.js +17 -39
- package/build/tools/generate-tests/generateUIRestTool.js +69 -4
- package/build/tools/submitReportTool.js +20 -14
- package/build/tools/test-management/analyzeChangesTool.js +8 -3
- package/build/tools/test-management/analyzeChangesTool.test.js +85 -0
- package/build/types/RepositoryAnalysis.js +2 -12
- package/build/types/TestRecommendation.js +43 -1
- package/build/types/TestTypes.js +20 -7
- package/build/utils/AnalysisStateManager.js +13 -5
- package/build/utils/AnalysisStateManager.test.js +35 -0
- package/node_modules/playwright/lib/mcp/browser/browserServerBackend.js +3 -0
- package/node_modules/playwright/lib/mcp/browser/tab.js +8 -1
- package/node_modules/playwright/lib/mcp/browser/tools/keyboard.js +3 -2
- package/node_modules/playwright/lib/mcp/browser/tools/navigate.js +1 -1
- package/node_modules/playwright/lib/mcp/browser/tools/snapshot.js +4 -4
- package/node_modules/playwright/lib/mcp/browser/tools/tabs.js +5 -4
- package/node_modules/playwright/lib/mcp/browser/tools/wait.js +1 -1
- package/node_modules/playwright/lib/mcp/skyramp/exportTool.js +10 -9
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +304 -7
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +128 -20
- package/package.json +2 -2
- package/node_modules/playwright/lib/mcp/terminal/help.json +0 -32
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Mock all heavy dependencies so the module can be loaded in isolation
|
|
2
|
+
jest.mock("@skyramp/skyramp", () => ({}));
|
|
3
|
+
jest.mock("simple-git", () => ({ simpleGit: jest.fn() }));
|
|
4
|
+
jest.mock("../../services/AnalyticsService.js", () => ({
|
|
5
|
+
AnalyticsService: { pushMCPToolEvent: jest.fn() },
|
|
6
|
+
}));
|
|
7
|
+
jest.mock("../../prompts/test-recommendation/test-recommendation-prompt.js", () => ({
|
|
8
|
+
buildRecommendationPrompt: jest.fn(),
|
|
9
|
+
}));
|
|
10
|
+
jest.mock("../../prompts/test-recommendation/recommendationSections.js", () => ({
|
|
11
|
+
MAX_RECOMMENDATIONS: 10,
|
|
12
|
+
MAX_TESTS_TO_GENERATE: 3,
|
|
13
|
+
}));
|
|
14
|
+
jest.mock("../../prompts/test-recommendation/analysisOutputPrompt.js", () => ({
|
|
15
|
+
buildAnalysisOutputText: jest.fn(),
|
|
16
|
+
}));
|
|
17
|
+
jest.mock("../../services/TestDiscoveryService.js", () => ({
|
|
18
|
+
TestDiscoveryService: jest.fn(),
|
|
19
|
+
}));
|
|
20
|
+
jest.mock("../../utils/branchDiff.js", () => ({
|
|
21
|
+
computeBranchDiff: jest.fn(),
|
|
22
|
+
}));
|
|
23
|
+
jest.mock("../../utils/routeParsers.js", () => ({
|
|
24
|
+
parseEndpointsFromDiff: jest.fn(),
|
|
25
|
+
}));
|
|
26
|
+
jest.mock("../../utils/repoScanner.js", () => ({
|
|
27
|
+
scanAllRepoEndpoints: jest.fn(),
|
|
28
|
+
scanRelatedEndpoints: jest.fn(),
|
|
29
|
+
grepRouterMountingContext: jest.fn(),
|
|
30
|
+
}));
|
|
31
|
+
jest.mock("../../utils/projectMetadata.js", () => ({
|
|
32
|
+
detectProjectMetadata: jest.fn(),
|
|
33
|
+
}));
|
|
34
|
+
jest.mock("../../utils/scenarioDrafting.js", () => ({
|
|
35
|
+
draftScenariosFromEndpoints: jest.fn(),
|
|
36
|
+
}));
|
|
37
|
+
jest.mock("../../utils/trace-parser.js", () => ({
|
|
38
|
+
parseTraceFile: jest.fn(),
|
|
39
|
+
discoverTraceFiles: jest.fn(),
|
|
40
|
+
discoverPlaywrightZips: jest.fn(),
|
|
41
|
+
}));
|
|
42
|
+
jest.mock("../../utils/pr-comment-parser.js", () => ({
|
|
43
|
+
parsePRComments: jest.fn(),
|
|
44
|
+
}));
|
|
45
|
+
jest.mock("../../utils/AnalysisStateManager.js", () => ({
|
|
46
|
+
StateManager: jest.fn(),
|
|
47
|
+
registerSession: jest.fn(),
|
|
48
|
+
storeSessionData: jest.fn(),
|
|
49
|
+
}));
|
|
50
|
+
jest.mock("../../utils/workspaceAuth.js", () => ({
|
|
51
|
+
parseWorkspaceAuthType: jest.fn(),
|
|
52
|
+
}));
|
|
53
|
+
jest.mock("../../utils/logger.js", () => ({
|
|
54
|
+
logger: { info: jest.fn(), debug: jest.fn(), error: jest.fn(), warn: jest.fn() },
|
|
55
|
+
}));
|
|
56
|
+
jest.mock("@modelcontextprotocol/sdk/server/mcp.js", () => ({
|
|
57
|
+
McpServer: jest.fn(),
|
|
58
|
+
}));
|
|
59
|
+
jest.mock("@modelcontextprotocol/sdk/types.js", () => ({}));
|
|
60
|
+
jest.mock("@modelcontextprotocol/sdk/shared/protocol.js", () => ({}));
|
|
61
|
+
import { z } from "zod";
|
|
62
|
+
import { analyzeChangesInputSchema } from "./analyzeChangesTool.js";
|
|
63
|
+
const schema = z.object(analyzeChangesInputSchema);
|
|
64
|
+
describe("analyzeChangesInputSchema — stateOutputFile validation", () => {
|
|
65
|
+
it("accepts a valid absolute path", () => {
|
|
66
|
+
const result = schema.safeParse({
|
|
67
|
+
repositoryPath: "/repo",
|
|
68
|
+
stateOutputFile: "/tmp/analyze-changes-state.json",
|
|
69
|
+
});
|
|
70
|
+
expect(result.success).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
it("rejects a relative path for stateOutputFile", () => {
|
|
73
|
+
// stateOutputFile must be absolute so the caller can guarantee the file location.
|
|
74
|
+
// Relative paths are silently ambiguous and should be rejected.
|
|
75
|
+
const result = schema.safeParse({
|
|
76
|
+
repositoryPath: "/repo",
|
|
77
|
+
stateOutputFile: "relative/path/state.json",
|
|
78
|
+
});
|
|
79
|
+
expect(result.success).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
it("accepts absence of stateOutputFile (optional field)", () => {
|
|
82
|
+
const result = schema.safeParse({ repositoryPath: "/repo" });
|
|
83
|
+
expect(result.success).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { SCENARIO_CATEGORIES } from "./TestRecommendation.js";
|
|
2
3
|
// ── Zod schemas ──
|
|
3
4
|
export const analysisScopeSchema = z.enum(["full_repo", "current_branch_diff"]);
|
|
4
5
|
export const paramInfoSchema = z.object({
|
|
@@ -76,18 +77,7 @@ export const scenarioStepSchema = z.object({
|
|
|
76
77
|
export const draftedScenarioSchema = z.object({
|
|
77
78
|
scenarioName: z.string(),
|
|
78
79
|
description: z.string(),
|
|
79
|
-
category: z.enum(
|
|
80
|
-
"crud",
|
|
81
|
-
"workflow",
|
|
82
|
-
"auth",
|
|
83
|
-
"error-handling",
|
|
84
|
-
"data-validation",
|
|
85
|
-
"security_boundary",
|
|
86
|
-
"business_rule",
|
|
87
|
-
"data_integrity",
|
|
88
|
-
"breaking_change",
|
|
89
|
-
"new_endpoint",
|
|
90
|
-
]),
|
|
80
|
+
category: z.enum(SCENARIO_CATEGORIES),
|
|
91
81
|
priority: z.enum(["high", "medium", "low"]),
|
|
92
82
|
steps: z.array(scenarioStepSchema),
|
|
93
83
|
chainingKeys: z.array(z.string()),
|
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { TestType } from "./TestTypes.js";
|
|
3
|
+
/** Internal-only categories (not submitted to tools). */
|
|
4
|
+
const INTERNAL_CATEGORIES = [
|
|
5
|
+
"new_endpoint", // CRITICAL - diff-direct scenarios always fill GENERATE slots first
|
|
6
|
+
];
|
|
7
|
+
/** External categories valid for tool submissions, ordered by priority. */
|
|
8
|
+
const CATEGORIES = [
|
|
9
|
+
// HIGH priority
|
|
10
|
+
"security_boundary", // auth, permission, cross-user isolation, idempotency
|
|
11
|
+
"business_rule", // unique constraints, range validation, state machines
|
|
12
|
+
"data_integrity", // cascade deletes, orphan prevention, referential integrity
|
|
13
|
+
"breaking_change", // route renames, auth migration, response shape changes
|
|
14
|
+
"auth", // authentication and authorization flows
|
|
15
|
+
// MEDIUM priority
|
|
16
|
+
"workflow", // cross-resource integration, user journeys
|
|
17
|
+
"error_handling", // error responses and edge cases
|
|
18
|
+
"data_validation", // input validation and schema enforcement
|
|
19
|
+
// LOW priority
|
|
20
|
+
"crud", // basic create/read/update/delete operations
|
|
21
|
+
];
|
|
22
|
+
/** All categories including internal ones. */
|
|
23
|
+
export const SCENARIO_CATEGORIES = [...INTERNAL_CATEGORIES, ...CATEGORIES];
|
|
24
|
+
/** Categories valid for tool submissions (excludes internal-only categories). */
|
|
25
|
+
export const TEST_CATEGORIES = CATEGORIES;
|
|
26
|
+
/** Priority assignment for each category. */
|
|
27
|
+
export const CATEGORY_PRIORITY = {
|
|
28
|
+
new_endpoint: "CRITICAL",
|
|
29
|
+
security_boundary: "HIGH",
|
|
30
|
+
business_rule: "HIGH",
|
|
31
|
+
data_integrity: "HIGH",
|
|
32
|
+
breaking_change: "HIGH",
|
|
33
|
+
auth: "HIGH",
|
|
34
|
+
workflow: "MEDIUM",
|
|
35
|
+
error_handling: "MEDIUM",
|
|
36
|
+
data_validation: "MEDIUM",
|
|
37
|
+
crud: "LOW",
|
|
38
|
+
};
|
|
39
|
+
/** Map internal-only categories to their external equivalent for tool submission. */
|
|
40
|
+
export function externalCategory(cat) {
|
|
41
|
+
if (cat === "new_endpoint")
|
|
42
|
+
return "crud";
|
|
43
|
+
return cat;
|
|
44
|
+
}
|
|
3
45
|
// Test type to documentation URL mapping
|
|
4
46
|
export const TEST_TYPE_DOCS = {
|
|
5
47
|
[TestType.SMOKE]: "https://www.skyramp.dev/docs/smoke-tests",
|
|
@@ -33,7 +75,7 @@ export const specificTestSchema = z.object({
|
|
|
33
75
|
export const testTypeRecommendationSchema = z.object({
|
|
34
76
|
priority: z.enum(["high", "medium", "low"]),
|
|
35
77
|
testType: z.nativeEnum(TestType),
|
|
36
|
-
category: z.enum(
|
|
78
|
+
category: z.enum(TEST_CATEGORIES),
|
|
37
79
|
rationale: z.string(),
|
|
38
80
|
reasoning: z.string(),
|
|
39
81
|
specificTests: z.array(specificTestSchema),
|
package/build/types/TestTypes.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export const SESSION_STORAGE_FILENAME = "skyramp_session_storage.json";
|
|
3
3
|
export const AUTH_PLACEHOLDER_TOKEN = "SKYRAMP_PLACEHOLDER_TOKEN";
|
|
4
|
+
export var ProgrammingLanguage;
|
|
5
|
+
(function (ProgrammingLanguage) {
|
|
6
|
+
ProgrammingLanguage["PYTHON"] = "python";
|
|
7
|
+
ProgrammingLanguage["TYPESCRIPT"] = "typescript";
|
|
8
|
+
ProgrammingLanguage["JAVASCRIPT"] = "javascript";
|
|
9
|
+
ProgrammingLanguage["JAVA"] = "java";
|
|
10
|
+
})(ProgrammingLanguage || (ProgrammingLanguage = {}));
|
|
4
11
|
export var TestType;
|
|
5
12
|
(function (TestType) {
|
|
6
13
|
TestType["SMOKE"] = "smoke";
|
|
@@ -12,15 +19,17 @@ export var TestType;
|
|
|
12
19
|
TestType["UI"] = "ui";
|
|
13
20
|
TestType["MOCK"] = "mock";
|
|
14
21
|
})(TestType || (TestType = {}));
|
|
22
|
+
export var HttpMethod;
|
|
23
|
+
(function (HttpMethod) {
|
|
24
|
+
HttpMethod["GET"] = "GET";
|
|
25
|
+
HttpMethod["POST"] = "POST";
|
|
26
|
+
HttpMethod["PUT"] = "PUT";
|
|
27
|
+
HttpMethod["DELETE"] = "DELETE";
|
|
28
|
+
HttpMethod["PATCH"] = "PATCH";
|
|
29
|
+
})(HttpMethod || (HttpMethod = {}));
|
|
15
30
|
export const languageSchema = z.object({
|
|
16
31
|
language: z
|
|
17
|
-
.
|
|
18
|
-
.refine((val) => {
|
|
19
|
-
const validLanguages = ["python", "typescript", "javascript", "java"];
|
|
20
|
-
return validLanguages.includes(val.toLowerCase());
|
|
21
|
-
}, {
|
|
22
|
-
message: "Language must be one of: python, typescript, javascript, java",
|
|
23
|
-
})
|
|
32
|
+
.nativeEnum(ProgrammingLanguage)
|
|
24
33
|
.describe("Programming language for the generated test (default: python). Must be one of: python, typescript, javascript, java"),
|
|
25
34
|
framework: z
|
|
26
35
|
.string()
|
|
@@ -101,6 +110,10 @@ export const baseSchema = z.object({
|
|
|
101
110
|
.number()
|
|
102
111
|
.default(0)
|
|
103
112
|
.describe("Port number for the mock server"),
|
|
113
|
+
optionalFields: z
|
|
114
|
+
.boolean()
|
|
115
|
+
.default(false)
|
|
116
|
+
.describe("Whether to include optional fields in the generated test/mock"),
|
|
104
117
|
prompt: z.string().describe("The prompt user provided to generate the test"),
|
|
105
118
|
});
|
|
106
119
|
export const basePlaywrightSchema = z.object({
|
|
@@ -75,12 +75,17 @@ export class StateManager {
|
|
|
75
75
|
* @param sessionId Unique session identifier (defaults to UUID)
|
|
76
76
|
* @param stateDir Directory to store state files (defaults to /tmp)
|
|
77
77
|
*/
|
|
78
|
-
constructor(stateType = "analysis", sessionId, stateDir) {
|
|
78
|
+
constructor(stateType = "analysis", sessionId, stateDir, stateFilePath) {
|
|
79
79
|
this.stateType = stateType;
|
|
80
80
|
this.sessionId = sessionId || crypto.randomUUID();
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
if (stateFilePath) {
|
|
82
|
+
this.stateFile = stateFilePath;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const baseDir = stateDir || os.tmpdir();
|
|
86
|
+
const prefix = STATE_FILE_PREFIXES[stateType];
|
|
87
|
+
this.stateFile = path.join(baseDir, `${prefix}-${this.sessionId}.json`);
|
|
88
|
+
}
|
|
84
89
|
}
|
|
85
90
|
/**
|
|
86
91
|
* Create state manager from a sessionId (resolves the state file path internally)
|
|
@@ -104,7 +109,9 @@ export class StateManager {
|
|
|
104
109
|
break;
|
|
105
110
|
}
|
|
106
111
|
}
|
|
107
|
-
|
|
112
|
+
// Pass stateFilePath as the 4th arg so the constructor uses it directly
|
|
113
|
+
// instead of reconstructing a potentially-different path from the parsed parts.
|
|
114
|
+
return new StateManager(stateType, sessionId, stateDir, stateFilePath);
|
|
108
115
|
}
|
|
109
116
|
/**
|
|
110
117
|
* Read data from state file (excludes metadata)
|
|
@@ -164,6 +171,7 @@ export class StateManager {
|
|
|
164
171
|
step: options?.step,
|
|
165
172
|
},
|
|
166
173
|
};
|
|
174
|
+
await fs.promises.mkdir(path.dirname(this.stateFile), { recursive: true });
|
|
167
175
|
await fs.promises.writeFile(this.stateFile, JSON.stringify(state, null, 2), "utf-8");
|
|
168
176
|
logger.debug(`Wrote data to state file: ${this.stateFile}`);
|
|
169
177
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { StateManager } from "./AnalysisStateManager.js";
|
|
5
|
+
describe("StateManager.fromStatePath", () => {
|
|
6
|
+
it("preserves the exact supplied path for a standard-prefixed file", () => {
|
|
7
|
+
const stdPath = path.join(os.tmpdir(), "skyramp-analysis-some-uuid.json");
|
|
8
|
+
const manager = StateManager.fromStatePath(stdPath);
|
|
9
|
+
expect(manager.getStatePath()).toBe(stdPath);
|
|
10
|
+
});
|
|
11
|
+
it("preserves the exact supplied path for a custom filename like analyze-changes-state.json", () => {
|
|
12
|
+
// This is the filename testbot uses — it does NOT match any STATE_FILE_PREFIXES entry.
|
|
13
|
+
// fromStatePath must pass stateFilePath through to the constructor so the path is not rebuilt.
|
|
14
|
+
const customPath = path.join(os.tmpdir(), "analyze-changes-state.json");
|
|
15
|
+
const manager = StateManager.fromStatePath(customPath);
|
|
16
|
+
expect(manager.getStatePath()).toBe(customPath);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
describe("StateManager.writeData", () => {
|
|
20
|
+
it("creates parent directories when they do not exist", async () => {
|
|
21
|
+
const nestedDir = path.join(os.tmpdir(), `skyramp-test-mkdir-${Date.now()}`);
|
|
22
|
+
const nestedPath = path.join(nestedDir, "state.json");
|
|
23
|
+
// Directory must not exist before the test
|
|
24
|
+
expect(fs.existsSync(nestedDir)).toBe(false);
|
|
25
|
+
const manager = new StateManager("analysis", undefined, undefined, nestedPath);
|
|
26
|
+
await expect(manager.writeData({
|
|
27
|
+
existingTests: [],
|
|
28
|
+
analysisScope: "branch_diff",
|
|
29
|
+
newEndpoints: [],
|
|
30
|
+
})).resolves.not.toThrow();
|
|
31
|
+
expect(fs.existsSync(nestedPath)).toBe(true);
|
|
32
|
+
// cleanup
|
|
33
|
+
await fs.promises.rm(nestedDir, { recursive: true, force: true });
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -253,7 +253,14 @@ class Tab extends import_events.EventEmitter {
|
|
|
253
253
|
if (param.element)
|
|
254
254
|
locator = locator.describe(param.element);
|
|
255
255
|
const { resolvedSelector } = await locator._resolveSelector();
|
|
256
|
-
|
|
256
|
+
let fixedSelector = resolvedSelector;
|
|
257
|
+
if (!resolvedSelector.includes("internal:control=enter-frame") && /^(css=)?iframe\b[^>]*\s+>>\s+/.test(resolvedSelector)) {
|
|
258
|
+
fixedSelector = resolvedSelector.replace(
|
|
259
|
+
/^(css=)?(iframe\b[^>]*)\s+>>\s+/,
|
|
260
|
+
"css=$2 >> internal:control=enter-frame >> "
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
return { locator, resolved: (0, import_utils.asLocator)("javascript", fixedSelector) };
|
|
257
264
|
} catch (e) {
|
|
258
265
|
throw new Error(`Ref ${param.ref} not found in the current page snapshot. Try capturing new snapshot.`);
|
|
259
266
|
}
|
|
@@ -69,14 +69,15 @@ const pressSequentially = (0, import_tool.defineTabTool)({
|
|
|
69
69
|
const typeSchema = import_snapshot.elementSchema.extend({
|
|
70
70
|
text: import_mcpBundle.z.string().describe("Text to type into the element"),
|
|
71
71
|
submit: import_mcpBundle.z.boolean().optional().describe("Whether to submit entered text (press Enter after)"),
|
|
72
|
-
slowly: import_mcpBundle.z.boolean().optional().describe("
|
|
72
|
+
slowly: import_mcpBundle.z.boolean().optional().describe("DO NOT USE \u2014 causes silent failures in contenteditable and rich text editors. Use default fast fill instead."),
|
|
73
|
+
clear: import_mcpBundle.z.boolean().optional().describe("Ignored \u2014 browser_type always replaces existing content. Do not pass this parameter.")
|
|
73
74
|
});
|
|
74
75
|
const type = (0, import_tool.defineTabTool)({
|
|
75
76
|
capability: "core",
|
|
76
77
|
schema: {
|
|
77
78
|
name: "browser_type",
|
|
78
79
|
title: "Type text",
|
|
79
|
-
description: "Type text into editable element",
|
|
80
|
+
description: "Type text into an editable element. Auto-focuses and replaces existing content. NEVER call browser_click on the field first \u2014 clicking before typing injects extra network requests that corrupt the trace.",
|
|
80
81
|
inputSchema: typeSchema,
|
|
81
82
|
type: "input"
|
|
82
83
|
},
|
|
@@ -28,7 +28,7 @@ const navigate = (0, import_tool.defineTool)({
|
|
|
28
28
|
schema: {
|
|
29
29
|
name: "browser_navigate",
|
|
30
30
|
title: "Navigate to a URL",
|
|
31
|
-
description: "Navigate to a URL",
|
|
31
|
+
description: "Navigate to a URL. Prefer direct navigation to known URLs over clicking menus or carousels \u2014 menus may open unwanted popups and carousel items cause strict-mode violations. To reload the current page, navigate to the same URL \u2014 the backend converts this to page.reload(). After navigating to a folder where content was just created/edited, always call browser_wait_for with the file name before interacting with it.",
|
|
32
32
|
inputSchema: import_mcpBundle.z.object({
|
|
33
33
|
url: import_mcpBundle.z.string().describe("The URL to navigate to")
|
|
34
34
|
}),
|
|
@@ -30,7 +30,7 @@ const snapshot = (0, import_tool.defineTool)({
|
|
|
30
30
|
schema: {
|
|
31
31
|
name: "browser_snapshot",
|
|
32
32
|
title: "Page snapshot",
|
|
33
|
-
description: "Capture accessibility
|
|
33
|
+
description: "Capture the ARIA accessibility tree of the current page. Returns element refs required by all interaction tools (browser_click, browser_type, browser_hover, etc.). Call before any interaction and after every action that changes the page to get fresh refs. If any interaction tool fails with a stale ref error, call this first to refresh.",
|
|
34
34
|
inputSchema: import_mcpBundle.z.object({
|
|
35
35
|
filename: import_mcpBundle.z.string().optional().describe("Save snapshot to markdown file instead of returning it in the response.")
|
|
36
36
|
}),
|
|
@@ -55,7 +55,7 @@ const click = (0, import_tool.defineTabTool)({
|
|
|
55
55
|
schema: {
|
|
56
56
|
name: "browser_click",
|
|
57
57
|
title: "Click",
|
|
58
|
-
description: "
|
|
58
|
+
description: "Click an element on the page. Always click the actual interactive element (button, link, input) \u2014 never a container or wrapper div. NEVER click a text field before typing \u2014 browser_type auto-focuses; a prior click injects extra network requests that corrupt the trace. NEVER click a row or link in a file list to access contextual actions \u2014 use browser_hover on the row instead.",
|
|
59
59
|
inputSchema: clickSchema,
|
|
60
60
|
type: "input"
|
|
61
61
|
},
|
|
@@ -111,7 +111,7 @@ const hover = (0, import_tool.defineTabTool)({
|
|
|
111
111
|
schema: {
|
|
112
112
|
name: "browser_hover",
|
|
113
113
|
title: "Hover mouse",
|
|
114
|
-
description: "Hover over element on
|
|
114
|
+
description: "Hover over an element. Required pattern for contextual actions (More Options, Delete, Rename, \u22EF) on list/grid rows: (1) call browser_hover on the row element, (2) call browser_snapshot to reveal hover-only controls, (3) click the target button. NEVER click the row itself \u2014 that navigates into the item.",
|
|
115
115
|
inputSchema: elementSchema,
|
|
116
116
|
type: "input"
|
|
117
117
|
},
|
|
@@ -132,7 +132,7 @@ const selectOption = (0, import_tool.defineTabTool)({
|
|
|
132
132
|
schema: {
|
|
133
133
|
name: "browser_select_option",
|
|
134
134
|
title: "Select option",
|
|
135
|
-
description: "Select an option in a dropdown",
|
|
135
|
+
description: "Select an option in a native <select> dropdown only. For custom dropdowns (Radix, MUI, etc.) that appear as combobox in the snapshot, do NOT use this tool \u2014 instead: (1) click the combobox to open it, (2) call browser_snapshot to see the listbox options, (3) click the desired option.",
|
|
136
136
|
inputSchema: selectOptionSchema,
|
|
137
137
|
type: "input"
|
|
138
138
|
},
|
|
@@ -29,10 +29,10 @@ const browserTabs = (0, import_tool.defineTool)({
|
|
|
29
29
|
schema: {
|
|
30
30
|
name: "browser_tabs",
|
|
31
31
|
title: "Manage tabs",
|
|
32
|
-
description:
|
|
32
|
+
description: 'List, create, close, or switch to a browser tab. When a click opens a new tab, use action "select" with the tab index to switch to it. Do NOT call browser_navigate after switching \u2014 the tab is already on the right page.',
|
|
33
33
|
inputSchema: import_mcpBundle.z.object({
|
|
34
|
-
action: import_mcpBundle.z.enum(["list", "new", "close", "select"]).describe(
|
|
35
|
-
index: import_mcpBundle.z.number().optional().describe("Tab index, used for close/select. If omitted for close, current tab is closed.")
|
|
34
|
+
action: import_mcpBundle.z.enum(["list", "new", "close", "select", "switch"]).describe('Operation to perform. "select" and "switch" are equivalent \u2014 both switch to a tab by index.'),
|
|
35
|
+
index: import_mcpBundle.z.number().optional().describe("Tab index, used for close/select/switch. If omitted for close, current tab is closed.")
|
|
36
36
|
}),
|
|
37
37
|
type: "action"
|
|
38
38
|
},
|
|
@@ -50,7 +50,8 @@ const browserTabs = (0, import_tool.defineTool)({
|
|
|
50
50
|
await context.closeTab(params.index);
|
|
51
51
|
break;
|
|
52
52
|
}
|
|
53
|
-
case "select":
|
|
53
|
+
case "select":
|
|
54
|
+
case "switch": {
|
|
54
55
|
if (params.index === void 0)
|
|
55
56
|
throw new Error("Tab index is required");
|
|
56
57
|
await context.selectTab(params.index);
|
|
@@ -28,7 +28,7 @@ const wait = (0, import_tool.defineTool)({
|
|
|
28
28
|
schema: {
|
|
29
29
|
name: "browser_wait_for",
|
|
30
30
|
title: "Wait for",
|
|
31
|
-
description:
|
|
31
|
+
description: 'Wait for text to appear, disappear, or a time to pass. REQUIRED after navigating to a folder where content was just created or renamed \u2014 file/item names update asynchronously and will not be present immediately. Always call with text: "<filename>" before attempting to hover or click a newly created item.',
|
|
32
32
|
inputSchema: import_mcpBundle.z.object({
|
|
33
33
|
time: import_mcpBundle.z.number().optional().describe("The time to wait in seconds"),
|
|
34
34
|
text: import_mcpBundle.z.string().optional().describe("The text to wait for"),
|
|
@@ -41,26 +41,27 @@ const exportZipSchema = {
|
|
|
41
41
|
name: "skyramp_export_zip",
|
|
42
42
|
title: "Export Skyramp zip",
|
|
43
43
|
description: [
|
|
44
|
-
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"Only
|
|
48
|
-
"
|
|
44
|
+
"Export the recorded browser interactions as a Skyramp zip (JSONL + HAR).",
|
|
45
|
+
"Pass outputPath as the absolute path for the zip \u2014 use the same directory and base name as the test file, replacing .spec.ts with .zip.",
|
|
46
|
+
"BEFORE calling this tool, output a <thinking> block that: (1) lists every user-requested step and confirms it was completed, (2) confirms no step was skipped or hallucinated.",
|
|
47
|
+
"Only call this tool when all interactions are fully complete. Do NOT ask the user for confirmation \u2014 call it automatically.",
|
|
48
|
+
"Only the last complete attempt is exported \u2014 retries from the start URL are deduplicated automatically.",
|
|
49
|
+
"Do NOT reuse zip files from previous sessions \u2014 always record fresh."
|
|
49
50
|
].join(" "),
|
|
50
51
|
inputSchema: import_mcpBundle.z.object({
|
|
51
|
-
|
|
52
|
+
outputPath: import_mcpBundle.z.string().describe("Absolute path where the zip should be written, e.g. /path/to/box_notes.zip. Use the same directory and base name as the test file, replacing .spec.ts with .zip.")
|
|
52
53
|
}),
|
|
53
54
|
type: "readOnly"
|
|
54
55
|
};
|
|
55
56
|
function createExportZipHandler(ctx) {
|
|
56
|
-
return async (
|
|
57
|
+
return async (params) => {
|
|
57
58
|
if (!ctx.trackedActions.length) {
|
|
58
59
|
return {
|
|
59
|
-
content: [{ type: "text", text: "### Error\nNo browser
|
|
60
|
+
content: [{ type: "text", text: "### Error\nNo browser interactions recorded. At minimum, call browser_navigate to open the target URL, then use browser_click or browser_type to record at least one user action before exporting." }],
|
|
60
61
|
isError: true
|
|
61
62
|
};
|
|
62
63
|
}
|
|
63
|
-
const outputZip = import_path.default.
|
|
64
|
+
const outputZip = import_path.default.resolve(params.outputPath);
|
|
64
65
|
const { jsonl: jsonlContent, actionCount, skipped } = (0, import_skyRampExport.buildJsonlContent)(ctx.trackedActions, "chromium", ctx.harPath);
|
|
65
66
|
await (0, import_skyRampExport.writeSkyrampZip)(outputZip, jsonlContent, ctx.harPath);
|
|
66
67
|
const skippedNote = skipped.length ? `
|