@browserstack/mcp-server 1.2.14 → 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 +4 -3
@@ -0,0 +1,442 @@
1
+ /**
2
+ * percy_create_app_build — Create an App Percy BYOS (Bring Your Own Screenshots) build.
3
+ *
4
+ * Two modes:
5
+ * 1. Sample mode (use_sample_data=true): auto-generates 3 devices × 2 screenshots
6
+ * using sharp. Zero setup — just provide a project name.
7
+ * 2. Custom mode (resources_dir): reads your own device folders with device.json + PNGs.
8
+ *
9
+ * Expected directory structure for custom mode:
10
+ * resources/
11
+ * iPhone_14_Pro/
12
+ * device.json ← { deviceName, osName, osVersion, orientation, deviceScreenSize }
13
+ * Home.png
14
+ * Settings.png
15
+ * Pixel_7/
16
+ * device.json
17
+ * Home.png
18
+ */
19
+ import { percyTokenPost, getOrCreateProjectToken, } from "../../../lib/percy-api/percy-auth.js";
20
+ import { setActiveProject, setActiveBuild, } from "../../../lib/percy-api/percy-session.js";
21
+ import { execFile } from "child_process";
22
+ import { promisify } from "util";
23
+ import { readdir, readFile, stat, writeFile, mkdir } from "fs/promises";
24
+ import { join, basename, extname } from "path";
25
+ import { tmpdir } from "os";
26
+ import { createHash } from "crypto";
27
+ import sharp from "sharp";
28
+ const execFileAsync = promisify(execFile);
29
+ // ── Built-in sample devices ─────────────────────────────────────────────────
30
+ const SAMPLE_DEVICES = [
31
+ {
32
+ folder: "iPhone_14_Pro",
33
+ config: {
34
+ deviceName: "iPhone 14 Pro",
35
+ osName: "iOS",
36
+ osVersion: "16",
37
+ orientation: "portrait",
38
+ deviceScreenSize: "1179x2556",
39
+ statusBarHeight: 132,
40
+ navBarHeight: 0,
41
+ },
42
+ screenshots: ["Home Screen", "Login Screen"],
43
+ background: { r: 230, g: 230, b: 250 }, // light lavender
44
+ },
45
+ {
46
+ folder: "Pixel_7",
47
+ config: {
48
+ deviceName: "Pixel 7",
49
+ osName: "Android",
50
+ osVersion: "13",
51
+ orientation: "portrait",
52
+ deviceScreenSize: "1080x2400",
53
+ statusBarHeight: 118,
54
+ navBarHeight: 63,
55
+ },
56
+ screenshots: ["Home Screen", "Login Screen"],
57
+ background: { r: 230, g: 250, b: 230 }, // light green
58
+ },
59
+ {
60
+ folder: "Samsung_Galaxy_S23",
61
+ config: {
62
+ deviceName: "Samsung Galaxy S23",
63
+ osName: "Android",
64
+ osVersion: "13",
65
+ orientation: "portrait",
66
+ deviceScreenSize: "1080x2340",
67
+ statusBarHeight: 110,
68
+ navBarHeight: 63,
69
+ },
70
+ screenshots: ["Home Screen", "Login Screen"],
71
+ background: { r: 250, g: 240, b: 230 }, // light peach
72
+ },
73
+ ];
74
+ // ── Helpers ─────────────────────────────────────────────────────────────────
75
+ async function getGitBranch() {
76
+ try {
77
+ return ((await execFileAsync("git", ["branch", "--show-current"])).stdout.trim() || "main");
78
+ }
79
+ catch {
80
+ return "main";
81
+ }
82
+ }
83
+ async function getGitSha() {
84
+ try {
85
+ return (await execFileAsync("git", ["rev-parse", "HEAD"])).stdout.trim();
86
+ }
87
+ catch {
88
+ return createHash("sha1").update(Date.now().toString()).digest("hex");
89
+ }
90
+ }
91
+ function parseDimensions(sizeStr) {
92
+ const match = sizeStr.match(/^(\d+)\s*[xX×]\s*(\d+)$/);
93
+ if (!match)
94
+ return null;
95
+ return [parseInt(match[1], 10), parseInt(match[2], 10)];
96
+ }
97
+ function readPngDimensions(buffer) {
98
+ if (buffer.length >= 24 &&
99
+ buffer[0] === 0x89 &&
100
+ buffer[1] === 0x50 &&
101
+ buffer[2] === 0x4e &&
102
+ buffer[3] === 0x47) {
103
+ return {
104
+ width: buffer.readUInt32BE(16),
105
+ height: buffer.readUInt32BE(20),
106
+ };
107
+ }
108
+ return null;
109
+ }
110
+ // ── Sample data generation ──────────────────────────────────────────────────
111
+ async function generateSampleResources() {
112
+ const ts = Date.now();
113
+ const tmpDir = join(tmpdir(), `percy-app-samples-${ts}`);
114
+ await mkdir(tmpDir, { recursive: true });
115
+ for (const device of SAMPLE_DEVICES) {
116
+ const deviceDir = join(tmpDir, device.folder);
117
+ await mkdir(deviceDir, { recursive: true });
118
+ // Write device.json
119
+ await writeFile(join(deviceDir, "device.json"), JSON.stringify(device.config, null, 2));
120
+ // Generate PNGs at correct dimensions
121
+ const dims = parseDimensions(device.config.deviceScreenSize);
122
+ const [width, height] = dims;
123
+ for (const name of device.screenshots) {
124
+ await sharp({
125
+ create: {
126
+ width,
127
+ height,
128
+ channels: 3,
129
+ background: device.background,
130
+ },
131
+ })
132
+ .png({ compressionLevel: 9 })
133
+ .toFile(join(deviceDir, `${name}.png`));
134
+ }
135
+ }
136
+ return tmpDir;
137
+ }
138
+ // ── Discovery: find device folders ──────────────────────────────────────────
139
+ async function discoverDevices(resourcesDir) {
140
+ const devices = [];
141
+ const errors = [];
142
+ let entries;
143
+ try {
144
+ entries = await readdir(resourcesDir);
145
+ }
146
+ catch (e) {
147
+ return {
148
+ devices: [],
149
+ errors: [`Cannot read "${resourcesDir}": ${e.message}`],
150
+ };
151
+ }
152
+ for (const entry of entries) {
153
+ const folderPath = join(resourcesDir, entry);
154
+ const folderStat = await stat(folderPath).catch(() => null);
155
+ if (!folderStat?.isDirectory())
156
+ continue;
157
+ // Must have device.json
158
+ const configPath = join(folderPath, "device.json");
159
+ const configExists = await stat(configPath).catch(() => null);
160
+ if (!configExists)
161
+ continue;
162
+ // Parse device.json
163
+ let deviceConfig;
164
+ try {
165
+ const raw = await readFile(configPath, "utf-8");
166
+ deviceConfig = JSON.parse(raw);
167
+ }
168
+ catch (e) {
169
+ errors.push(`${entry}: invalid device.json — ${e.message}`);
170
+ continue;
171
+ }
172
+ // Validate required fields
173
+ if (!deviceConfig.deviceName) {
174
+ errors.push(`${entry}: device.json missing "deviceName"`);
175
+ continue;
176
+ }
177
+ if (!deviceConfig.osName) {
178
+ errors.push(`${entry}: device.json missing "osName"`);
179
+ continue;
180
+ }
181
+ if (!deviceConfig.deviceScreenSize) {
182
+ errors.push(`${entry}: device.json missing "deviceScreenSize"`);
183
+ continue;
184
+ }
185
+ const dims = parseDimensions(deviceConfig.deviceScreenSize);
186
+ if (!dims) {
187
+ errors.push(`${entry}: invalid deviceScreenSize "${deviceConfig.deviceScreenSize}" — expected "WIDTHxHEIGHT"`);
188
+ continue;
189
+ }
190
+ // Find .png screenshots
191
+ const allFiles = await readdir(folderPath);
192
+ const screenshots = allFiles
193
+ .filter((f) => /\.png$/i.test(f))
194
+ .map((f) => join(folderPath, f));
195
+ if (screenshots.length === 0) {
196
+ errors.push(`${entry}: no .png files found`);
197
+ continue;
198
+ }
199
+ devices.push({
200
+ folder: entry,
201
+ config: deviceConfig,
202
+ screenshots,
203
+ width: dims[0],
204
+ height: dims[1],
205
+ });
206
+ }
207
+ return { devices, errors };
208
+ }
209
+ // ── Main handler ────────────────────────────────────────────────────────────
210
+ export async function percyCreateAppBuildV2(args, config) {
211
+ const branch = args.branch || (await getGitBranch());
212
+ const commitSha = await getGitSha();
213
+ const usingSamples = args.use_sample_data === true || !args.resources_dir;
214
+ // ── 1. Resolve resources directory ────────────────────────────────────────
215
+ let resourcesDir;
216
+ if (usingSamples) {
217
+ try {
218
+ resourcesDir = await generateSampleResources();
219
+ }
220
+ catch (e) {
221
+ return {
222
+ content: [
223
+ {
224
+ type: "text",
225
+ text: `Failed to generate sample data: ${e.message}\n\nMake sure \`sharp\` is installed: \`npm install sharp\``,
226
+ },
227
+ ],
228
+ isError: true,
229
+ };
230
+ }
231
+ }
232
+ else {
233
+ resourcesDir = args.resources_dir;
234
+ }
235
+ // ── 2. Get app project token ──────────────────────────────────────────────
236
+ let token;
237
+ try {
238
+ token = await getOrCreateProjectToken(args.project_name, config, "app");
239
+ setActiveProject({ name: args.project_name, token, type: "app" });
240
+ }
241
+ catch (e) {
242
+ return {
243
+ content: [
244
+ {
245
+ type: "text",
246
+ text: `Failed to access app project "${args.project_name}": ${e.message}\n\nMake sure the project exists or your BrowserStack credentials have permission to create app Percy projects.`,
247
+ },
248
+ ],
249
+ isError: true,
250
+ };
251
+ }
252
+ // ── 3. Discover devices & screenshots ─────────────────────────────────────
253
+ const { devices, errors: discoveryErrors } = await discoverDevices(resourcesDir);
254
+ if (devices.length === 0) {
255
+ let output = `## App Percy Build — No Valid Devices\n\n`;
256
+ output += `No device folders with valid device.json found in \`${resourcesDir}\`.\n\n`;
257
+ if (discoveryErrors.length > 0) {
258
+ output += `**Errors:**\n`;
259
+ for (const err of discoveryErrors) {
260
+ output += `- ${err}\n`;
261
+ }
262
+ }
263
+ output += `\n**Expected structure:**\n`;
264
+ output += `\`\`\`\nresources/\n iPhone_14_Pro/\n device.json\n Home.png\n Pixel_7/\n device.json\n Home.png\n\`\`\`\n`;
265
+ output += `\n**device.json format:**\n`;
266
+ output += `\`\`\`json\n{\n "deviceName": "iPhone 14 Pro",\n "osName": "iOS",\n "osVersion": "16",\n "orientation": "portrait",\n "deviceScreenSize": "1290x2796"\n}\n\`\`\`\n`;
267
+ return { content: [{ type: "text", text: output }], isError: true };
268
+ }
269
+ const totalScreenshots = devices.reduce((sum, d) => sum + d.screenshots.length, 0);
270
+ // ── 4. Create build ───────────────────────────────────────────────────────
271
+ let buildId;
272
+ let buildUrl;
273
+ try {
274
+ const buildResponse = await percyTokenPost("/builds", token, {
275
+ data: {
276
+ type: "builds",
277
+ attributes: { branch, "commit-sha": commitSha },
278
+ relationships: { resources: { data: [] } },
279
+ },
280
+ });
281
+ buildId = buildResponse?.data?.id;
282
+ buildUrl = buildResponse?.data?.attributes?.["web-url"] || "";
283
+ // Store in session
284
+ if (buildId) {
285
+ setActiveBuild({ id: buildId, url: buildUrl, branch });
286
+ }
287
+ if (!buildId) {
288
+ return {
289
+ content: [
290
+ {
291
+ type: "text",
292
+ text: "Failed to create app build — no build ID returned.",
293
+ },
294
+ ],
295
+ isError: true,
296
+ };
297
+ }
298
+ }
299
+ catch (e) {
300
+ return {
301
+ content: [
302
+ { type: "text", text: `Failed to create app build: ${e.message}` },
303
+ ],
304
+ isError: true,
305
+ };
306
+ }
307
+ // ── 5. Upload screenshots per device ──────────────────────────────────────
308
+ let output = `## App Percy Build — ${args.project_name}\n\n`;
309
+ if (usingSamples) {
310
+ output += `> Using built-in sample data (3 devices × 2 screenshots). Pass \`resources_dir\` for custom screenshots.\n\n`;
311
+ }
312
+ output += `| Field | Value |\n|---|---|\n`;
313
+ output += `| **Build ID** | ${buildId} |\n`;
314
+ output += `| **Project** | ${args.project_name} |\n`;
315
+ output += `| **Branch** | ${branch} |\n`;
316
+ output += `| **Devices** | ${devices.length} |\n`;
317
+ output += `| **Screenshots** | ${totalScreenshots} |\n`;
318
+ output += `| **Token** | \`${token.slice(0, 8)}...${token.slice(-4)}\` |\n`;
319
+ if (buildUrl)
320
+ output += `| **Build URL** | ${buildUrl} |\n`;
321
+ output += "\n";
322
+ if (discoveryErrors.length > 0) {
323
+ output += `**Skipped (validation errors):**\n`;
324
+ for (const err of discoveryErrors) {
325
+ output += `- ${err}\n`;
326
+ }
327
+ output += `\n`;
328
+ }
329
+ let uploaded = 0;
330
+ let failed = 0;
331
+ for (const device of devices) {
332
+ const dc = device.config;
333
+ output += `### ${dc.deviceName}`;
334
+ if (dc.osName)
335
+ output += ` (${dc.osName}${dc.osVersion ? ` ${dc.osVersion}` : ""})`;
336
+ if (dc.orientation)
337
+ output += ` — ${dc.orientation}`;
338
+ output += `\n`;
339
+ for (const screenshotPath of device.screenshots) {
340
+ const screenshotName = basename(screenshotPath, extname(screenshotPath)).replace(/[-_]/g, " ");
341
+ try {
342
+ const content = await readFile(screenshotPath);
343
+ const sha = createHash("sha256").update(content).digest("hex");
344
+ // Validate PNG dimensions match device config
345
+ const pngDims = readPngDimensions(content);
346
+ if (pngDims) {
347
+ if (pngDims.width !== device.width ||
348
+ pngDims.height !== device.height) {
349
+ output += `- ✗ **${screenshotName}** — dimension mismatch: image is ${pngDims.width}x${pngDims.height}, device.json expects ${device.width}x${device.height}\n`;
350
+ failed++;
351
+ continue;
352
+ }
353
+ }
354
+ const base64 = content.toString("base64");
355
+ // Create snapshot
356
+ const snapAttrs = { name: screenshotName };
357
+ if (args.test_case)
358
+ snapAttrs["test-case"] = args.test_case;
359
+ const snapRes = await percyTokenPost(`/builds/${buildId}/snapshots`, token, { data: { type: "snapshots", attributes: snapAttrs } });
360
+ const snapId = snapRes?.data?.id;
361
+ if (!snapId) {
362
+ output += `- ✗ **${screenshotName}** — snapshot creation failed\n`;
363
+ failed++;
364
+ continue;
365
+ }
366
+ // Create comparison with device tag
367
+ const compRes = await percyTokenPost(`/snapshots/${snapId}/comparisons`, token, {
368
+ data: {
369
+ attributes: {
370
+ "external-debug-url": null,
371
+ "dom-info-sha": null,
372
+ },
373
+ relationships: {
374
+ tag: {
375
+ data: {
376
+ attributes: {
377
+ name: dc.deviceName,
378
+ width: device.width,
379
+ height: device.height,
380
+ "os-name": dc.osName,
381
+ ...(dc.osVersion ? { "os-version": dc.osVersion } : {}),
382
+ orientation: dc.orientation || "portrait",
383
+ },
384
+ },
385
+ },
386
+ tiles: {
387
+ data: [
388
+ {
389
+ attributes: {
390
+ sha,
391
+ "status-bar-height": dc.statusBarHeight || 0,
392
+ "nav-bar-height": dc.navBarHeight || 0,
393
+ },
394
+ },
395
+ ],
396
+ },
397
+ },
398
+ },
399
+ });
400
+ const compId = compRes?.data?.id;
401
+ if (!compId) {
402
+ output += `- ✗ **${screenshotName}** — comparison creation failed\n`;
403
+ failed++;
404
+ continue;
405
+ }
406
+ // Upload tile
407
+ await percyTokenPost(`/comparisons/${compId}/tiles`, token, {
408
+ data: { attributes: { "base64-content": base64 } },
409
+ });
410
+ // Finalize comparison
411
+ await percyTokenPost(`/comparisons/${compId}/finalize`, token, {});
412
+ uploaded++;
413
+ output += `- ✓ **${screenshotName}** (${device.width}×${device.height})\n`;
414
+ }
415
+ catch (e) {
416
+ output += `- ✗ **${screenshotName}** — ${e.message}\n`;
417
+ failed++;
418
+ }
419
+ }
420
+ output += `\n`;
421
+ }
422
+ // ── 6. Finalize build ─────────────────────────────────────────────────────
423
+ try {
424
+ await percyTokenPost(`/builds/${buildId}/finalize`, token, {});
425
+ output += `---\n\n**Build finalized.** ${uploaded}/${totalScreenshots} snapshots uploaded`;
426
+ if (failed > 0)
427
+ output += `, ${failed} failed`;
428
+ output += `.\n`;
429
+ }
430
+ catch (e) {
431
+ output += `---\n\n**Finalize failed:** ${e.message}\n`;
432
+ }
433
+ if (buildUrl) {
434
+ output += `\n**View build:** ${buildUrl}\n`;
435
+ }
436
+ output += `\n### Next Steps\n\n`;
437
+ output += `- \`percy_get_build\` with build_id "${buildId}" — View build details\n`;
438
+ output += `- \`percy_get_build\` with build_id "${buildId}" and detail "snapshots" — List snapshots\n`;
439
+ output += `- \`percy_get_build\` with build_id "${buildId}" and detail "ai_summary" — AI analysis\n`;
440
+ output += `- \`percy_get_builds\` — List all builds for this project\n`;
441
+ return { content: [{ type: "text", text: output }] };
442
+ }
@@ -0,0 +1,16 @@
1
+ import { BrowserStackConfig } from "../../../lib/types.js";
2
+ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3
+ interface CreateBuildArgs {
4
+ project_name: string;
5
+ urls?: string;
6
+ screenshots_dir?: string;
7
+ screenshot_files?: string;
8
+ test_command?: string;
9
+ branch?: string;
10
+ widths?: string;
11
+ type?: string;
12
+ snapshot_names?: string;
13
+ test_case?: string;
14
+ }
15
+ export declare function percyCreateBuildV2(args: CreateBuildArgs, config: BrowserStackConfig): Promise<CallToolResult>;
16
+ export {};