@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.
- package/dist/lib/percy-api/auth.d.ts +41 -0
- package/dist/lib/percy-api/auth.js +96 -0
- package/dist/lib/percy-api/cache.d.ts +28 -0
- package/dist/lib/percy-api/cache.js +48 -0
- package/dist/lib/percy-api/client.d.ts +69 -0
- package/dist/lib/percy-api/client.js +275 -0
- package/dist/lib/percy-api/errors.d.ts +15 -0
- package/dist/lib/percy-api/errors.js +52 -0
- package/dist/lib/percy-api/formatter.d.ts +16 -0
- package/dist/lib/percy-api/formatter.js +344 -0
- package/dist/lib/percy-api/percy-auth.d.ts +43 -0
- package/dist/lib/percy-api/percy-auth.js +137 -0
- package/dist/lib/percy-api/percy-error-handler.d.ts +24 -0
- package/dist/lib/percy-api/percy-error-handler.js +302 -0
- package/dist/lib/percy-api/percy-session.d.ts +42 -0
- package/dist/lib/percy-api/percy-session.js +87 -0
- package/dist/lib/percy-api/polling.d.ts +26 -0
- package/dist/lib/percy-api/polling.js +42 -0
- package/dist/lib/percy-api/types.d.ts +56 -0
- package/dist/lib/percy-api/types.js +76 -0
- package/dist/server-factory.js +4 -0
- package/dist/tools/percy-mcp/advanced/branchline-operations.d.ts +16 -0
- package/dist/tools/percy-mcp/advanced/branchline-operations.js +81 -0
- package/dist/tools/percy-mcp/advanced/manage-variants.d.ts +16 -0
- package/dist/tools/percy-mcp/advanced/manage-variants.js +155 -0
- package/dist/tools/percy-mcp/advanced/manage-visual-monitoring.d.ts +16 -0
- package/dist/tools/percy-mcp/advanced/manage-visual-monitoring.js +171 -0
- package/dist/tools/percy-mcp/auth/auth-status.d.ts +3 -0
- package/dist/tools/percy-mcp/auth/auth-status.js +131 -0
- package/dist/tools/percy-mcp/core/approve-build.d.ts +14 -0
- package/dist/tools/percy-mcp/core/approve-build.js +97 -0
- package/dist/tools/percy-mcp/core/get-build-items.d.ts +13 -0
- package/dist/tools/percy-mcp/core/get-build-items.js +65 -0
- package/dist/tools/percy-mcp/core/get-build.d.ts +10 -0
- package/dist/tools/percy-mcp/core/get-build.js +16 -0
- package/dist/tools/percy-mcp/core/get-comparison.d.ts +11 -0
- package/dist/tools/percy-mcp/core/get-comparison.js +59 -0
- package/dist/tools/percy-mcp/core/get-snapshot.d.ts +10 -0
- package/dist/tools/percy-mcp/core/get-snapshot.js +40 -0
- package/dist/tools/percy-mcp/core/list-builds.d.ts +14 -0
- package/dist/tools/percy-mcp/core/list-builds.js +45 -0
- package/dist/tools/percy-mcp/core/list-projects.d.ts +12 -0
- package/dist/tools/percy-mcp/core/list-projects.js +51 -0
- package/dist/tools/percy-mcp/creation/create-app-snapshot.d.ts +12 -0
- package/dist/tools/percy-mcp/creation/create-app-snapshot.js +29 -0
- package/dist/tools/percy-mcp/creation/create-build.d.ts +19 -0
- package/dist/tools/percy-mcp/creation/create-build.js +68 -0
- package/dist/tools/percy-mcp/creation/create-comparison.d.ts +18 -0
- package/dist/tools/percy-mcp/creation/create-comparison.js +90 -0
- package/dist/tools/percy-mcp/creation/create-snapshot.d.ts +17 -0
- package/dist/tools/percy-mcp/creation/create-snapshot.js +99 -0
- package/dist/tools/percy-mcp/creation/finalize-build.d.ts +12 -0
- package/dist/tools/percy-mcp/creation/finalize-build.js +33 -0
- package/dist/tools/percy-mcp/creation/finalize-comparison.d.ts +10 -0
- package/dist/tools/percy-mcp/creation/finalize-comparison.js +16 -0
- package/dist/tools/percy-mcp/creation/finalize-snapshot.d.ts +12 -0
- package/dist/tools/percy-mcp/creation/finalize-snapshot.js +33 -0
- package/dist/tools/percy-mcp/creation/upload-resource.d.ts +15 -0
- package/dist/tools/percy-mcp/creation/upload-resource.js +43 -0
- package/dist/tools/percy-mcp/creation/upload-tile.d.ts +11 -0
- package/dist/tools/percy-mcp/creation/upload-tile.js +53 -0
- package/dist/tools/percy-mcp/diagnostics/analyze-logs-realtime.d.ts +13 -0
- package/dist/tools/percy-mcp/diagnostics/analyze-logs-realtime.js +65 -0
- package/dist/tools/percy-mcp/diagnostics/get-build-logs.d.ts +17 -0
- package/dist/tools/percy-mcp/diagnostics/get-build-logs.js +74 -0
- package/dist/tools/percy-mcp/diagnostics/get-network-logs.d.ts +5 -0
- package/dist/tools/percy-mcp/diagnostics/get-network-logs.js +21 -0
- package/dist/tools/percy-mcp/diagnostics/get-suggestions.d.ts +7 -0
- package/dist/tools/percy-mcp/diagnostics/get-suggestions.js +24 -0
- package/dist/tools/percy-mcp/index.d.ts +36 -0
- package/dist/tools/percy-mcp/index.js +1137 -0
- package/dist/tools/percy-mcp/intelligence/get-ai-analysis.d.ts +15 -0
- package/dist/tools/percy-mcp/intelligence/get-ai-analysis.js +166 -0
- package/dist/tools/percy-mcp/intelligence/get-ai-quota.d.ts +9 -0
- package/dist/tools/percy-mcp/intelligence/get-ai-quota.js +73 -0
- package/dist/tools/percy-mcp/intelligence/get-build-summary.d.ts +11 -0
- package/dist/tools/percy-mcp/intelligence/get-build-summary.js +78 -0
- package/dist/tools/percy-mcp/intelligence/get-rca.d.ts +6 -0
- package/dist/tools/percy-mcp/intelligence/get-rca.js +153 -0
- package/dist/tools/percy-mcp/intelligence/suggest-prompt.d.ts +15 -0
- package/dist/tools/percy-mcp/intelligence/suggest-prompt.js +86 -0
- package/dist/tools/percy-mcp/intelligence/trigger-ai-recompute.d.ts +16 -0
- package/dist/tools/percy-mcp/intelligence/trigger-ai-recompute.js +64 -0
- package/dist/tools/percy-mcp/management/create-project.d.ts +14 -0
- package/dist/tools/percy-mcp/management/create-project.js +52 -0
- package/dist/tools/percy-mcp/management/get-usage-stats.d.ts +12 -0
- package/dist/tools/percy-mcp/management/get-usage-stats.js +61 -0
- package/dist/tools/percy-mcp/management/manage-browser-targets.d.ts +12 -0
- package/dist/tools/percy-mcp/management/manage-browser-targets.js +136 -0
- package/dist/tools/percy-mcp/management/manage-comments.d.ts +14 -0
- package/dist/tools/percy-mcp/management/manage-comments.js +147 -0
- package/dist/tools/percy-mcp/management/manage-ignored-regions.d.ts +18 -0
- package/dist/tools/percy-mcp/management/manage-ignored-regions.js +182 -0
- package/dist/tools/percy-mcp/management/manage-project-settings.d.ts +16 -0
- package/dist/tools/percy-mcp/management/manage-project-settings.js +97 -0
- package/dist/tools/percy-mcp/management/manage-tokens.d.ts +14 -0
- package/dist/tools/percy-mcp/management/manage-tokens.js +90 -0
- package/dist/tools/percy-mcp/management/manage-webhooks.d.ts +15 -0
- package/dist/tools/percy-mcp/management/manage-webhooks.js +180 -0
- package/dist/tools/percy-mcp/v2/auth-status.d.ts +3 -0
- package/dist/tools/percy-mcp/v2/auth-status.js +80 -0
- package/dist/tools/percy-mcp/v2/clone-build.d.ts +24 -0
- package/dist/tools/percy-mcp/v2/clone-build.js +539 -0
- package/dist/tools/percy-mcp/v2/create-app-build.d.ts +28 -0
- package/dist/tools/percy-mcp/v2/create-app-build.js +442 -0
- package/dist/tools/percy-mcp/v2/create-build.d.ts +16 -0
- package/dist/tools/percy-mcp/v2/create-build.js +601 -0
- package/dist/tools/percy-mcp/v2/create-project.d.ts +8 -0
- package/dist/tools/percy-mcp/v2/create-project.js +33 -0
- package/dist/tools/percy-mcp/v2/discover-urls.d.ts +7 -0
- package/dist/tools/percy-mcp/v2/discover-urls.js +38 -0
- package/dist/tools/percy-mcp/v2/figma-baseline.d.ts +7 -0
- package/dist/tools/percy-mcp/v2/figma-baseline.js +18 -0
- package/dist/tools/percy-mcp/v2/figma-build.d.ts +7 -0
- package/dist/tools/percy-mcp/v2/figma-build.js +39 -0
- package/dist/tools/percy-mcp/v2/figma-link.d.ts +6 -0
- package/dist/tools/percy-mcp/v2/figma-link.js +27 -0
- package/dist/tools/percy-mcp/v2/get-ai-summary.d.ts +5 -0
- package/dist/tools/percy-mcp/v2/get-ai-summary.js +109 -0
- package/dist/tools/percy-mcp/v2/get-build-detail.d.ts +22 -0
- package/dist/tools/percy-mcp/v2/get-build-detail.js +567 -0
- package/dist/tools/percy-mcp/v2/get-builds.d.ts +8 -0
- package/dist/tools/percy-mcp/v2/get-builds.js +63 -0
- package/dist/tools/percy-mcp/v2/get-comparison.d.ts +5 -0
- package/dist/tools/percy-mcp/v2/get-comparison.js +94 -0
- package/dist/tools/percy-mcp/v2/get-devices.d.ts +5 -0
- package/dist/tools/percy-mcp/v2/get-devices.js +33 -0
- package/dist/tools/percy-mcp/v2/get-insights.d.ts +7 -0
- package/dist/tools/percy-mcp/v2/get-insights.js +52 -0
- package/dist/tools/percy-mcp/v2/get-projects.d.ts +6 -0
- package/dist/tools/percy-mcp/v2/get-projects.js +41 -0
- package/dist/tools/percy-mcp/v2/get-snapshot.d.ts +5 -0
- package/dist/tools/percy-mcp/v2/get-snapshot.js +96 -0
- package/dist/tools/percy-mcp/v2/get-test-case-history.d.ts +5 -0
- package/dist/tools/percy-mcp/v2/get-test-case-history.js +20 -0
- package/dist/tools/percy-mcp/v2/get-test-cases.d.ts +6 -0
- package/dist/tools/percy-mcp/v2/get-test-cases.js +36 -0
- package/dist/tools/percy-mcp/v2/index.d.ts +35 -0
- package/dist/tools/percy-mcp/v2/index.js +544 -0
- package/dist/tools/percy-mcp/v2/list-integrations.d.ts +5 -0
- package/dist/tools/percy-mcp/v2/list-integrations.js +41 -0
- package/dist/tools/percy-mcp/v2/manage-domains.d.ts +8 -0
- package/dist/tools/percy-mcp/v2/manage-domains.js +33 -0
- package/dist/tools/percy-mcp/v2/manage-insights-email.d.ts +8 -0
- package/dist/tools/percy-mcp/v2/manage-insights-email.js +49 -0
- package/dist/tools/percy-mcp/v2/manage-usage-alerts.d.ts +10 -0
- package/dist/tools/percy-mcp/v2/manage-usage-alerts.js +43 -0
- package/dist/tools/percy-mcp/v2/migrate-integrations.d.ts +6 -0
- package/dist/tools/percy-mcp/v2/migrate-integrations.js +20 -0
- package/dist/tools/percy-mcp/v2/preview-comparison.d.ts +5 -0
- package/dist/tools/percy-mcp/v2/preview-comparison.js +17 -0
- package/dist/tools/percy-mcp/v2/search-build-items.d.ts +12 -0
- package/dist/tools/percy-mcp/v2/search-build-items.js +45 -0
- package/dist/tools/percy-mcp/workflows/auto-triage.d.ts +7 -0
- package/dist/tools/percy-mcp/workflows/auto-triage.js +82 -0
- package/dist/tools/percy-mcp/workflows/clone-build.d.ts +22 -0
- package/dist/tools/percy-mcp/workflows/clone-build.js +414 -0
- package/dist/tools/percy-mcp/workflows/create-percy-build.d.ts +32 -0
- package/dist/tools/percy-mcp/workflows/create-percy-build.js +434 -0
- package/dist/tools/percy-mcp/workflows/debug-failed-build.d.ts +5 -0
- package/dist/tools/percy-mcp/workflows/debug-failed-build.js +122 -0
- package/dist/tools/percy-mcp/workflows/diff-explain.d.ts +6 -0
- package/dist/tools/percy-mcp/workflows/diff-explain.js +147 -0
- package/dist/tools/percy-mcp/workflows/pr-visual-report.d.ts +8 -0
- package/dist/tools/percy-mcp/workflows/pr-visual-report.js +184 -0
- package/dist/tools/percy-mcp/workflows/run-tests.d.ts +17 -0
- package/dist/tools/percy-mcp/workflows/run-tests.js +107 -0
- package/dist/tools/percy-mcp/workflows/snapshot-urls.d.ts +18 -0
- package/dist/tools/percy-mcp/workflows/snapshot-urls.js +197 -0
- package/package.json +4 -3
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown formatting utilities for Percy API responses.
|
|
3
|
+
*
|
|
4
|
+
* Each function transforms typed Percy API data into concise,
|
|
5
|
+
* agent-readable markdown. All functions handle null/undefined
|
|
6
|
+
* fields gracefully — showing "N/A" or omitting the section.
|
|
7
|
+
*/
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Helpers
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
function pct(value) {
|
|
12
|
+
if (value == null)
|
|
13
|
+
return "N/A";
|
|
14
|
+
return `${(value * 100).toFixed(1)}%`;
|
|
15
|
+
}
|
|
16
|
+
function na(value) {
|
|
17
|
+
if (value == null || value === "")
|
|
18
|
+
return "N/A";
|
|
19
|
+
return String(value);
|
|
20
|
+
}
|
|
21
|
+
function formatDuration(startIso, endIso) {
|
|
22
|
+
if (!startIso || !endIso)
|
|
23
|
+
return "N/A";
|
|
24
|
+
const ms = new Date(endIso).getTime() - new Date(startIso).getTime();
|
|
25
|
+
if (Number.isNaN(ms) || ms < 0)
|
|
26
|
+
return "N/A";
|
|
27
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
28
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
29
|
+
const seconds = totalSeconds % 60;
|
|
30
|
+
if (minutes === 0)
|
|
31
|
+
return `${seconds}s`;
|
|
32
|
+
return `${minutes}m ${seconds}s`;
|
|
33
|
+
}
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// formatBuild
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
export function formatBuild(build) {
|
|
38
|
+
if (!build)
|
|
39
|
+
return "_No build data available._";
|
|
40
|
+
const num = build.buildNumber ?? "?";
|
|
41
|
+
const state = (build.state ?? "unknown").toUpperCase();
|
|
42
|
+
const lines = [];
|
|
43
|
+
// Header — state-aware
|
|
44
|
+
if (build.state === "processing") {
|
|
45
|
+
const total = build.totalComparisons ?? 0;
|
|
46
|
+
const finished = build.totalComparisonsFinished ?? 0;
|
|
47
|
+
const percent = total > 0 ? Math.round((finished / total) * 100) : 0;
|
|
48
|
+
lines.push(`## Build #${num} — PROCESSING (${percent}% complete)`);
|
|
49
|
+
}
|
|
50
|
+
else if (build.state === "failed") {
|
|
51
|
+
lines.push(`## Build #${num} — FAILED`);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
lines.push(`## Build #${num} — ${state}`);
|
|
55
|
+
}
|
|
56
|
+
// Branch / SHA
|
|
57
|
+
const branch = na(build.branch);
|
|
58
|
+
const sha = na(build.commit?.sha ?? build.sha);
|
|
59
|
+
lines.push(`**Branch:** ${branch} | **SHA:** ${sha}`);
|
|
60
|
+
// Review state
|
|
61
|
+
if (build.reviewState) {
|
|
62
|
+
lines.push(`**Review:** ${build.reviewState}`);
|
|
63
|
+
}
|
|
64
|
+
// Snapshot stats — handle both camelCase and kebab-case
|
|
65
|
+
const total = build.totalSnapshots ?? build["total-snapshots"];
|
|
66
|
+
const changed = build.totalComparisonsDiff ?? build["total-comparisons-diff"];
|
|
67
|
+
const totalComparisons = build.totalComparisons ?? build["total-comparisons"];
|
|
68
|
+
const unreviewed = build.totalSnapshotsUnreviewed ?? build["total-snapshots-unreviewed"];
|
|
69
|
+
const newSnaps = null; // Not in API — derived from build-items category
|
|
70
|
+
const removed = null; // Not in API — derived from build-items category
|
|
71
|
+
const unchanged = null; // Not in API — derived from build-items category
|
|
72
|
+
if (total != null) {
|
|
73
|
+
const parts = [`${total} snapshots`];
|
|
74
|
+
if (totalComparisons != null)
|
|
75
|
+
parts.push(`${totalComparisons} comparisons`);
|
|
76
|
+
if (changed != null)
|
|
77
|
+
parts.push(`${changed} with diffs`);
|
|
78
|
+
if (unreviewed != null)
|
|
79
|
+
parts.push(`${unreviewed} unreviewed`);
|
|
80
|
+
if (newSnaps != null)
|
|
81
|
+
parts.push(`${newSnaps} new`);
|
|
82
|
+
if (removed != null)
|
|
83
|
+
parts.push(`${removed} removed`);
|
|
84
|
+
if (unchanged != null)
|
|
85
|
+
parts.push(`${unchanged} unchanged`);
|
|
86
|
+
lines.push(`**Stats:** ${parts.join(" | ")}`);
|
|
87
|
+
}
|
|
88
|
+
// Duration
|
|
89
|
+
const duration = formatDuration(build.createdAt, build.finishedAt);
|
|
90
|
+
if (duration !== "N/A") {
|
|
91
|
+
lines.push(`**Duration:** ${duration}`);
|
|
92
|
+
}
|
|
93
|
+
// No visual changes
|
|
94
|
+
if (build.state === "finished" &&
|
|
95
|
+
(build.totalComparisonsDiff === 0 || build.totalComparisonsDiff == null) &&
|
|
96
|
+
(build.totalSnapshotsNew ?? 0) === 0 &&
|
|
97
|
+
(build.totalSnapshotsRemoved ?? 0) === 0) {
|
|
98
|
+
lines.push("");
|
|
99
|
+
lines.push("> **No visual changes detected in this build.**");
|
|
100
|
+
}
|
|
101
|
+
// Failure info
|
|
102
|
+
if (build.state === "failed") {
|
|
103
|
+
lines.push("");
|
|
104
|
+
if (build.failureReason) {
|
|
105
|
+
lines.push(`**Failure Reason:** ${build.failureReason}`);
|
|
106
|
+
}
|
|
107
|
+
if (build.errorBuckets && build.errorBuckets.length > 0) {
|
|
108
|
+
lines.push("");
|
|
109
|
+
lines.push("### Error Buckets");
|
|
110
|
+
for (const bucket of build.errorBuckets) {
|
|
111
|
+
const name = bucket.name ?? bucket.errorType ?? "Unknown";
|
|
112
|
+
const count = bucket.count ?? bucket.snapshotCount ?? "?";
|
|
113
|
+
lines.push(`- **${name}** — ${count} snapshot(s)`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// AI analysis — handle both camelCase (from deserializer) and kebab-case keys
|
|
118
|
+
const ai = build.aiDetails || build["ai-details"];
|
|
119
|
+
if (ai && build.state !== "failed") {
|
|
120
|
+
const aiEnabled = ai.aiEnabled ?? ai["ai-enabled"] ?? false;
|
|
121
|
+
if (aiEnabled) {
|
|
122
|
+
lines.push("");
|
|
123
|
+
lines.push("### AI Analysis");
|
|
124
|
+
const compsWithAi = ai.totalComparisonsWithAi ?? ai["total-comparisons-with-ai"];
|
|
125
|
+
const bugs = ai.totalPotentialBugs ?? ai["total-potential-bugs"];
|
|
126
|
+
const diffsReduced = ai.totalDiffsReducedCapped ?? ai["total-diffs-reduced-capped"];
|
|
127
|
+
const aiVisualDiffs = ai.totalAiVisualDiffs ?? ai["total-ai-visual-diffs"];
|
|
128
|
+
const allCompleted = ai.allAiJobsCompleted ?? ai["all-ai-jobs-completed"];
|
|
129
|
+
const summaryStatus = ai.summaryStatus ?? ai["summary-status"];
|
|
130
|
+
if (compsWithAi != null) {
|
|
131
|
+
lines.push(`- Comparisons analyzed by AI: ${compsWithAi}`);
|
|
132
|
+
}
|
|
133
|
+
if (bugs != null && bugs > 0) {
|
|
134
|
+
lines.push(`- **Potential bugs: ${bugs}**`);
|
|
135
|
+
}
|
|
136
|
+
if (diffsReduced != null && diffsReduced > 0) {
|
|
137
|
+
lines.push(`- Diffs reduced by AI: ${diffsReduced}`);
|
|
138
|
+
}
|
|
139
|
+
if (aiVisualDiffs != null) {
|
|
140
|
+
lines.push(`- AI visual diffs: ${aiVisualDiffs}`);
|
|
141
|
+
}
|
|
142
|
+
if (allCompleted != null) {
|
|
143
|
+
lines.push(`- AI jobs: ${allCompleted ? "completed" : "in progress"}`);
|
|
144
|
+
}
|
|
145
|
+
if (summaryStatus) {
|
|
146
|
+
lines.push(`- Summary: ${summaryStatus}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Build summary — from included build-summary relationship
|
|
151
|
+
const buildSummary = build.buildSummary;
|
|
152
|
+
const summaryText = buildSummary?.summary || build.summary;
|
|
153
|
+
if (summaryText) {
|
|
154
|
+
lines.push("");
|
|
155
|
+
lines.push("### Build Summary");
|
|
156
|
+
try {
|
|
157
|
+
const parsed = typeof summaryText === "string" ? JSON.parse(summaryText) : summaryText;
|
|
158
|
+
if (parsed?.title) {
|
|
159
|
+
lines.push(`> ${parsed.title}`);
|
|
160
|
+
}
|
|
161
|
+
if (Array.isArray(parsed?.items)) {
|
|
162
|
+
parsed.items.forEach((item) => {
|
|
163
|
+
lines.push(`- ${item.title || item}`);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// Not JSON — treat as plain text
|
|
169
|
+
const text = String(summaryText);
|
|
170
|
+
lines.push(text
|
|
171
|
+
.split("\n")
|
|
172
|
+
.map((l) => `> ${l}`)
|
|
173
|
+
.join("\n"));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return lines.join("\n");
|
|
177
|
+
}
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// formatSnapshot
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
export function formatSnapshot(snapshot, comparisons) {
|
|
182
|
+
if (!snapshot)
|
|
183
|
+
return "_No snapshot data available._";
|
|
184
|
+
const lines = [];
|
|
185
|
+
lines.push(`### ${na(snapshot.name)}`);
|
|
186
|
+
if (snapshot.reviewState) {
|
|
187
|
+
lines.push(`**Review:** ${snapshot.reviewState}`);
|
|
188
|
+
}
|
|
189
|
+
if (comparisons && comparisons.length > 0) {
|
|
190
|
+
lines.push("");
|
|
191
|
+
lines.push("| Browser | Width | Diff | AI Diff | AI Status |");
|
|
192
|
+
lines.push("|---------|-------|------|---------|-----------|");
|
|
193
|
+
for (const c of comparisons) {
|
|
194
|
+
const browser = na(c.browser?.name ?? c.browserName);
|
|
195
|
+
const width = c.width != null ? `${c.width}px` : "N/A";
|
|
196
|
+
const diff = pct(c.diffRatio);
|
|
197
|
+
const aiDiff = pct(c.aiDiffRatio);
|
|
198
|
+
const aiStatus = na(c.aiProcessingState);
|
|
199
|
+
lines.push(`| ${browser} | ${width} | ${diff} | ${aiDiff} | ${aiStatus} |`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return lines.join("\n");
|
|
203
|
+
}
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// formatComparison
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
export function formatComparison(comparison, options) {
|
|
208
|
+
if (!comparison)
|
|
209
|
+
return "_No comparison data available._";
|
|
210
|
+
const browser = na(comparison.browser?.name ?? comparison.browserName);
|
|
211
|
+
const width = comparison.width != null ? `${comparison.width}px` : "";
|
|
212
|
+
const diff = pct(comparison.diffRatio);
|
|
213
|
+
const lines = [];
|
|
214
|
+
// Header
|
|
215
|
+
let header = `**${browser} ${width}** — ${diff} diff`;
|
|
216
|
+
if (comparison.aiDiffRatio != null) {
|
|
217
|
+
header += ` (AI: ${pct(comparison.aiDiffRatio)})`;
|
|
218
|
+
}
|
|
219
|
+
lines.push(header);
|
|
220
|
+
// Image URLs
|
|
221
|
+
const baseUrl = comparison.baseScreenshot?.url ?? comparison.baseUrl;
|
|
222
|
+
const headUrl = comparison.headScreenshot?.url ?? comparison.headUrl;
|
|
223
|
+
const diffUrl = comparison.diffImage?.url ?? comparison.diffUrl;
|
|
224
|
+
if (baseUrl || headUrl || diffUrl) {
|
|
225
|
+
lines.push("");
|
|
226
|
+
lines.push("Images:");
|
|
227
|
+
if (baseUrl)
|
|
228
|
+
lines.push(`- Base: ${baseUrl}`);
|
|
229
|
+
if (headUrl)
|
|
230
|
+
lines.push(`- Head: ${headUrl}`);
|
|
231
|
+
if (diffUrl)
|
|
232
|
+
lines.push(`- Diff: ${diffUrl}`);
|
|
233
|
+
}
|
|
234
|
+
// AI Regions
|
|
235
|
+
if (options?.includeRegions &&
|
|
236
|
+
comparison.appliedRegions &&
|
|
237
|
+
comparison.appliedRegions.length > 0) {
|
|
238
|
+
const regions = comparison.appliedRegions;
|
|
239
|
+
lines.push("");
|
|
240
|
+
lines.push(`AI Regions (${regions.length}):`);
|
|
241
|
+
regions.forEach((region, i) => {
|
|
242
|
+
const label = na(region.label ?? region.name);
|
|
243
|
+
const type = region.type ?? region.changeType ?? "unknown";
|
|
244
|
+
const desc = region.description ?? "";
|
|
245
|
+
let line = `${i + 1}. **${label}** (${type})`;
|
|
246
|
+
if (desc)
|
|
247
|
+
line += ` — ${desc}`;
|
|
248
|
+
lines.push(line);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return lines.join("\n");
|
|
252
|
+
}
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// formatSuggestions
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
export function formatSuggestions(suggestions) {
|
|
257
|
+
if (!suggestions || suggestions.length === 0) {
|
|
258
|
+
return "_No failure suggestions available._";
|
|
259
|
+
}
|
|
260
|
+
const lines = [];
|
|
261
|
+
lines.push("## Build Failure Suggestions");
|
|
262
|
+
lines.push("");
|
|
263
|
+
suggestions.forEach((s, i) => {
|
|
264
|
+
const title = na(s.title ?? s.name);
|
|
265
|
+
const affected = s.affectedSnapshots ?? s.snapshotsAffected ?? null;
|
|
266
|
+
let heading = `### ${i + 1}. ${title}`;
|
|
267
|
+
if (affected != null)
|
|
268
|
+
heading += ` (${affected} snapshots affected)`;
|
|
269
|
+
lines.push(heading);
|
|
270
|
+
if (s.reason)
|
|
271
|
+
lines.push(`**Reason:** ${s.reason}`);
|
|
272
|
+
if (s.description)
|
|
273
|
+
lines.push(`**Reason:** ${s.description}`);
|
|
274
|
+
if (s.fixSteps && s.fixSteps.length > 0) {
|
|
275
|
+
lines.push("**Fix Steps:**");
|
|
276
|
+
s.fixSteps.forEach((step, j) => {
|
|
277
|
+
lines.push(`${j + 1}. ${step}`);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
if (s.docsUrl ?? s.docs) {
|
|
281
|
+
lines.push(`**Docs:** ${s.docsUrl ?? s.docs}`);
|
|
282
|
+
}
|
|
283
|
+
lines.push("");
|
|
284
|
+
});
|
|
285
|
+
return lines.join("\n").trimEnd();
|
|
286
|
+
}
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
// formatNetworkLogs
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
export function formatNetworkLogs(logs) {
|
|
291
|
+
if (!logs || logs.length === 0) {
|
|
292
|
+
return "_No network logs available._";
|
|
293
|
+
}
|
|
294
|
+
const lines = [];
|
|
295
|
+
lines.push("## Network Logs");
|
|
296
|
+
lines.push("");
|
|
297
|
+
lines.push("| URL | Base Status | Head Status | Type | Issue |");
|
|
298
|
+
lines.push("|-----|-------------|-------------|------|-------|");
|
|
299
|
+
for (const log of logs) {
|
|
300
|
+
const url = na(log.url);
|
|
301
|
+
const baseStatus = na(log.baseStatus ?? log.baseStatusCode);
|
|
302
|
+
const headStatus = na(log.headStatus ?? log.headStatusCode);
|
|
303
|
+
const type = na(log.resourceType ?? log.type);
|
|
304
|
+
const issue = na(log.issue ?? log.error);
|
|
305
|
+
lines.push(`| ${url} | ${baseStatus} | ${headStatus} | ${type} | ${issue} |`);
|
|
306
|
+
}
|
|
307
|
+
return lines.join("\n");
|
|
308
|
+
}
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
// formatBuildStatus
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
export function formatBuildStatus(build) {
|
|
313
|
+
if (!build)
|
|
314
|
+
return "Build: N/A";
|
|
315
|
+
const num = build.buildNumber ?? "?";
|
|
316
|
+
const state = (build.state ?? "unknown").toUpperCase();
|
|
317
|
+
const parts = [];
|
|
318
|
+
if (build.totalComparisonsDiff != null) {
|
|
319
|
+
parts.push(`${build.totalComparisonsDiff} changed`);
|
|
320
|
+
}
|
|
321
|
+
const ai = build.aiDetails;
|
|
322
|
+
if (ai?.potentialBugs != null) {
|
|
323
|
+
parts.push(`${ai.potentialBugs} bugs`);
|
|
324
|
+
}
|
|
325
|
+
if (ai?.noiseFiltered != null) {
|
|
326
|
+
parts.push(`${ai.noiseFiltered}% noise filtered`);
|
|
327
|
+
}
|
|
328
|
+
const suffix = parts.length > 0 ? ` — ${parts.join(", ")}` : "";
|
|
329
|
+
return `Build #${num}: ${state}${suffix}`;
|
|
330
|
+
}
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
// formatAiWarning
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
export function formatAiWarning(comparisons) {
|
|
335
|
+
if (!comparisons || comparisons.length === 0)
|
|
336
|
+
return "";
|
|
337
|
+
const incomplete = comparisons.filter((c) => c.aiProcessingState &&
|
|
338
|
+
c.aiProcessingState !== "completed" &&
|
|
339
|
+
c.aiProcessingState !== "not_enabled");
|
|
340
|
+
if (incomplete.length === 0)
|
|
341
|
+
return "";
|
|
342
|
+
const total = comparisons.length;
|
|
343
|
+
return `> ⚠ AI analysis in progress for ${incomplete.length} of ${total} comparisons. Re-run for complete analysis.`;
|
|
344
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Percy authentication — uses BrowserStack Basic Auth for ALL Percy API calls.
|
|
3
|
+
*
|
|
4
|
+
* This is the correct auth method. The existing working tools (fetchPercyChanges,
|
|
5
|
+
* managePercyBuildApproval) all use Basic Auth successfully.
|
|
6
|
+
*
|
|
7
|
+
* Percy Token (PERCY_TOKEN) is only needed for:
|
|
8
|
+
* - percy CLI commands (percy exec, percy snapshot)
|
|
9
|
+
* - Direct build creation when no BrowserStack credentials available
|
|
10
|
+
*/
|
|
11
|
+
import { BrowserStackConfig } from "../types.js";
|
|
12
|
+
/**
|
|
13
|
+
* Get auth headers for Percy API calls.
|
|
14
|
+
* Uses BrowserStack Basic Auth (username:accessKey).
|
|
15
|
+
*/
|
|
16
|
+
export declare function getPercyAuthHeaders(config: BrowserStackConfig): Record<string, string>;
|
|
17
|
+
/**
|
|
18
|
+
* Get Percy Token auth headers (for token-scoped operations).
|
|
19
|
+
* Falls back to fetching token via BrowserStack API if not in env.
|
|
20
|
+
*/
|
|
21
|
+
export declare function getPercyTokenHeaders(token: string): Record<string, string>;
|
|
22
|
+
/**
|
|
23
|
+
* Make a GET request to Percy API with Basic Auth.
|
|
24
|
+
*/
|
|
25
|
+
export declare function percyGet(path: string, config: BrowserStackConfig, params?: Record<string, string>): Promise<any>;
|
|
26
|
+
/**
|
|
27
|
+
* Make a POST request to Percy API with Basic Auth.
|
|
28
|
+
*/
|
|
29
|
+
export declare function percyPost(path: string, config: BrowserStackConfig, body?: unknown): Promise<any>;
|
|
30
|
+
/**
|
|
31
|
+
* Make a PATCH request to Percy API with Basic Auth.
|
|
32
|
+
*/
|
|
33
|
+
export declare function percyPatch(path: string, config: BrowserStackConfig, body?: unknown): Promise<any>;
|
|
34
|
+
/**
|
|
35
|
+
* Make a POST to Percy API using Percy Token auth.
|
|
36
|
+
* Used for build creation when a project token is available.
|
|
37
|
+
*/
|
|
38
|
+
export declare function percyTokenPost(path: string, token: string, body?: unknown): Promise<any>;
|
|
39
|
+
/**
|
|
40
|
+
* Get or create a Percy project token via BrowserStack API.
|
|
41
|
+
* Creates the project if it doesn't exist.
|
|
42
|
+
*/
|
|
43
|
+
export declare function getOrCreateProjectToken(projectName: string, config: BrowserStackConfig, type?: string): Promise<string>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Percy authentication — uses BrowserStack Basic Auth for ALL Percy API calls.
|
|
3
|
+
*
|
|
4
|
+
* This is the correct auth method. The existing working tools (fetchPercyChanges,
|
|
5
|
+
* managePercyBuildApproval) all use Basic Auth successfully.
|
|
6
|
+
*
|
|
7
|
+
* Percy Token (PERCY_TOKEN) is only needed for:
|
|
8
|
+
* - percy CLI commands (percy exec, percy snapshot)
|
|
9
|
+
* - Direct build creation when no BrowserStack credentials available
|
|
10
|
+
*/
|
|
11
|
+
import { getBrowserStackAuth } from "../get-auth.js";
|
|
12
|
+
/**
|
|
13
|
+
* Get auth headers for Percy API calls.
|
|
14
|
+
* Uses BrowserStack Basic Auth (username:accessKey).
|
|
15
|
+
*/
|
|
16
|
+
export function getPercyAuthHeaders(config) {
|
|
17
|
+
const authString = getBrowserStackAuth(config);
|
|
18
|
+
const auth = Buffer.from(authString).toString("base64");
|
|
19
|
+
return {
|
|
20
|
+
Authorization: `Basic ${auth}`,
|
|
21
|
+
"Content-Type": "application/json",
|
|
22
|
+
"User-Agent": "browserstack-mcp-server",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Get Percy Token auth headers (for token-scoped operations).
|
|
27
|
+
* Falls back to fetching token via BrowserStack API if not in env.
|
|
28
|
+
*/
|
|
29
|
+
export function getPercyTokenHeaders(token) {
|
|
30
|
+
return {
|
|
31
|
+
Authorization: `Token token=${token}`,
|
|
32
|
+
"Content-Type": "application/json",
|
|
33
|
+
"User-Agent": "browserstack-mcp-server",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const PERCY_API_BASE = "https://percy.io/api/v1";
|
|
37
|
+
/**
|
|
38
|
+
* Make a GET request to Percy API with Basic Auth.
|
|
39
|
+
*/
|
|
40
|
+
export async function percyGet(path, config, params) {
|
|
41
|
+
const headers = getPercyAuthHeaders(config);
|
|
42
|
+
const url = new URL(`${PERCY_API_BASE}${path}`);
|
|
43
|
+
if (params) {
|
|
44
|
+
for (const [key, value] of Object.entries(params)) {
|
|
45
|
+
url.searchParams.set(key, value);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const response = await fetch(url.toString(), { headers });
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
const body = await response.text().catch(() => "");
|
|
51
|
+
throw new Error(`GET ${path}: ${response.status} ${response.statusText}. ${body}`);
|
|
52
|
+
}
|
|
53
|
+
if (response.status === 204)
|
|
54
|
+
return null;
|
|
55
|
+
return response.json();
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Make a POST request to Percy API with Basic Auth.
|
|
59
|
+
*/
|
|
60
|
+
export async function percyPost(path, config, body) {
|
|
61
|
+
const headers = getPercyAuthHeaders(config);
|
|
62
|
+
const url = `${PERCY_API_BASE}${path}`;
|
|
63
|
+
const response = await fetch(url, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers,
|
|
66
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
67
|
+
});
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
const responseBody = await response.text().catch(() => "");
|
|
70
|
+
throw new Error(`POST ${path}: ${response.status} ${response.statusText}. ${responseBody}`);
|
|
71
|
+
}
|
|
72
|
+
if (response.status === 204)
|
|
73
|
+
return null;
|
|
74
|
+
return response.json();
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Make a PATCH request to Percy API with Basic Auth.
|
|
78
|
+
*/
|
|
79
|
+
export async function percyPatch(path, config, body) {
|
|
80
|
+
const headers = getPercyAuthHeaders(config);
|
|
81
|
+
const url = `${PERCY_API_BASE}${path}`;
|
|
82
|
+
const response = await fetch(url, {
|
|
83
|
+
method: "PATCH",
|
|
84
|
+
headers,
|
|
85
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
86
|
+
});
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
const responseBody = await response.text().catch(() => "");
|
|
89
|
+
throw new Error(`PATCH ${path}: ${response.status} ${response.statusText}. ${responseBody}`);
|
|
90
|
+
}
|
|
91
|
+
if (response.status === 204)
|
|
92
|
+
return null;
|
|
93
|
+
return response.json();
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Make a POST to Percy API using Percy Token auth.
|
|
97
|
+
* Used for build creation when a project token is available.
|
|
98
|
+
*/
|
|
99
|
+
export async function percyTokenPost(path, token, body) {
|
|
100
|
+
const headers = getPercyTokenHeaders(token);
|
|
101
|
+
const url = `${PERCY_API_BASE}${path}`;
|
|
102
|
+
const response = await fetch(url, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers,
|
|
105
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
106
|
+
});
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
const responseBody = await response.text().catch(() => "");
|
|
109
|
+
throw new Error(`POST ${path}: ${response.status} ${response.statusText}. ${responseBody}`);
|
|
110
|
+
}
|
|
111
|
+
if (response.status === 204)
|
|
112
|
+
return null;
|
|
113
|
+
return response.json();
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Get or create a Percy project token via BrowserStack API.
|
|
117
|
+
* Creates the project if it doesn't exist.
|
|
118
|
+
*/
|
|
119
|
+
export async function getOrCreateProjectToken(projectName, config, type) {
|
|
120
|
+
const authString = getBrowserStackAuth(config);
|
|
121
|
+
const auth = Buffer.from(authString).toString("base64");
|
|
122
|
+
const params = new URLSearchParams({ name: projectName });
|
|
123
|
+
if (type)
|
|
124
|
+
params.append("type", type);
|
|
125
|
+
const url = `https://api.browserstack.com/api/app_percy/get_project_token?${params.toString()}`;
|
|
126
|
+
const response = await fetch(url, {
|
|
127
|
+
headers: { Authorization: `Basic ${auth}` },
|
|
128
|
+
});
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
throw new Error(`Failed to get token for project "${projectName}": ${response.status}`);
|
|
131
|
+
}
|
|
132
|
+
const data = await response.json();
|
|
133
|
+
if (!data?.token || !data?.success) {
|
|
134
|
+
throw new Error(`No token returned for project "${projectName}". Check the project name.`);
|
|
135
|
+
}
|
|
136
|
+
return data.token;
|
|
137
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Percy error handler — turns raw API errors into helpful guidance.
|
|
3
|
+
*
|
|
4
|
+
* Instead of showing "403 Forbidden" or "404 Not Found", returns:
|
|
5
|
+
* - What went wrong
|
|
6
|
+
* - What the correct input looks like
|
|
7
|
+
* - Suggested next steps
|
|
8
|
+
*/
|
|
9
|
+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
+
interface ToolParam {
|
|
11
|
+
name: string;
|
|
12
|
+
required: boolean;
|
|
13
|
+
description: string;
|
|
14
|
+
example: string;
|
|
15
|
+
}
|
|
16
|
+
interface ToolHelp {
|
|
17
|
+
name: string;
|
|
18
|
+
description: string;
|
|
19
|
+
params: ToolParam[];
|
|
20
|
+
examples: string[];
|
|
21
|
+
}
|
|
22
|
+
export declare function handlePercyToolError(error: unknown, toolHelp: ToolHelp, args: Record<string, unknown>): CallToolResult;
|
|
23
|
+
export declare const TOOL_HELP: Record<string, ToolHelp>;
|
|
24
|
+
export {};
|