@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,567 @@
1
+ /**
2
+ * percy_get_build — Unified build details tool.
3
+ *
4
+ * detail param routes to different views:
5
+ * - overview: status, stats, AI metrics, browsers, summary preview
6
+ * - ai_summary: full AI change descriptions with occurrences
7
+ * - changes: changed snapshots with diff ratios and bugs
8
+ * - rca: root cause analysis for a comparison
9
+ * - logs: failure diagnostics and suggestions
10
+ * - network: network request logs for a comparison
11
+ * - snapshots: all snapshots with review states
12
+ */
13
+ import { percyGet, percyPost } from "../../../lib/percy-api/percy-auth.js";
14
+ import { setActiveBuild } from "../../../lib/percy-api/percy-session.js";
15
+ export async function percyGetBuildDetail(args, config) {
16
+ const detail = args.detail || "overview";
17
+ switch (detail) {
18
+ case "overview":
19
+ return getOverview(args.build_id, config);
20
+ case "ai_summary":
21
+ return getAiSummary(args.build_id, config);
22
+ case "changes":
23
+ return getChanges(args.build_id, config);
24
+ case "rca":
25
+ return getRca(args, config);
26
+ case "logs":
27
+ return getLogs(args.build_id, config);
28
+ case "network":
29
+ return getNetwork(args, config);
30
+ case "snapshots":
31
+ return getSnapshots(args.build_id, config);
32
+ default:
33
+ return {
34
+ content: [
35
+ {
36
+ type: "text",
37
+ text: `Unknown detail: ${detail}. Use: overview, ai_summary, changes, rca, logs, network, snapshots.`,
38
+ },
39
+ ],
40
+ isError: true,
41
+ };
42
+ }
43
+ }
44
+ // ── Helper: parse build response ────────────────────────────────────────────
45
+ function parseBuild(response) {
46
+ const attrs = response?.data?.attributes || {};
47
+ const ai = attrs["ai-details"] || {};
48
+ const included = response?.included || [];
49
+ const rels = response?.data?.relationships || {};
50
+ // Parse browsers from unique-browsers-across-snapshots (more detailed)
51
+ const uniqueBrowsers = (attrs["unique-browsers-across-snapshots"] || []).map((b) => {
52
+ const bf = b.browser_family || {};
53
+ const os = b.operating_system || {};
54
+ const dp = b.device_pool || {};
55
+ return `${bf.name || "?"} ${b.version || ""} on ${os.name || "?"} ${os.version || ""} ${dp.name || ""}`.trim();
56
+ });
57
+ // Parse build summary
58
+ let summaryItems = [];
59
+ const summaryObj = included.find((i) => i.type === "build-summaries");
60
+ if (summaryObj?.attributes?.summary) {
61
+ const raw = summaryObj.attributes.summary;
62
+ try {
63
+ summaryItems = typeof raw === "string" ? JSON.parse(raw) : raw;
64
+ if (!Array.isArray(summaryItems))
65
+ summaryItems = [];
66
+ }
67
+ catch {
68
+ summaryItems = [];
69
+ }
70
+ }
71
+ // Parse commit
72
+ const commitObj = included.find((i) => i.type === "commits");
73
+ const commit = commitObj?.attributes || {};
74
+ // Base build
75
+ const baseBuildId = rels["base-build"]?.data?.id;
76
+ const hasAiData = (ai["total-comparisons-with-ai"] ?? 0) > 0 ||
77
+ (ai["total-potential-bugs"] ?? 0) > 0 ||
78
+ (ai["total-ai-visual-diffs"] ?? 0) > 0 ||
79
+ ai["summary-status"] === "ok";
80
+ return {
81
+ attrs,
82
+ ai,
83
+ included,
84
+ uniqueBrowsers,
85
+ summaryItems,
86
+ commit,
87
+ baseBuildId,
88
+ hasAiData,
89
+ };
90
+ }
91
+ // ── Overview ────────────────────────────────────────────────────────────────
92
+ async function getOverview(buildId, config) {
93
+ const response = await percyGet(`/builds/${buildId}`, config, {
94
+ include: "build-summary,browsers,commit",
95
+ });
96
+ const { attrs, ai, uniqueBrowsers, summaryItems, commit, baseBuildId, hasAiData, } = parseBuild(response);
97
+ const buildNum = attrs["build-number"] || buildId;
98
+ const webUrl = attrs["web-url"] || "";
99
+ // Store in session
100
+ setActiveBuild({
101
+ id: buildId,
102
+ number: buildNum?.toString(),
103
+ url: webUrl,
104
+ branch: attrs.branch,
105
+ });
106
+ let output = `## Percy Build #${buildNum} (ID: ${buildId})\n\n`;
107
+ // Status table
108
+ output += `| Field | Value |\n|---|---|\n`;
109
+ output += `| **Build ID** | ${buildId} |\n`;
110
+ output += `| **Build #** | ${buildNum} |\n`;
111
+ output += `| **State** | ${attrs.state || "?"} |\n`;
112
+ output += `| **Branch** | ${attrs.branch || "?"} |\n`;
113
+ output += `| **Review** | ${attrs["review-state"] || "—"} (${attrs["review-state-reason"] || ""}) |\n`;
114
+ output += `| **Type** | ${attrs.type || "?"} |\n`;
115
+ if (webUrl)
116
+ output += `| **Build URL** | ${webUrl} |\n`;
117
+ if (commit.sha)
118
+ output += `| **Commit** | ${commit.sha?.slice(0, 8)} — ${commit.message || "no message"} |\n`;
119
+ if (commit["author-name"])
120
+ output += `| **Author** | ${commit["author-name"]} |\n`;
121
+ if (baseBuildId)
122
+ output += `| **Base build** | #${baseBuildId} |\n`;
123
+ // Stats
124
+ output += `\n### Stats\n\n`;
125
+ output += `| Metric | Value |\n|---|---|\n`;
126
+ output += `| Snapshots | ${attrs["total-snapshots"] ?? "?"} |\n`;
127
+ output += `| Comparisons | ${attrs["total-comparisons"] ?? "?"} |\n`;
128
+ output += `| With diffs | ${attrs["total-comparisons-diff"] ?? "—"} |\n`;
129
+ output += `| Unreviewed | ${attrs["total-snapshots-unreviewed"] ?? "—"} |\n`;
130
+ output += `| Failed | ${attrs["failed-snapshots-count"] ?? 0} |\n`;
131
+ output += `| Comments | ${attrs["total-open-comments"] ?? 0} |\n`;
132
+ output += `| Issues | ${attrs["total-open-issues"] ?? 0} |\n`;
133
+ // AI metrics
134
+ if (hasAiData) {
135
+ output += `\n### AI Analysis\n\n`;
136
+ output += `| Metric | Value |\n|---|---|\n`;
137
+ output += `| Potential bugs | **${ai["total-potential-bugs"] ?? 0}** |\n`;
138
+ output += `| AI visual diffs | ${ai["total-ai-visual-diffs"] ?? 0} |\n`;
139
+ output += `| Diffs reduced | ${ai["total-diffs-reduced-capped"] ?? 0} filtered |\n`;
140
+ output += `| Comparisons analyzed | ${ai["total-comparisons-with-ai"] ?? 0} |\n`;
141
+ output += `| Jobs | ${ai["all-ai-jobs-completed"] ? "completed" : "in progress"} |\n`;
142
+ }
143
+ // Browsers
144
+ if (uniqueBrowsers.length > 0) {
145
+ output += `\n### Browsers (${uniqueBrowsers.length})\n\n`;
146
+ uniqueBrowsers.forEach((b) => {
147
+ output += `- ${b}\n`;
148
+ });
149
+ }
150
+ // AI Summary preview
151
+ if (summaryItems.length > 0) {
152
+ output += `\n### AI Summary (${summaryItems.length} changes)\n\n`;
153
+ summaryItems.slice(0, 3).forEach((item) => {
154
+ output += `- **${item.title}** (${item.occurrences} occurrences)\n`;
155
+ });
156
+ if (summaryItems.length > 3) {
157
+ output += `- ... and ${summaryItems.length - 3} more\n`;
158
+ }
159
+ output += `\nUse \`detail "ai_summary"\` for full details.\n`;
160
+ }
161
+ // Failure info
162
+ if (attrs["failure-reason"]) {
163
+ output += `\n### Failure\n\n`;
164
+ output += `**Reason:** ${attrs["failure-reason"]}\n`;
165
+ if (attrs["failure-details"])
166
+ output += `**Details:** ${attrs["failure-details"]}\n`;
167
+ const buckets = attrs["error-buckets"];
168
+ if (Array.isArray(buckets) && buckets.length > 0) {
169
+ output += `\n**Error categories:**\n`;
170
+ buckets.forEach((b) => {
171
+ output += `- ${b.bucket || b.name || "?"}: ${b.count ?? "?"} snapshot(s)\n`;
172
+ });
173
+ }
174
+ }
175
+ // Timing
176
+ if (attrs["created-at"]) {
177
+ output += `\n### Timing\n\n`;
178
+ output += `| | |\n|---|---|\n`;
179
+ output += `| Created | ${attrs["created-at"]} |\n`;
180
+ if (attrs["finished-at"])
181
+ output += `| Finished | ${attrs["finished-at"]} |\n`;
182
+ if (attrs["percy-processing-duration"])
183
+ output += `| Processing | ${attrs["percy-processing-duration"]}s |\n`;
184
+ if (attrs["build-processing-duration"])
185
+ output += `| Total | ${attrs["build-processing-duration"]}s |\n`;
186
+ }
187
+ // URL
188
+ if (attrs["web-url"])
189
+ output += `\n**View:** ${attrs["web-url"]}\n`;
190
+ // Available details
191
+ output += `\n### More Details\n\n`;
192
+ output += `| Command | Shows |\n|---|---|\n`;
193
+ output += `| \`detail "ai_summary"\` | Full AI change descriptions with occurrences |\n`;
194
+ output += `| \`detail "changes"\` | Changed snapshots with diff ratios |\n`;
195
+ output += `| \`detail "snapshots"\` | All snapshots with review states |\n`;
196
+ output += `| \`detail "logs"\` | Failure diagnostics and suggestions |\n`;
197
+ output += `| \`detail "rca"\` | Root cause analysis (needs comparison_id) |\n`;
198
+ output += `| \`detail "network"\` | Network logs (needs comparison_id) |\n`;
199
+ return { content: [{ type: "text", text: output }] };
200
+ }
201
+ // ── AI Summary ──────────────────────────────────────────────────────────────
202
+ async function getAiSummary(buildId, config) {
203
+ const response = await percyGet(`/builds/${buildId}`, config, {
204
+ include: "build-summary",
205
+ });
206
+ const { attrs, ai, summaryItems, hasAiData } = parseBuild(response);
207
+ const buildNum = attrs["build-number"] || buildId;
208
+ let output = `## Build #${buildNum} — AI Summary\n\n`;
209
+ if (!hasAiData) {
210
+ output += `No AI analysis data found for this build.\n`;
211
+ return { content: [{ type: "text", text: output }] };
212
+ }
213
+ // AI stats
214
+ output += `**${ai["total-potential-bugs"] ?? 0} potential bugs** · **${ai["total-ai-visual-diffs"] ?? 0} AI visual diffs** · **${ai["total-diffs-reduced-capped"] ?? 0} diffs filtered**\n\n`;
215
+ output += `${ai["total-comparisons-with-ai"] ?? 0} of ${attrs["total-comparisons"] ?? "?"} comparisons analyzed by AI.\n\n`;
216
+ // Summary items with full detail
217
+ if (summaryItems.length > 0) {
218
+ output += `### Changes (${summaryItems.length})\n\n`;
219
+ summaryItems.forEach((item, i) => {
220
+ output += `#### ${i + 1}. ${item.title}\n\n`;
221
+ output += `**Occurrences:** ${item.occurrences}\n`;
222
+ const snaps = item.snapshots || [];
223
+ if (snaps.length > 0) {
224
+ output += `**Affected snapshots:** ${snaps.length}\n`;
225
+ const totalComps = snaps.reduce((sum, s) => sum + (s.comparisons?.length || 0), 0);
226
+ output += `**Affected comparisons:** ${totalComps}\n`;
227
+ // Show snapshot IDs and comparison details
228
+ output += `\n| Snapshot | Comparisons | Dimensions |\n|---|---|---|\n`;
229
+ snaps.slice(0, 5).forEach((s) => {
230
+ const comps = s.comparisons || [];
231
+ const dims = comps
232
+ .map((c) => `${c.width || "?"}×${c.height || "?"}`)
233
+ .join(", ");
234
+ output += `| ${s.snapshot_id} | ${comps.length} | ${dims} |\n`;
235
+ });
236
+ if (snaps.length > 5) {
237
+ output += `| ... | +${snaps.length - 5} more | |\n`;
238
+ }
239
+ }
240
+ output += "\n";
241
+ });
242
+ }
243
+ else {
244
+ output += `AI analysis complete but no summary items generated.\n`;
245
+ if (ai["summary-status"] && ai["summary-status"] !== "ok") {
246
+ output += `Summary status: ${ai["summary-status"]}`;
247
+ if (ai["summary-reason"])
248
+ output += ` — ${ai["summary-reason"]}`;
249
+ output += "\n";
250
+ }
251
+ }
252
+ return { content: [{ type: "text", text: output }] };
253
+ }
254
+ // ── Changes ─────────────────────────────────────────────────────────────────
255
+ async function getChanges(buildId, config) {
256
+ const response = await percyGet("/build-items", config, {
257
+ "filter[build-id]": buildId,
258
+ "filter[category]": "changed",
259
+ "page[limit]": "30",
260
+ });
261
+ const items = response?.data || [];
262
+ if (!items.length) {
263
+ return {
264
+ content: [
265
+ {
266
+ type: "text",
267
+ text: `## Build #${buildId} — No Changes\n\nAll snapshots match the baseline.`,
268
+ },
269
+ ],
270
+ };
271
+ }
272
+ let output = `## Build #${buildId} — Changed Snapshots (${items.length})\n\n`;
273
+ output += `| # | Snapshot | Display Name | Diff | Bugs | Review | Comparisons |\n|---|---|---|---|---|---|---|\n`;
274
+ items.forEach((item, i) => {
275
+ const a = item.attributes || item;
276
+ const name = a["cover-snapshot-name"] || a.coverSnapshotName || "?";
277
+ const displayName = a["cover-snapshot-display-name"] || a.coverSnapshotDisplayName || "";
278
+ const diff = (a["max-diff-ratio"] ?? a.maxDiffRatio) != null
279
+ ? ((a["max-diff-ratio"] ?? a.maxDiffRatio) * 100).toFixed(1) + "%"
280
+ : "—";
281
+ const bugs = a["max-bug-total-potential-bugs"] ?? a.maxBugTotalPotentialBugs ?? 0;
282
+ const review = a["review-state"] || a.reviewState || "?";
283
+ const count = a["item-count"] || a.itemCount || 1;
284
+ output += `| ${i + 1} | ${name} | ${displayName || "—"} | ${diff} | ${bugs} | ${review} | ${count} |\n`;
285
+ });
286
+ output += `\nUse \`percy_get_snapshot\` with a snapshot ID from above for full details.\n`;
287
+ return { content: [{ type: "text", text: output }] };
288
+ }
289
+ // ── RCA ─────────────────────────────────────────────────────────────────────
290
+ async function getRca(args, config) {
291
+ if (!args.comparison_id) {
292
+ return {
293
+ content: [
294
+ {
295
+ type: "text",
296
+ text: `RCA requires a comparison_id.\n\nFind one with:\n\`Use percy_get_build with build_id "${args.build_id}" and detail "changes"\`\nThen: \`Use percy_get_snapshot with snapshot_id "..."\``,
297
+ },
298
+ ],
299
+ isError: true,
300
+ };
301
+ }
302
+ let rcaData;
303
+ try {
304
+ rcaData = await percyGet("/rca", config, {
305
+ comparison_id: args.comparison_id,
306
+ });
307
+ }
308
+ catch {
309
+ try {
310
+ await percyPost("/rca", config, {
311
+ data: {
312
+ attributes: {
313
+ "comparison-id": args.comparison_id,
314
+ },
315
+ },
316
+ });
317
+ return {
318
+ content: [
319
+ {
320
+ type: "text",
321
+ text: `## RCA Triggered\n\nStarted for comparison ${args.comparison_id}. Re-run in 30-60 seconds.`,
322
+ },
323
+ ],
324
+ };
325
+ }
326
+ catch (e) {
327
+ return {
328
+ content: [
329
+ {
330
+ type: "text",
331
+ text: `RCA not available: ${e.message}\nThis comparison may not have DOM metadata.`,
332
+ },
333
+ ],
334
+ isError: true,
335
+ };
336
+ }
337
+ }
338
+ const status = rcaData?.data?.attributes?.status || "unknown";
339
+ if (status === "pending") {
340
+ return {
341
+ content: [
342
+ {
343
+ type: "text",
344
+ text: `## RCA — Processing\n\nStill analyzing. Try again in 30 seconds.`,
345
+ },
346
+ ],
347
+ };
348
+ }
349
+ if (status === "failed") {
350
+ return {
351
+ content: [
352
+ {
353
+ type: "text",
354
+ text: `## RCA — Failed\n\nAnalysis failed. Missing DOM metadata.`,
355
+ },
356
+ ],
357
+ };
358
+ }
359
+ let output = `## Root Cause Analysis — Comparison ${args.comparison_id}\n\n`;
360
+ const diffNodes = rcaData?.data?.attributes?.["diff-nodes"] || {};
361
+ const common = diffNodes.common_diffs || [];
362
+ const removed = diffNodes.extra_base || [];
363
+ const added = diffNodes.extra_head || [];
364
+ if (common.length > 0) {
365
+ output += `### Changed (${common.length})\n\n`;
366
+ output += `| # | Element | XPath | Diff Type |\n|---|---|---|---|\n`;
367
+ common.slice(0, 20).forEach((diff, i) => {
368
+ const head = diff.head || {};
369
+ const tag = head.tagName || "?";
370
+ const xpath = (head.xpath || "").slice(0, 60);
371
+ const dt = head.diff_type === 1
372
+ ? "change"
373
+ : head.diff_type === 2
374
+ ? "ignored"
375
+ : "?";
376
+ output += `| ${i + 1} | ${tag} | \`${xpath}\` | ${dt} |\n`;
377
+ });
378
+ output += "\n";
379
+ }
380
+ if (removed.length > 0) {
381
+ output += `### Removed (${removed.length})\n\n`;
382
+ removed.slice(0, 10).forEach((n) => {
383
+ const d = n.node_detail || n;
384
+ output += `- ${d.tagName || "element"}`;
385
+ if (d.xpath)
386
+ output += ` — \`${d.xpath.slice(0, 60)}\``;
387
+ output += "\n";
388
+ });
389
+ output += "\n";
390
+ }
391
+ if (added.length > 0) {
392
+ output += `### Added (${added.length})\n\n`;
393
+ added.slice(0, 10).forEach((n) => {
394
+ const d = n.node_detail || n;
395
+ output += `- ${d.tagName || "element"}`;
396
+ if (d.xpath)
397
+ output += ` — \`${d.xpath.slice(0, 60)}\``;
398
+ output += "\n";
399
+ });
400
+ output += "\n";
401
+ }
402
+ if (!common.length && !removed.length && !added.length) {
403
+ output += `No DOM differences found.\n`;
404
+ }
405
+ return { content: [{ type: "text", text: output }] };
406
+ }
407
+ // ── Logs ────────────────────────────────────────────────────────────────────
408
+ async function getLogs(buildId, config) {
409
+ let output = `## Build #${buildId} — Diagnostics\n\n`;
410
+ // Build info
411
+ try {
412
+ const buildResponse = await percyGet(`/builds/${buildId}`, config);
413
+ const attrs = buildResponse?.data?.attributes || {};
414
+ if (attrs["failure-reason"]) {
415
+ output += `### Failure\n\n`;
416
+ output += `**Reason:** ${attrs["failure-reason"]}\n`;
417
+ if (attrs["failure-details"])
418
+ output += `**Details:** ${attrs["failure-details"]}\n`;
419
+ const buckets = attrs["error-buckets"];
420
+ if (Array.isArray(buckets) && buckets.length > 0) {
421
+ output += `\n**Error categories:**\n`;
422
+ output += `| Category | Snapshots |\n|---|---|\n`;
423
+ buckets.forEach((b) => {
424
+ output += `| ${b.bucket || b.name || "?"} | ${b.count ?? "?"} |\n`;
425
+ });
426
+ }
427
+ output += "\n";
428
+ }
429
+ else {
430
+ output += `Build state: **${attrs.state || "?"}** — no failure recorded.\n\n`;
431
+ }
432
+ // Failed snapshots
433
+ if ((attrs["failed-snapshots-count"] ?? 0) > 0) {
434
+ output += `### Failed Snapshots (${attrs["failed-snapshots-count"]})\n\n`;
435
+ try {
436
+ const failedResponse = await percyGet(`/builds/${buildId}/failed-snapshots`, config);
437
+ const failed = failedResponse?.data || [];
438
+ if (failed.length > 0) {
439
+ output += `| # | Name |\n|---|---|\n`;
440
+ failed.slice(0, 10).forEach((s, i) => {
441
+ output += `| ${i + 1} | ${s.attributes?.name || s.name || "?"} |\n`;
442
+ });
443
+ output += "\n";
444
+ }
445
+ }
446
+ catch {
447
+ output += `Could not fetch failed snapshot details.\n\n`;
448
+ }
449
+ }
450
+ }
451
+ catch {
452
+ output += `Could not fetch build info.\n\n`;
453
+ }
454
+ // Suggestions
455
+ try {
456
+ const sugResponse = await percyGet("/suggestions", config, {
457
+ build_id: buildId,
458
+ });
459
+ const suggestions = sugResponse?.data || [];
460
+ if (Array.isArray(suggestions) && suggestions.length > 0) {
461
+ output += `### Suggestions (${suggestions.length})\n\n`;
462
+ suggestions.forEach((s, i) => {
463
+ const a = s.attributes || s;
464
+ output += `${i + 1}. **${a["bucket-display-name"] || a.bucket || "Issue"}**\n`;
465
+ if (a["reason-message"])
466
+ output += ` ${a["reason-message"]}\n`;
467
+ const steps = a.suggestion || [];
468
+ if (Array.isArray(steps)) {
469
+ steps.forEach((step) => {
470
+ output += ` - ${step}\n`;
471
+ });
472
+ }
473
+ if (a["reference-doc-link"])
474
+ output += ` [Docs](${a["reference-doc-link"]})\n`;
475
+ output += "\n";
476
+ });
477
+ }
478
+ }
479
+ catch {
480
+ /* suggestions endpoint may not exist */
481
+ }
482
+ return { content: [{ type: "text", text: output }] };
483
+ }
484
+ // ── Network ─────────────────────────────────────────────────────────────────
485
+ async function getNetwork(args, config) {
486
+ if (!args.comparison_id) {
487
+ return {
488
+ content: [
489
+ {
490
+ type: "text",
491
+ text: `Network logs require comparison_id.\n\nFind one with:\n\`Use percy_get_snapshot with snapshot_id "..."\``,
492
+ },
493
+ ],
494
+ isError: true,
495
+ };
496
+ }
497
+ const response = await percyGet("/network-logs", config, {
498
+ comparison_id: args.comparison_id,
499
+ });
500
+ const logs = response?.data || response || {};
501
+ const entries = Array.isArray(logs) ? logs : Object.values(logs);
502
+ if (!entries.length) {
503
+ return {
504
+ content: [
505
+ {
506
+ type: "text",
507
+ text: `No network logs for comparison ${args.comparison_id}.`,
508
+ },
509
+ ],
510
+ };
511
+ }
512
+ let output = `## Network Logs — Comparison ${args.comparison_id}\n\n`;
513
+ output += `| # | URL | Base | Head | Type | Issue |\n|---|---|---|---|---|---|\n`;
514
+ entries.slice(0, 30).forEach((entry, i) => {
515
+ const url = entry.file || entry.domain || entry.url || "?";
516
+ const base = entry["base-status"] || entry.baseStatus || "—";
517
+ const head = entry["head-status"] || entry.headStatus || "—";
518
+ const type = entry.mimetype || entry.type || "—";
519
+ const summary = entry["status-summary"] || entry.statusSummary || "";
520
+ output += `| ${i + 1} | ${url} | ${base} | ${head} | ${type} | ${summary} |\n`;
521
+ });
522
+ return { content: [{ type: "text", text: output }] };
523
+ }
524
+ // ── Snapshots ───────────────────────────────────────────────────────────────
525
+ async function getSnapshots(buildId, config) {
526
+ const response = await percyGet("/build-items", config, {
527
+ "filter[build-id]": buildId,
528
+ "page[limit]": "30",
529
+ });
530
+ const items = response?.data || [];
531
+ if (!items.length) {
532
+ return {
533
+ content: [
534
+ {
535
+ type: "text",
536
+ text: `No snapshots found for build ${buildId}.`,
537
+ },
538
+ ],
539
+ };
540
+ }
541
+ // Count totals
542
+ let totalItems = 0;
543
+ items.forEach((item) => {
544
+ totalItems += item.attributes?.["item-count"] || item.itemCount || 1;
545
+ });
546
+ let output = `## Build #${buildId} — Snapshots\n\n`;
547
+ output += `**Groups:** ${items.length} | **Total snapshots:** ${totalItems}\n\n`;
548
+ output += `| # | Name | Display | Diff | Bugs | Review | Items | Snapshot IDs |\n|---|---|---|---|---|---|---|---|\n`;
549
+ items.forEach((item, i) => {
550
+ const a = item.attributes || item;
551
+ const name = a["cover-snapshot-name"] || a.coverSnapshotName || "?";
552
+ const display = a["cover-snapshot-display-name"] || a.coverSnapshotDisplayName || "—";
553
+ const diff = (a["max-diff-ratio"] ?? a.maxDiffRatio) != null
554
+ ? ((a["max-diff-ratio"] ?? a.maxDiffRatio) * 100).toFixed(1) + "%"
555
+ : "—";
556
+ const bugs = a["max-bug-total-potential-bugs"] ?? a.maxBugTotalPotentialBugs ?? "—";
557
+ const review = a["review-state"] || a.reviewState || "?";
558
+ const count = a["item-count"] || a.itemCount || 1;
559
+ const snapIds = (a["snapshot-ids"] || a.snapshotIds || [])
560
+ .slice(0, 3)
561
+ .join(", ");
562
+ const more = (a["snapshot-ids"] || a.snapshotIds || []).length > 3 ? "..." : "";
563
+ output += `| ${i + 1} | ${name} | ${display} | ${diff} | ${bugs} | ${review} | ${count} | ${snapIds}${more} |\n`;
564
+ });
565
+ output += `\nUse \`percy_get_snapshot\` with a snapshot ID for full comparison details.\n`;
566
+ return { content: [{ type: "text", text: output }] };
567
+ }
@@ -0,0 +1,8 @@
1
+ import { BrowserStackConfig } from "../../../lib/types.js";
2
+ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3
+ export declare function percyGetBuildsV2(args: {
4
+ project_slug?: string;
5
+ branch?: string;
6
+ state?: string;
7
+ limit?: number;
8
+ }, config: BrowserStackConfig): Promise<CallToolResult>;
@@ -0,0 +1,63 @@
1
+ import { percyGet } from "../../../lib/percy-api/percy-auth.js";
2
+ import { setActiveBuild } from "../../../lib/percy-api/percy-session.js";
3
+ export async function percyGetBuildsV2(args, config) {
4
+ let path = "/builds";
5
+ const params = {};
6
+ if (args.project_slug) {
7
+ path = `/projects/${args.project_slug}/builds`;
8
+ }
9
+ if (args.branch)
10
+ params["filter[branch]"] = args.branch;
11
+ if (args.state)
12
+ params["filter[state]"] = args.state;
13
+ params["page[limit]"] = String(args.limit || 10);
14
+ const response = await percyGet(path, config, params);
15
+ const builds = response?.data || [];
16
+ if (builds.length === 0) {
17
+ return {
18
+ content: [
19
+ {
20
+ type: "text",
21
+ text: "No builds found. Use `percy_get_projects` to find project slugs, then filter with `project_slug`.",
22
+ },
23
+ ],
24
+ };
25
+ }
26
+ let output = `## Percy Builds (${builds.length})\n\n`;
27
+ output += `| # | Build ID | Build # | Branch | State | Review | Snapshots | Diffs | URL |\n`;
28
+ output += `|---|---|---|---|---|---|---|---|---|\n`;
29
+ builds.forEach((b, i) => {
30
+ const attrs = b.attributes || {};
31
+ const num = attrs["build-number"] || "—";
32
+ const branch = attrs.branch || "?";
33
+ const state = attrs.state || "?";
34
+ const review = attrs["review-state"] || "—";
35
+ const snaps = attrs["total-snapshots"] ?? "?";
36
+ const diffs = attrs["total-comparisons-diff"] ?? "—";
37
+ const webUrl = attrs["web-url"] || "";
38
+ const urlShort = webUrl ? `[View](${webUrl})` : "—";
39
+ output += `| ${i + 1} | ${b.id} | #${num} | ${branch} | ${state} | ${review} | ${snaps} | ${diffs} | ${urlShort} |\n`;
40
+ });
41
+ // Set the most recent build as active
42
+ const latest = builds[0];
43
+ if (latest) {
44
+ const latestAttrs = latest.attributes || {};
45
+ setActiveBuild({
46
+ id: latest.id,
47
+ number: latestAttrs["build-number"]?.toString(),
48
+ url: latestAttrs["web-url"],
49
+ branch: latestAttrs.branch,
50
+ });
51
+ }
52
+ // Quick access to latest build
53
+ output += `\n### Latest Build: #${latest.attributes?.["build-number"] || latest.id} (ID: ${latest.id})\n\n`;
54
+ if (latest.attributes?.["web-url"]) {
55
+ output += `**URL:** ${latest.attributes["web-url"]}\n\n`;
56
+ }
57
+ output += `### Drill Down\n\n`;
58
+ output += `- \`percy_get_build\` with build_id "${latest.id}" — Full overview\n`;
59
+ output += `- \`percy_get_build\` with build_id "${latest.id}" and detail "snapshots" — All snapshots\n`;
60
+ output += `- \`percy_get_build\` with build_id "${latest.id}" and detail "ai_summary" — AI analysis\n`;
61
+ output += `- \`percy_search_builds\` with build_id "${latest.id}" and category "changed" — Only diffs\n`;
62
+ return { content: [{ type: "text", text: output }] };
63
+ }
@@ -0,0 +1,5 @@
1
+ import { BrowserStackConfig } from "../../../lib/types.js";
2
+ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3
+ export declare function percyGetComparison(args: {
4
+ comparison_id: string;
5
+ }, config: BrowserStackConfig): Promise<CallToolResult>;