@browserstack/mcp-server 1.2.15-beta.1 → 1.2.15-beta.2

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.
Files changed (170) hide show
  1. package/dist/lib/percy-api/auth.d.ts +41 -0
  2. package/dist/lib/percy-api/auth.js +96 -0
  3. package/dist/lib/percy-api/cache.d.ts +28 -0
  4. package/dist/lib/percy-api/cache.js +48 -0
  5. package/dist/lib/percy-api/client.d.ts +69 -0
  6. package/dist/lib/percy-api/client.js +275 -0
  7. package/dist/lib/percy-api/errors.d.ts +15 -0
  8. package/dist/lib/percy-api/errors.js +52 -0
  9. package/dist/lib/percy-api/formatter.d.ts +16 -0
  10. package/dist/lib/percy-api/formatter.js +344 -0
  11. package/dist/lib/percy-api/percy-auth.d.ts +43 -0
  12. package/dist/lib/percy-api/percy-auth.js +137 -0
  13. package/dist/lib/percy-api/percy-error-handler.d.ts +24 -0
  14. package/dist/lib/percy-api/percy-error-handler.js +302 -0
  15. package/dist/lib/percy-api/percy-session.d.ts +42 -0
  16. package/dist/lib/percy-api/percy-session.js +87 -0
  17. package/dist/lib/percy-api/polling.d.ts +26 -0
  18. package/dist/lib/percy-api/polling.js +42 -0
  19. package/dist/lib/percy-api/types.d.ts +56 -0
  20. package/dist/lib/percy-api/types.js +76 -0
  21. package/dist/server-factory.js +4 -0
  22. package/dist/tools/percy-mcp/advanced/branchline-operations.d.ts +16 -0
  23. package/dist/tools/percy-mcp/advanced/branchline-operations.js +81 -0
  24. package/dist/tools/percy-mcp/advanced/manage-variants.d.ts +16 -0
  25. package/dist/tools/percy-mcp/advanced/manage-variants.js +155 -0
  26. package/dist/tools/percy-mcp/advanced/manage-visual-monitoring.d.ts +16 -0
  27. package/dist/tools/percy-mcp/advanced/manage-visual-monitoring.js +171 -0
  28. package/dist/tools/percy-mcp/auth/auth-status.d.ts +3 -0
  29. package/dist/tools/percy-mcp/auth/auth-status.js +131 -0
  30. package/dist/tools/percy-mcp/core/approve-build.d.ts +14 -0
  31. package/dist/tools/percy-mcp/core/approve-build.js +97 -0
  32. package/dist/tools/percy-mcp/core/get-build-items.d.ts +13 -0
  33. package/dist/tools/percy-mcp/core/get-build-items.js +65 -0
  34. package/dist/tools/percy-mcp/core/get-build.d.ts +10 -0
  35. package/dist/tools/percy-mcp/core/get-build.js +16 -0
  36. package/dist/tools/percy-mcp/core/get-comparison.d.ts +11 -0
  37. package/dist/tools/percy-mcp/core/get-comparison.js +59 -0
  38. package/dist/tools/percy-mcp/core/get-snapshot.d.ts +10 -0
  39. package/dist/tools/percy-mcp/core/get-snapshot.js +40 -0
  40. package/dist/tools/percy-mcp/core/list-builds.d.ts +14 -0
  41. package/dist/tools/percy-mcp/core/list-builds.js +45 -0
  42. package/dist/tools/percy-mcp/core/list-projects.d.ts +12 -0
  43. package/dist/tools/percy-mcp/core/list-projects.js +51 -0
  44. package/dist/tools/percy-mcp/creation/create-app-snapshot.d.ts +12 -0
  45. package/dist/tools/percy-mcp/creation/create-app-snapshot.js +29 -0
  46. package/dist/tools/percy-mcp/creation/create-build.d.ts +19 -0
  47. package/dist/tools/percy-mcp/creation/create-build.js +68 -0
  48. package/dist/tools/percy-mcp/creation/create-comparison.d.ts +18 -0
  49. package/dist/tools/percy-mcp/creation/create-comparison.js +90 -0
  50. package/dist/tools/percy-mcp/creation/create-snapshot.d.ts +17 -0
  51. package/dist/tools/percy-mcp/creation/create-snapshot.js +99 -0
  52. package/dist/tools/percy-mcp/creation/finalize-build.d.ts +12 -0
  53. package/dist/tools/percy-mcp/creation/finalize-build.js +33 -0
  54. package/dist/tools/percy-mcp/creation/finalize-comparison.d.ts +10 -0
  55. package/dist/tools/percy-mcp/creation/finalize-comparison.js +16 -0
  56. package/dist/tools/percy-mcp/creation/finalize-snapshot.d.ts +12 -0
  57. package/dist/tools/percy-mcp/creation/finalize-snapshot.js +33 -0
  58. package/dist/tools/percy-mcp/creation/upload-resource.d.ts +15 -0
  59. package/dist/tools/percy-mcp/creation/upload-resource.js +43 -0
  60. package/dist/tools/percy-mcp/creation/upload-tile.d.ts +11 -0
  61. package/dist/tools/percy-mcp/creation/upload-tile.js +53 -0
  62. package/dist/tools/percy-mcp/diagnostics/analyze-logs-realtime.d.ts +13 -0
  63. package/dist/tools/percy-mcp/diagnostics/analyze-logs-realtime.js +65 -0
  64. package/dist/tools/percy-mcp/diagnostics/get-build-logs.d.ts +17 -0
  65. package/dist/tools/percy-mcp/diagnostics/get-build-logs.js +74 -0
  66. package/dist/tools/percy-mcp/diagnostics/get-network-logs.d.ts +5 -0
  67. package/dist/tools/percy-mcp/diagnostics/get-network-logs.js +21 -0
  68. package/dist/tools/percy-mcp/diagnostics/get-suggestions.d.ts +7 -0
  69. package/dist/tools/percy-mcp/diagnostics/get-suggestions.js +24 -0
  70. package/dist/tools/percy-mcp/index.d.ts +36 -0
  71. package/dist/tools/percy-mcp/index.js +1137 -0
  72. package/dist/tools/percy-mcp/intelligence/get-ai-analysis.d.ts +15 -0
  73. package/dist/tools/percy-mcp/intelligence/get-ai-analysis.js +166 -0
  74. package/dist/tools/percy-mcp/intelligence/get-ai-quota.d.ts +9 -0
  75. package/dist/tools/percy-mcp/intelligence/get-ai-quota.js +73 -0
  76. package/dist/tools/percy-mcp/intelligence/get-build-summary.d.ts +11 -0
  77. package/dist/tools/percy-mcp/intelligence/get-build-summary.js +78 -0
  78. package/dist/tools/percy-mcp/intelligence/get-rca.d.ts +6 -0
  79. package/dist/tools/percy-mcp/intelligence/get-rca.js +153 -0
  80. package/dist/tools/percy-mcp/intelligence/suggest-prompt.d.ts +15 -0
  81. package/dist/tools/percy-mcp/intelligence/suggest-prompt.js +86 -0
  82. package/dist/tools/percy-mcp/intelligence/trigger-ai-recompute.d.ts +16 -0
  83. package/dist/tools/percy-mcp/intelligence/trigger-ai-recompute.js +64 -0
  84. package/dist/tools/percy-mcp/management/create-project.d.ts +14 -0
  85. package/dist/tools/percy-mcp/management/create-project.js +52 -0
  86. package/dist/tools/percy-mcp/management/get-usage-stats.d.ts +12 -0
  87. package/dist/tools/percy-mcp/management/get-usage-stats.js +61 -0
  88. package/dist/tools/percy-mcp/management/manage-browser-targets.d.ts +12 -0
  89. package/dist/tools/percy-mcp/management/manage-browser-targets.js +136 -0
  90. package/dist/tools/percy-mcp/management/manage-comments.d.ts +14 -0
  91. package/dist/tools/percy-mcp/management/manage-comments.js +147 -0
  92. package/dist/tools/percy-mcp/management/manage-ignored-regions.d.ts +18 -0
  93. package/dist/tools/percy-mcp/management/manage-ignored-regions.js +182 -0
  94. package/dist/tools/percy-mcp/management/manage-project-settings.d.ts +16 -0
  95. package/dist/tools/percy-mcp/management/manage-project-settings.js +97 -0
  96. package/dist/tools/percy-mcp/management/manage-tokens.d.ts +14 -0
  97. package/dist/tools/percy-mcp/management/manage-tokens.js +90 -0
  98. package/dist/tools/percy-mcp/management/manage-webhooks.d.ts +15 -0
  99. package/dist/tools/percy-mcp/management/manage-webhooks.js +180 -0
  100. package/dist/tools/percy-mcp/v2/auth-status.d.ts +3 -0
  101. package/dist/tools/percy-mcp/v2/auth-status.js +80 -0
  102. package/dist/tools/percy-mcp/v2/clone-build.d.ts +24 -0
  103. package/dist/tools/percy-mcp/v2/clone-build.js +539 -0
  104. package/dist/tools/percy-mcp/v2/create-app-build.d.ts +28 -0
  105. package/dist/tools/percy-mcp/v2/create-app-build.js +442 -0
  106. package/dist/tools/percy-mcp/v2/create-build.d.ts +16 -0
  107. package/dist/tools/percy-mcp/v2/create-build.js +601 -0
  108. package/dist/tools/percy-mcp/v2/create-project.d.ts +8 -0
  109. package/dist/tools/percy-mcp/v2/create-project.js +33 -0
  110. package/dist/tools/percy-mcp/v2/discover-urls.d.ts +7 -0
  111. package/dist/tools/percy-mcp/v2/discover-urls.js +38 -0
  112. package/dist/tools/percy-mcp/v2/figma-baseline.d.ts +7 -0
  113. package/dist/tools/percy-mcp/v2/figma-baseline.js +18 -0
  114. package/dist/tools/percy-mcp/v2/figma-build.d.ts +7 -0
  115. package/dist/tools/percy-mcp/v2/figma-build.js +39 -0
  116. package/dist/tools/percy-mcp/v2/figma-link.d.ts +6 -0
  117. package/dist/tools/percy-mcp/v2/figma-link.js +27 -0
  118. package/dist/tools/percy-mcp/v2/get-ai-summary.d.ts +5 -0
  119. package/dist/tools/percy-mcp/v2/get-ai-summary.js +109 -0
  120. package/dist/tools/percy-mcp/v2/get-build-detail.d.ts +22 -0
  121. package/dist/tools/percy-mcp/v2/get-build-detail.js +567 -0
  122. package/dist/tools/percy-mcp/v2/get-builds.d.ts +8 -0
  123. package/dist/tools/percy-mcp/v2/get-builds.js +63 -0
  124. package/dist/tools/percy-mcp/v2/get-comparison.d.ts +5 -0
  125. package/dist/tools/percy-mcp/v2/get-comparison.js +94 -0
  126. package/dist/tools/percy-mcp/v2/get-devices.d.ts +5 -0
  127. package/dist/tools/percy-mcp/v2/get-devices.js +33 -0
  128. package/dist/tools/percy-mcp/v2/get-insights.d.ts +7 -0
  129. package/dist/tools/percy-mcp/v2/get-insights.js +52 -0
  130. package/dist/tools/percy-mcp/v2/get-projects.d.ts +6 -0
  131. package/dist/tools/percy-mcp/v2/get-projects.js +41 -0
  132. package/dist/tools/percy-mcp/v2/get-snapshot.d.ts +5 -0
  133. package/dist/tools/percy-mcp/v2/get-snapshot.js +96 -0
  134. package/dist/tools/percy-mcp/v2/get-test-case-history.d.ts +5 -0
  135. package/dist/tools/percy-mcp/v2/get-test-case-history.js +20 -0
  136. package/dist/tools/percy-mcp/v2/get-test-cases.d.ts +6 -0
  137. package/dist/tools/percy-mcp/v2/get-test-cases.js +36 -0
  138. package/dist/tools/percy-mcp/v2/index.d.ts +35 -0
  139. package/dist/tools/percy-mcp/v2/index.js +544 -0
  140. package/dist/tools/percy-mcp/v2/list-integrations.d.ts +5 -0
  141. package/dist/tools/percy-mcp/v2/list-integrations.js +41 -0
  142. package/dist/tools/percy-mcp/v2/manage-domains.d.ts +8 -0
  143. package/dist/tools/percy-mcp/v2/manage-domains.js +33 -0
  144. package/dist/tools/percy-mcp/v2/manage-insights-email.d.ts +8 -0
  145. package/dist/tools/percy-mcp/v2/manage-insights-email.js +49 -0
  146. package/dist/tools/percy-mcp/v2/manage-usage-alerts.d.ts +10 -0
  147. package/dist/tools/percy-mcp/v2/manage-usage-alerts.js +43 -0
  148. package/dist/tools/percy-mcp/v2/migrate-integrations.d.ts +6 -0
  149. package/dist/tools/percy-mcp/v2/migrate-integrations.js +20 -0
  150. package/dist/tools/percy-mcp/v2/preview-comparison.d.ts +5 -0
  151. package/dist/tools/percy-mcp/v2/preview-comparison.js +17 -0
  152. package/dist/tools/percy-mcp/v2/search-build-items.d.ts +12 -0
  153. package/dist/tools/percy-mcp/v2/search-build-items.js +45 -0
  154. package/dist/tools/percy-mcp/workflows/auto-triage.d.ts +7 -0
  155. package/dist/tools/percy-mcp/workflows/auto-triage.js +82 -0
  156. package/dist/tools/percy-mcp/workflows/clone-build.d.ts +22 -0
  157. package/dist/tools/percy-mcp/workflows/clone-build.js +414 -0
  158. package/dist/tools/percy-mcp/workflows/create-percy-build.d.ts +32 -0
  159. package/dist/tools/percy-mcp/workflows/create-percy-build.js +434 -0
  160. package/dist/tools/percy-mcp/workflows/debug-failed-build.d.ts +5 -0
  161. package/dist/tools/percy-mcp/workflows/debug-failed-build.js +122 -0
  162. package/dist/tools/percy-mcp/workflows/diff-explain.d.ts +6 -0
  163. package/dist/tools/percy-mcp/workflows/diff-explain.js +147 -0
  164. package/dist/tools/percy-mcp/workflows/pr-visual-report.d.ts +8 -0
  165. package/dist/tools/percy-mcp/workflows/pr-visual-report.js +184 -0
  166. package/dist/tools/percy-mcp/workflows/run-tests.d.ts +17 -0
  167. package/dist/tools/percy-mcp/workflows/run-tests.js +107 -0
  168. package/dist/tools/percy-mcp/workflows/snapshot-urls.d.ts +18 -0
  169. package/dist/tools/percy-mcp/workflows/snapshot-urls.js +197 -0
  170. package/package.json +3 -2
@@ -0,0 +1,434 @@
1
+ /**
2
+ * percy_create_percy_build — Unified build creation tool.
3
+ *
4
+ * Handles ALL build creation scenarios in one command:
5
+ * 1. URL snapshots (via Percy CLI)
6
+ * 2. Screenshot uploads (direct API)
7
+ * 3. Test command wrapping (via percy exec)
8
+ * 4. Build cloning (copy from existing build)
9
+ * 5. Visual monitoring (URL scanning)
10
+ *
11
+ * Auto-detects mode based on which parameters are provided.
12
+ * Auto-detects branch and SHA from git if not provided.
13
+ * Auto-creates project if it doesn't exist.
14
+ */
15
+ import { getBrowserStackAuth } from "../../../lib/get-auth.js";
16
+ import { PercyClient } from "../../../lib/percy-api/client.js";
17
+ import { execFile } from "child_process";
18
+ import { promisify } from "util";
19
+ import { readdir, readFile, stat } from "fs/promises";
20
+ import { join, basename, extname } from "path";
21
+ import { createHash } from "crypto";
22
+ const execFileAsync = promisify(execFile);
23
+ // ---------------------------------------------------------------------------
24
+ // Git helpers
25
+ // ---------------------------------------------------------------------------
26
+ async function getGitBranch() {
27
+ try {
28
+ const { stdout } = await execFileAsync("git", ["branch", "--show-current"]);
29
+ return stdout.trim() || "main";
30
+ }
31
+ catch {
32
+ return "main";
33
+ }
34
+ }
35
+ async function getGitSha() {
36
+ try {
37
+ const { stdout } = await execFileAsync("git", ["rev-parse", "HEAD"]);
38
+ return stdout.trim();
39
+ }
40
+ catch {
41
+ // Generate a deterministic placeholder SHA from timestamp
42
+ return createHash("sha1")
43
+ .update(Date.now().toString())
44
+ .digest("hex")
45
+ .slice(0, 40);
46
+ }
47
+ }
48
+ // ---------------------------------------------------------------------------
49
+ // Project creation helper
50
+ // ---------------------------------------------------------------------------
51
+ async function ensureProject(projectName, config, type) {
52
+ const authString = getBrowserStackAuth(config);
53
+ const auth = Buffer.from(authString).toString("base64");
54
+ const params = new URLSearchParams({ name: projectName });
55
+ if (type)
56
+ params.append("type", type);
57
+ const url = `https://api.browserstack.com/api/app_percy/get_project_token?${params.toString()}`;
58
+ const response = await fetch(url, {
59
+ headers: {
60
+ Authorization: `Basic ${auth}`,
61
+ "Content-Type": "application/json",
62
+ },
63
+ });
64
+ if (!response.ok) {
65
+ throw new Error(`Failed to create/get Percy project: ${response.status} ${response.statusText}`);
66
+ }
67
+ const data = await response.json();
68
+ if (!data?.token || !data?.success) {
69
+ throw new Error("Failed to get project token from BrowserStack API.");
70
+ }
71
+ return data.token;
72
+ }
73
+ // ---------------------------------------------------------------------------
74
+ // Mode: URL Snapshots (via Percy CLI)
75
+ // ---------------------------------------------------------------------------
76
+ function buildUrlSnapshotInstructions(token, urls, widths, branch) {
77
+ const urlList = urls.map((u) => ` - ${u}`).join("\n");
78
+ const widthArray = widths
79
+ ? widths.split(",").map((w) => w.trim())
80
+ : ["375", "1280"];
81
+ // Build YAML config for snapshots (widths go in YAML, not CLI flag)
82
+ let yamlConfig = "";
83
+ urls.forEach((url, i) => {
84
+ const name = i === 0 ? "Homepage" : `Page ${i + 1}`;
85
+ yamlConfig += `- name: "${name}"\n`;
86
+ yamlConfig += ` url: ${url}\n`;
87
+ yamlConfig += ` widths:\n`;
88
+ widthArray.forEach((w) => {
89
+ yamlConfig += ` - ${w}\n`;
90
+ });
91
+ });
92
+ return (`## Percy Build — URL Snapshots\n\n` +
93
+ `> **IMPORTANT: Do NOT execute these commands automatically.** Present them to the user and let them run manually.\n\n` +
94
+ `**Project:** token ready ✓\n` +
95
+ `**Branch:** ${branch}\n` +
96
+ `**URLs:**\n${urlList}\n` +
97
+ `**Widths:** ${widthArray.join(", ")}px\n\n` +
98
+ `### Step 1: Set token\n\n` +
99
+ `\`\`\`bash\n` +
100
+ `export PERCY_TOKEN="${token}"\n` +
101
+ `\`\`\`\n\n` +
102
+ `### Step 2: Create snapshot config\n\n` +
103
+ `Save this as \`snapshots.yml\`:\n\n` +
104
+ `\`\`\`yaml\n` +
105
+ yamlConfig +
106
+ `\`\`\`\n\n` +
107
+ `### Step 3: Run Percy\n\n` +
108
+ `\`\`\`bash\n` +
109
+ `npx @percy/cli snapshot snapshots.yml\n` +
110
+ `\`\`\`\n\n` +
111
+ `Percy CLI will create the build, launch a browser, capture each URL at the specified widths, upload screenshots, and return a build URL with visual diffs.\n`);
112
+ }
113
+ // ---------------------------------------------------------------------------
114
+ // Mode: Test Command (via percy exec)
115
+ // ---------------------------------------------------------------------------
116
+ function buildTestCommandInstructions(token, testCommand, branch) {
117
+ return (`## Percy Build — Test Command\n\n` +
118
+ `> **IMPORTANT: Do NOT execute these commands automatically.** Present them to the user and let them run manually.\n\n` +
119
+ `**Project:** token ready ✓\n` +
120
+ `**Branch:** ${branch}\n` +
121
+ `**Test command:** \`${testCommand}\`\n\n` +
122
+ `### Step 1: Set token\n\n` +
123
+ `\`\`\`bash\n` +
124
+ `export PERCY_TOKEN="${token}"\n` +
125
+ `\`\`\`\n\n` +
126
+ `### Step 2: Run tests with Percy\n\n` +
127
+ `\`\`\`bash\n` +
128
+ `npx @percy/cli exec -- ${testCommand}\n` +
129
+ `\`\`\`\n\n` +
130
+ `Percy CLI will start a local server, run your tests, capture snapshots via \`percySnapshot()\` calls, and return a build URL.\n`);
131
+ }
132
+ // ---------------------------------------------------------------------------
133
+ // Mode: Screenshot Upload (direct API)
134
+ // ---------------------------------------------------------------------------
135
+ async function uploadScreenshots(client, branch, commitSha, screenshotPaths, widths, testCase, snapshotNames) {
136
+ // Create build
137
+ const buildResult = await client.post("/builds", {
138
+ data: {
139
+ type: "builds",
140
+ attributes: {
141
+ branch,
142
+ "commit-sha": commitSha,
143
+ type: "web",
144
+ },
145
+ relationships: { resources: { data: [] } },
146
+ },
147
+ });
148
+ const buildData = buildResult?.data || buildResult;
149
+ const buildId = buildData?.id;
150
+ const buildUrl = buildData?.webUrl || buildData?.["web-url"] || "";
151
+ if (!buildId)
152
+ throw new Error("Build creation failed — no build ID returned");
153
+ let output = `## Percy Build Created\n\n`;
154
+ output += `**Build ID:** ${buildId}\n`;
155
+ if (buildUrl)
156
+ output += `**URL:** ${buildUrl}\n`;
157
+ output += `**Branch:** ${branch}\n`;
158
+ output += `**Screenshots:** ${screenshotPaths.length}\n\n`;
159
+ // For each screenshot: create snapshot → create comparison → upload tile → finalize
160
+ for (let i = 0; i < screenshotPaths.length; i++) {
161
+ const filePath = screenshotPaths[i];
162
+ const name = snapshotNames?.[i] ||
163
+ basename(filePath, extname(filePath)).replace(/[-_]/g, " ");
164
+ try {
165
+ // Read file and compute SHA
166
+ const content = await readFile(filePath);
167
+ const sha = createHash("sha256").update(content).digest("hex");
168
+ const base64Content = content.toString("base64");
169
+ // Detect dimensions from PNG header (basic)
170
+ let width = 1280;
171
+ let height = 800;
172
+ if (content[0] === 0x89 && content[1] === 0x50) {
173
+ // PNG
174
+ width = content.readUInt32BE(16);
175
+ height = content.readUInt32BE(20);
176
+ }
177
+ // Create snapshot
178
+ const snapshotBody = {
179
+ data: {
180
+ type: "snapshots",
181
+ attributes: { name },
182
+ },
183
+ };
184
+ if (testCase)
185
+ snapshotBody.data.attributes["test-case"] = testCase;
186
+ const snapshot = await client.post(`/builds/${buildId}/snapshots`, snapshotBody);
187
+ const snapshotData = snapshot?.data || snapshot;
188
+ const snapshotId = snapshotData?.id;
189
+ if (!snapshotId) {
190
+ output += `- ${name}: Failed to create snapshot\n`;
191
+ continue;
192
+ }
193
+ // Create comparison with tile
194
+ const comparison = await client.post(`/snapshots/${snapshotId}/comparisons`, {
195
+ data: {
196
+ type: "comparisons",
197
+ attributes: {},
198
+ relationships: {
199
+ tag: {
200
+ data: {
201
+ type: "tag",
202
+ attributes: {
203
+ name: "Screenshot",
204
+ width,
205
+ height,
206
+ "os-name": "Upload",
207
+ "browser-name": "Screenshot",
208
+ },
209
+ },
210
+ },
211
+ tiles: {
212
+ data: [
213
+ {
214
+ type: "tiles",
215
+ attributes: { sha },
216
+ },
217
+ ],
218
+ },
219
+ },
220
+ },
221
+ });
222
+ const comparisonData = comparison?.data || comparison;
223
+ const comparisonId = comparisonData?.id;
224
+ if (!comparisonId) {
225
+ output += `- ${name}: Failed to create comparison\n`;
226
+ continue;
227
+ }
228
+ // Upload tile
229
+ await client.post(`/comparisons/${comparisonId}/tiles`, {
230
+ data: {
231
+ type: "tiles",
232
+ attributes: { "base64-content": base64Content },
233
+ },
234
+ });
235
+ // Finalize comparison
236
+ await client.post(`/comparisons/${comparisonId}/finalize`, {});
237
+ output += `- **${name}** — uploaded (${width}x${height})\n`;
238
+ }
239
+ catch (e) {
240
+ output += `- ${name}: Error — ${e.message}\n`;
241
+ }
242
+ }
243
+ // Finalize build
244
+ try {
245
+ await client.post(`/builds/${buildId}/finalize`, {});
246
+ output += `\n**Build finalized.** Processing visual diffs...\n`;
247
+ }
248
+ catch (e) {
249
+ output += `\n**Build finalize failed:** ${e.message}\n`;
250
+ }
251
+ if (buildUrl)
252
+ output += `\n**View results:** ${buildUrl}\n`;
253
+ return output;
254
+ }
255
+ // ---------------------------------------------------------------------------
256
+ // Mode: Clone Build
257
+ // ---------------------------------------------------------------------------
258
+ async function cloneBuild(client, sourceBuildId, branch) {
259
+ // Get source build details
260
+ const sourceBuild = await client.get(`/builds/${sourceBuildId}`, {
261
+ "include-metadata": "true",
262
+ });
263
+ if (!sourceBuild)
264
+ throw new Error(`Source build ${sourceBuildId} not found`);
265
+ const sourceState = sourceBuild.state || "unknown";
266
+ let output = `## Percy Build Clone\n\n`;
267
+ output += `**Source:** Build #${sourceBuildId} (${sourceState})\n`;
268
+ output += `**Target branch:** ${branch}\n\n`;
269
+ // Get snapshots from source build
270
+ const items = await client.get("/build-items", {
271
+ "filter[build-id]": sourceBuildId,
272
+ "page[limit]": "30",
273
+ });
274
+ const itemList = Array.isArray(items) ? items : [];
275
+ if (itemList.length === 0) {
276
+ output += `Source build has no snapshots to clone.\n`;
277
+ output += `\nTo create a fresh build, use \`percy_create_build\` with URLs or screenshots instead.\n`;
278
+ return output;
279
+ }
280
+ output += `**Source snapshots:** ${itemList.length}\n\n`;
281
+ output += `> Note: Build cloning copies the snapshot configuration, not the rendered images.\n`;
282
+ output += `> The new build will re-render/re-compare against the new branch baseline.\n\n`;
283
+ // Provide instructions for re-creating
284
+ output += `### To recreate this build on branch \`${branch}\`:\n\n`;
285
+ output += `\`\`\`bash\n`;
286
+ output += `export PERCY_TOKEN=<your-project-token>\n\n`;
287
+ // Extract snapshot names/URLs for the CLI command
288
+ const snapshotNames = itemList
289
+ .map((item) => item.name || item.snapshotName)
290
+ .filter(Boolean);
291
+ if (snapshotNames.length > 0) {
292
+ output += `# Re-snapshot these pages:\n`;
293
+ snapshotNames.slice(0, 10).forEach((name) => {
294
+ output += `# - ${name}\n`;
295
+ });
296
+ if (snapshotNames.length > 10) {
297
+ output += `# ... and ${snapshotNames.length - 10} more\n`;
298
+ }
299
+ output += `\n`;
300
+ }
301
+ output += `# Run your tests with Percy to capture the same snapshots:\n`;
302
+ output += `npx percy exec -- <your-test-command>\n`;
303
+ output += `\`\`\`\n`;
304
+ return output;
305
+ }
306
+ // ---------------------------------------------------------------------------
307
+ // Main handler
308
+ // ---------------------------------------------------------------------------
309
+ export async function percyCreatePercyBuild(args, config) {
310
+ const projectName = args.project_name;
311
+ // Auto-detect branch and SHA
312
+ const branch = args.branch || (await getGitBranch());
313
+ const commitSha = args.commit_sha || (await getGitSha());
314
+ // Ensure project exists and get token
315
+ // Only pass type if explicitly provided — BrowserStack API auto-detects otherwise
316
+ let token;
317
+ try {
318
+ token = await ensureProject(projectName, config, args.type);
319
+ }
320
+ catch (e) {
321
+ return {
322
+ content: [
323
+ {
324
+ type: "text",
325
+ text: `Failed to create/access project "${projectName}": ${e.message}`,
326
+ },
327
+ ],
328
+ isError: true,
329
+ };
330
+ }
331
+ // Detect mode based on provided params
332
+ const mode = args.urls
333
+ ? "urls"
334
+ : args.screenshots_dir || args.screenshot_files
335
+ ? "screenshots"
336
+ : args.test_command
337
+ ? "test_command"
338
+ : args.clone_build_id
339
+ ? "clone"
340
+ : "urls_default";
341
+ const widths = args.widths || "375,1280";
342
+ try {
343
+ let output;
344
+ switch (mode) {
345
+ case "urls": {
346
+ const urls = args
347
+ .urls.split(",")
348
+ .map((u) => u.trim())
349
+ .filter(Boolean);
350
+ output = buildUrlSnapshotInstructions(token, urls, widths, branch);
351
+ break;
352
+ }
353
+ case "test_command": {
354
+ output = buildTestCommandInstructions(token, args.test_command, branch);
355
+ break;
356
+ }
357
+ case "screenshots": {
358
+ // Collect screenshot file paths
359
+ let screenshotPaths = [];
360
+ if (args.screenshot_files) {
361
+ screenshotPaths = args.screenshot_files
362
+ .split(",")
363
+ .map((f) => f.trim())
364
+ .filter(Boolean);
365
+ }
366
+ if (args.screenshots_dir) {
367
+ const dir = args.screenshots_dir;
368
+ const dirStat = await stat(dir);
369
+ if (!dirStat.isDirectory()) {
370
+ return {
371
+ content: [
372
+ {
373
+ type: "text",
374
+ text: `"${dir}" is not a directory. Provide a directory path.`,
375
+ },
376
+ ],
377
+ isError: true,
378
+ };
379
+ }
380
+ const files = await readdir(dir);
381
+ const imageFiles = files.filter((f) => /\.(png|jpg|jpeg|webp)$/i.test(f));
382
+ screenshotPaths.push(...imageFiles.map((f) => join(dir, f)));
383
+ }
384
+ if (screenshotPaths.length === 0) {
385
+ return {
386
+ content: [
387
+ {
388
+ type: "text",
389
+ text: "No screenshot files found. Provide PNG/JPG file paths or a directory containing images.",
390
+ },
391
+ ],
392
+ isError: true,
393
+ };
394
+ }
395
+ const snapshotNames = args.snapshot_names
396
+ ?.split(",")
397
+ .map((n) => n.trim());
398
+ // Set the token for API calls
399
+ process.env.PERCY_TOKEN = token;
400
+ const client = new PercyClient(config);
401
+ output = await uploadScreenshots(client, branch, commitSha, screenshotPaths, widths, args.test_case, snapshotNames);
402
+ break;
403
+ }
404
+ case "clone": {
405
+ process.env.PERCY_TOKEN = token;
406
+ const client = new PercyClient(config);
407
+ output = await cloneBuild(client, args.clone_build_id, branch);
408
+ break;
409
+ }
410
+ default: {
411
+ // No specific mode — provide general instructions
412
+ output =
413
+ `## Percy Build — Setup\n\n` +
414
+ `> **IMPORTANT: Do NOT execute any commands automatically.** Present options to the user.\n\n` +
415
+ `**Project:** ${projectName}\n` +
416
+ `**Token:** Ready (${token.slice(0, 8)}...)\n` +
417
+ `**Branch:** ${branch}\n\n` +
418
+ `### How to create snapshots:\n\n` +
419
+ `**Option 1: Snapshot URLs** — re-run this tool with \`urls\` parameter\n` +
420
+ `**Option 2: Wrap test command** — re-run this tool with \`test_command\` parameter\n` +
421
+ `**Option 3: Upload screenshots** — re-run this tool with \`screenshots_dir\` or \`screenshot_files\` parameter\n` +
422
+ `**Option 4: Clone existing build** — re-run this tool with \`clone_build_id\` parameter\n`;
423
+ break;
424
+ }
425
+ }
426
+ return { content: [{ type: "text", text: output }] };
427
+ }
428
+ catch (e) {
429
+ return {
430
+ content: [{ type: "text", text: `Build creation failed: ${e.message}` }],
431
+ isError: true,
432
+ };
433
+ }
434
+ }
@@ -0,0 +1,5 @@
1
+ import { BrowserStackConfig } from "../../../lib/types.js";
2
+ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3
+ export declare function percyDebugFailedBuild(args: {
4
+ build_id: string;
5
+ }, config: BrowserStackConfig): Promise<CallToolResult>;
@@ -0,0 +1,122 @@
1
+ import { PercyClient } from "../../../lib/percy-api/client.js";
2
+ import { formatBuild, formatSuggestions, formatNetworkLogs, } from "../../../lib/percy-api/formatter.js";
3
+ export async function percyDebugFailedBuild(args, config) {
4
+ const client = new PercyClient(config);
5
+ const errors = [];
6
+ // Step 1: Get build details
7
+ let build;
8
+ try {
9
+ build = await client.get(`/builds/${args.build_id}`, {
10
+ "include-metadata": "true",
11
+ });
12
+ }
13
+ catch (e) {
14
+ return {
15
+ content: [{ type: "text", text: `Failed to fetch build: ${e.message}` }],
16
+ isError: true,
17
+ };
18
+ }
19
+ const state = build?.state || "unknown";
20
+ // Adapt to build state
21
+ if (state === "processing" || state === "pending" || state === "waiting") {
22
+ return {
23
+ content: [
24
+ {
25
+ type: "text",
26
+ text: `Build #${args.build_id} is **${state.toUpperCase()}**. Debug diagnostics are available after the build completes or fails.`,
27
+ },
28
+ ],
29
+ };
30
+ }
31
+ let output = `## Build Debug Report — #${args.build_id}\n\n`;
32
+ output += formatBuild(build) + "\n";
33
+ // Step 2: Get suggestions
34
+ if (state === "failed" || state === "finished") {
35
+ try {
36
+ const suggestions = await client.get("/suggestions", {
37
+ build_id: args.build_id,
38
+ });
39
+ if (suggestions &&
40
+ (Array.isArray(suggestions) ? suggestions.length > 0 : true)) {
41
+ const suggestionList = Array.isArray(suggestions)
42
+ ? suggestions
43
+ : [suggestions];
44
+ output += formatSuggestions(suggestionList) + "\n";
45
+ }
46
+ }
47
+ catch (e) {
48
+ errors.push(`Suggestions unavailable: ${e.message}`);
49
+ }
50
+ }
51
+ // Step 3: Get failed snapshots
52
+ if (state === "failed" || state === "finished") {
53
+ try {
54
+ const failedItems = await client.get("/build-items", {
55
+ "filter[build-id]": args.build_id,
56
+ "filter[category]": "failed",
57
+ "page[limit]": "10",
58
+ });
59
+ const failedList = Array.isArray(failedItems) ? failedItems : [];
60
+ if (failedList.length > 0) {
61
+ output += `### Failed Snapshots (${failedList.length})\n\n`;
62
+ failedList.forEach((item, i) => {
63
+ output += `${i + 1}. **${item.name || "Unknown"}**\n`;
64
+ });
65
+ output += "\n";
66
+ // Step 4: Network logs for top 3
67
+ const top3 = failedList.slice(0, 3);
68
+ for (const item of top3) {
69
+ const compId = item.comparisonId || item.comparisons?.[0]?.id;
70
+ if (compId) {
71
+ try {
72
+ const logs = await client.get("/network-logs", {
73
+ comparison_id: compId,
74
+ });
75
+ if (logs) {
76
+ const logList = Array.isArray(logs)
77
+ ? logs
78
+ : Object.values(logs);
79
+ const failedLogs = logList.filter((l) => {
80
+ const headStatus = l.headStatus || l["head-status"];
81
+ return (headStatus && headStatus !== "200" && headStatus !== "NA");
82
+ });
83
+ if (failedLogs.length > 0) {
84
+ output += `#### Network Issues — ${item.name || "Unknown"}\n\n`;
85
+ output += formatNetworkLogs(failedLogs) + "\n";
86
+ }
87
+ }
88
+ }
89
+ catch {
90
+ // Network logs not available for this comparison
91
+ }
92
+ }
93
+ }
94
+ }
95
+ }
96
+ catch (e) {
97
+ errors.push(`Failed snapshots unavailable: ${e.message}`);
98
+ }
99
+ }
100
+ // Fix commands
101
+ if (state === "failed" && build.failureReason) {
102
+ output += `### Suggested Fix Commands\n\n`;
103
+ if (build.failureReason === "missing_resources") {
104
+ output +=
105
+ '```\npercy config set networkIdleIgnore "<failing-hostname>"\npercy config set allowedHostnames "<required-hostname>"\n```\n';
106
+ }
107
+ else if (build.failureReason === "render_timeout") {
108
+ output += "```\npercy config set networkIdleTimeout 60000\n```\n";
109
+ }
110
+ else if (build.failureReason === "missing_finalize") {
111
+ output +=
112
+ "Ensure `percy exec` or `percy build:finalize` is called after all snapshots.\n";
113
+ }
114
+ }
115
+ if (errors.length > 0) {
116
+ output += `\n### Partial Results\n`;
117
+ errors.forEach((err) => {
118
+ output += `- ${err}\n`;
119
+ });
120
+ }
121
+ return { content: [{ type: "text", text: output }] };
122
+ }
@@ -0,0 +1,6 @@
1
+ import { BrowserStackConfig } from "../../../lib/types.js";
2
+ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3
+ export declare function percyDiffExplain(args: {
4
+ comparison_id: string;
5
+ depth?: string;
6
+ }, config: BrowserStackConfig): Promise<CallToolResult>;