@browserstack/mcp-server 1.2.15-beta.2 → 1.2.16-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server-factory.js +0 -4
- package/dist/tools/percy-sdk.js +20 -11
- package/dist/tools/testmanagement-utils/get-testplan.d.ts +16 -0
- package/dist/tools/testmanagement-utils/get-testplan.js +99 -0
- package/dist/tools/testmanagement-utils/list-folders.d.ts +16 -0
- package/dist/tools/testmanagement-utils/list-folders.js +77 -0
- package/dist/tools/testmanagement-utils/list-testcases.js +1 -1
- package/dist/tools/testmanagement-utils/list-testplans.d.ts +15 -0
- package/dist/tools/testmanagement-utils/list-testplans.js +75 -0
- package/dist/tools/testmanagement-utils/update-testcase.d.ts +16 -0
- package/dist/tools/testmanagement-utils/update-testcase.js +133 -10
- package/dist/tools/testmanagement.d.ts +15 -0
- package/dist/tools/testmanagement.js +73 -2
- package/package.json +2 -3
- package/dist/lib/percy-api/auth.d.ts +0 -41
- package/dist/lib/percy-api/auth.js +0 -96
- package/dist/lib/percy-api/cache.d.ts +0 -28
- package/dist/lib/percy-api/cache.js +0 -48
- package/dist/lib/percy-api/client.d.ts +0 -69
- package/dist/lib/percy-api/client.js +0 -275
- package/dist/lib/percy-api/errors.d.ts +0 -15
- package/dist/lib/percy-api/errors.js +0 -52
- package/dist/lib/percy-api/formatter.d.ts +0 -16
- package/dist/lib/percy-api/formatter.js +0 -344
- package/dist/lib/percy-api/percy-auth.d.ts +0 -43
- package/dist/lib/percy-api/percy-auth.js +0 -137
- package/dist/lib/percy-api/percy-error-handler.d.ts +0 -24
- package/dist/lib/percy-api/percy-error-handler.js +0 -302
- package/dist/lib/percy-api/percy-session.d.ts +0 -42
- package/dist/lib/percy-api/percy-session.js +0 -87
- package/dist/lib/percy-api/polling.d.ts +0 -26
- package/dist/lib/percy-api/polling.js +0 -42
- package/dist/lib/percy-api/types.d.ts +0 -56
- package/dist/lib/percy-api/types.js +0 -76
- package/dist/tools/percy-mcp/advanced/branchline-operations.d.ts +0 -16
- package/dist/tools/percy-mcp/advanced/branchline-operations.js +0 -81
- package/dist/tools/percy-mcp/advanced/manage-variants.d.ts +0 -16
- package/dist/tools/percy-mcp/advanced/manage-variants.js +0 -155
- package/dist/tools/percy-mcp/advanced/manage-visual-monitoring.d.ts +0 -16
- package/dist/tools/percy-mcp/advanced/manage-visual-monitoring.js +0 -171
- package/dist/tools/percy-mcp/auth/auth-status.d.ts +0 -3
- package/dist/tools/percy-mcp/auth/auth-status.js +0 -131
- package/dist/tools/percy-mcp/core/approve-build.d.ts +0 -14
- package/dist/tools/percy-mcp/core/approve-build.js +0 -97
- package/dist/tools/percy-mcp/core/get-build-items.d.ts +0 -13
- package/dist/tools/percy-mcp/core/get-build-items.js +0 -65
- package/dist/tools/percy-mcp/core/get-build.d.ts +0 -10
- package/dist/tools/percy-mcp/core/get-build.js +0 -16
- package/dist/tools/percy-mcp/core/get-comparison.d.ts +0 -11
- package/dist/tools/percy-mcp/core/get-comparison.js +0 -59
- package/dist/tools/percy-mcp/core/get-snapshot.d.ts +0 -10
- package/dist/tools/percy-mcp/core/get-snapshot.js +0 -40
- package/dist/tools/percy-mcp/core/list-builds.d.ts +0 -14
- package/dist/tools/percy-mcp/core/list-builds.js +0 -45
- package/dist/tools/percy-mcp/core/list-projects.d.ts +0 -12
- package/dist/tools/percy-mcp/core/list-projects.js +0 -51
- package/dist/tools/percy-mcp/creation/create-app-snapshot.d.ts +0 -12
- package/dist/tools/percy-mcp/creation/create-app-snapshot.js +0 -29
- package/dist/tools/percy-mcp/creation/create-build.d.ts +0 -19
- package/dist/tools/percy-mcp/creation/create-build.js +0 -68
- package/dist/tools/percy-mcp/creation/create-comparison.d.ts +0 -18
- package/dist/tools/percy-mcp/creation/create-comparison.js +0 -90
- package/dist/tools/percy-mcp/creation/create-snapshot.d.ts +0 -17
- package/dist/tools/percy-mcp/creation/create-snapshot.js +0 -99
- package/dist/tools/percy-mcp/creation/finalize-build.d.ts +0 -12
- package/dist/tools/percy-mcp/creation/finalize-build.js +0 -33
- package/dist/tools/percy-mcp/creation/finalize-comparison.d.ts +0 -10
- package/dist/tools/percy-mcp/creation/finalize-comparison.js +0 -16
- package/dist/tools/percy-mcp/creation/finalize-snapshot.d.ts +0 -12
- package/dist/tools/percy-mcp/creation/finalize-snapshot.js +0 -33
- package/dist/tools/percy-mcp/creation/upload-resource.d.ts +0 -15
- package/dist/tools/percy-mcp/creation/upload-resource.js +0 -43
- package/dist/tools/percy-mcp/creation/upload-tile.d.ts +0 -11
- package/dist/tools/percy-mcp/creation/upload-tile.js +0 -53
- package/dist/tools/percy-mcp/diagnostics/analyze-logs-realtime.d.ts +0 -13
- package/dist/tools/percy-mcp/diagnostics/analyze-logs-realtime.js +0 -65
- package/dist/tools/percy-mcp/diagnostics/get-build-logs.d.ts +0 -17
- package/dist/tools/percy-mcp/diagnostics/get-build-logs.js +0 -74
- package/dist/tools/percy-mcp/diagnostics/get-network-logs.d.ts +0 -5
- package/dist/tools/percy-mcp/diagnostics/get-network-logs.js +0 -21
- package/dist/tools/percy-mcp/diagnostics/get-suggestions.d.ts +0 -7
- package/dist/tools/percy-mcp/diagnostics/get-suggestions.js +0 -24
- package/dist/tools/percy-mcp/index.d.ts +0 -36
- package/dist/tools/percy-mcp/index.js +0 -1137
- package/dist/tools/percy-mcp/intelligence/get-ai-analysis.d.ts +0 -15
- package/dist/tools/percy-mcp/intelligence/get-ai-analysis.js +0 -166
- package/dist/tools/percy-mcp/intelligence/get-ai-quota.d.ts +0 -9
- package/dist/tools/percy-mcp/intelligence/get-ai-quota.js +0 -73
- package/dist/tools/percy-mcp/intelligence/get-build-summary.d.ts +0 -11
- package/dist/tools/percy-mcp/intelligence/get-build-summary.js +0 -78
- package/dist/tools/percy-mcp/intelligence/get-rca.d.ts +0 -6
- package/dist/tools/percy-mcp/intelligence/get-rca.js +0 -153
- package/dist/tools/percy-mcp/intelligence/suggest-prompt.d.ts +0 -15
- package/dist/tools/percy-mcp/intelligence/suggest-prompt.js +0 -86
- package/dist/tools/percy-mcp/intelligence/trigger-ai-recompute.d.ts +0 -16
- package/dist/tools/percy-mcp/intelligence/trigger-ai-recompute.js +0 -64
- package/dist/tools/percy-mcp/management/create-project.d.ts +0 -14
- package/dist/tools/percy-mcp/management/create-project.js +0 -52
- package/dist/tools/percy-mcp/management/get-usage-stats.d.ts +0 -12
- package/dist/tools/percy-mcp/management/get-usage-stats.js +0 -61
- package/dist/tools/percy-mcp/management/manage-browser-targets.d.ts +0 -12
- package/dist/tools/percy-mcp/management/manage-browser-targets.js +0 -136
- package/dist/tools/percy-mcp/management/manage-comments.d.ts +0 -14
- package/dist/tools/percy-mcp/management/manage-comments.js +0 -147
- package/dist/tools/percy-mcp/management/manage-ignored-regions.d.ts +0 -18
- package/dist/tools/percy-mcp/management/manage-ignored-regions.js +0 -182
- package/dist/tools/percy-mcp/management/manage-project-settings.d.ts +0 -16
- package/dist/tools/percy-mcp/management/manage-project-settings.js +0 -97
- package/dist/tools/percy-mcp/management/manage-tokens.d.ts +0 -14
- package/dist/tools/percy-mcp/management/manage-tokens.js +0 -90
- package/dist/tools/percy-mcp/management/manage-webhooks.d.ts +0 -15
- package/dist/tools/percy-mcp/management/manage-webhooks.js +0 -180
- package/dist/tools/percy-mcp/v2/auth-status.d.ts +0 -3
- package/dist/tools/percy-mcp/v2/auth-status.js +0 -80
- package/dist/tools/percy-mcp/v2/clone-build.d.ts +0 -24
- package/dist/tools/percy-mcp/v2/clone-build.js +0 -539
- package/dist/tools/percy-mcp/v2/create-app-build.d.ts +0 -28
- package/dist/tools/percy-mcp/v2/create-app-build.js +0 -442
- package/dist/tools/percy-mcp/v2/create-build.d.ts +0 -16
- package/dist/tools/percy-mcp/v2/create-build.js +0 -601
- package/dist/tools/percy-mcp/v2/create-project.d.ts +0 -8
- package/dist/tools/percy-mcp/v2/create-project.js +0 -33
- package/dist/tools/percy-mcp/v2/discover-urls.d.ts +0 -7
- package/dist/tools/percy-mcp/v2/discover-urls.js +0 -38
- package/dist/tools/percy-mcp/v2/figma-baseline.d.ts +0 -7
- package/dist/tools/percy-mcp/v2/figma-baseline.js +0 -18
- package/dist/tools/percy-mcp/v2/figma-build.d.ts +0 -7
- package/dist/tools/percy-mcp/v2/figma-build.js +0 -39
- package/dist/tools/percy-mcp/v2/figma-link.d.ts +0 -6
- package/dist/tools/percy-mcp/v2/figma-link.js +0 -27
- package/dist/tools/percy-mcp/v2/get-ai-summary.d.ts +0 -5
- package/dist/tools/percy-mcp/v2/get-ai-summary.js +0 -109
- package/dist/tools/percy-mcp/v2/get-build-detail.d.ts +0 -22
- package/dist/tools/percy-mcp/v2/get-build-detail.js +0 -567
- package/dist/tools/percy-mcp/v2/get-builds.d.ts +0 -8
- package/dist/tools/percy-mcp/v2/get-builds.js +0 -63
- package/dist/tools/percy-mcp/v2/get-comparison.d.ts +0 -5
- package/dist/tools/percy-mcp/v2/get-comparison.js +0 -94
- package/dist/tools/percy-mcp/v2/get-devices.d.ts +0 -5
- package/dist/tools/percy-mcp/v2/get-devices.js +0 -33
- package/dist/tools/percy-mcp/v2/get-insights.d.ts +0 -7
- package/dist/tools/percy-mcp/v2/get-insights.js +0 -52
- package/dist/tools/percy-mcp/v2/get-projects.d.ts +0 -6
- package/dist/tools/percy-mcp/v2/get-projects.js +0 -41
- package/dist/tools/percy-mcp/v2/get-snapshot.d.ts +0 -5
- package/dist/tools/percy-mcp/v2/get-snapshot.js +0 -96
- package/dist/tools/percy-mcp/v2/get-test-case-history.d.ts +0 -5
- package/dist/tools/percy-mcp/v2/get-test-case-history.js +0 -20
- package/dist/tools/percy-mcp/v2/get-test-cases.d.ts +0 -6
- package/dist/tools/percy-mcp/v2/get-test-cases.js +0 -36
- package/dist/tools/percy-mcp/v2/index.d.ts +0 -35
- package/dist/tools/percy-mcp/v2/index.js +0 -544
- package/dist/tools/percy-mcp/v2/list-integrations.d.ts +0 -5
- package/dist/tools/percy-mcp/v2/list-integrations.js +0 -41
- package/dist/tools/percy-mcp/v2/manage-domains.d.ts +0 -8
- package/dist/tools/percy-mcp/v2/manage-domains.js +0 -33
- package/dist/tools/percy-mcp/v2/manage-insights-email.d.ts +0 -8
- package/dist/tools/percy-mcp/v2/manage-insights-email.js +0 -49
- package/dist/tools/percy-mcp/v2/manage-usage-alerts.d.ts +0 -10
- package/dist/tools/percy-mcp/v2/manage-usage-alerts.js +0 -43
- package/dist/tools/percy-mcp/v2/migrate-integrations.d.ts +0 -6
- package/dist/tools/percy-mcp/v2/migrate-integrations.js +0 -20
- package/dist/tools/percy-mcp/v2/preview-comparison.d.ts +0 -5
- package/dist/tools/percy-mcp/v2/preview-comparison.js +0 -17
- package/dist/tools/percy-mcp/v2/search-build-items.d.ts +0 -12
- package/dist/tools/percy-mcp/v2/search-build-items.js +0 -45
- package/dist/tools/percy-mcp/workflows/auto-triage.d.ts +0 -7
- package/dist/tools/percy-mcp/workflows/auto-triage.js +0 -82
- package/dist/tools/percy-mcp/workflows/clone-build.d.ts +0 -22
- package/dist/tools/percy-mcp/workflows/clone-build.js +0 -414
- package/dist/tools/percy-mcp/workflows/create-percy-build.d.ts +0 -32
- package/dist/tools/percy-mcp/workflows/create-percy-build.js +0 -434
- package/dist/tools/percy-mcp/workflows/debug-failed-build.d.ts +0 -5
- package/dist/tools/percy-mcp/workflows/debug-failed-build.js +0 -122
- package/dist/tools/percy-mcp/workflows/diff-explain.d.ts +0 -6
- package/dist/tools/percy-mcp/workflows/diff-explain.js +0 -147
- package/dist/tools/percy-mcp/workflows/pr-visual-report.d.ts +0 -8
- package/dist/tools/percy-mcp/workflows/pr-visual-report.js +0 -184
- package/dist/tools/percy-mcp/workflows/run-tests.d.ts +0 -17
- package/dist/tools/percy-mcp/workflows/run-tests.js +0 -107
- package/dist/tools/percy-mcp/workflows/snapshot-urls.d.ts +0 -18
- package/dist/tools/percy-mcp/workflows/snapshot-urls.js +0 -197
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import { PercyClient } from "../../../lib/percy-api/client.js";
|
|
2
|
-
import { pollUntil } from "../../../lib/percy-api/polling.js";
|
|
3
|
-
export async function percyDiffExplain(args, config) {
|
|
4
|
-
const client = new PercyClient(config);
|
|
5
|
-
const depth = args.depth || "detailed"; // summary, detailed, full_rca
|
|
6
|
-
// Get comparison with AI data
|
|
7
|
-
const comparison = await client.get(`/comparisons/${args.comparison_id}`, {}, [
|
|
8
|
-
"head-screenshot.image",
|
|
9
|
-
"base-screenshot.image",
|
|
10
|
-
"diff-image",
|
|
11
|
-
"ai-diff-image",
|
|
12
|
-
"browser.browser-family",
|
|
13
|
-
"comparison-tag",
|
|
14
|
-
]);
|
|
15
|
-
if (!comparison) {
|
|
16
|
-
return {
|
|
17
|
-
content: [{ type: "text", text: "Comparison not found." }],
|
|
18
|
-
isError: true,
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
let output = `## Visual Diff Explanation — Comparison #${args.comparison_id}\n\n`;
|
|
22
|
-
// Basic diff info
|
|
23
|
-
const diffRatio = comparison.diffRatio ?? 0;
|
|
24
|
-
const aiDiffRatio = comparison.aiDiffRatio;
|
|
25
|
-
output += `**Diff:** ${(diffRatio * 100).toFixed(1)}%`;
|
|
26
|
-
if (aiDiffRatio !== null && aiDiffRatio !== undefined) {
|
|
27
|
-
output += ` | **AI Diff:** ${(aiDiffRatio * 100).toFixed(1)}%`;
|
|
28
|
-
const reduction = diffRatio > 0 ? ((1 - aiDiffRatio / diffRatio) * 100).toFixed(0) : "0";
|
|
29
|
-
output += ` (${reduction}% noise filtered)`;
|
|
30
|
-
}
|
|
31
|
-
output += "\n\n";
|
|
32
|
-
// Summary depth: AI descriptions only
|
|
33
|
-
const regions = comparison.appliedRegions || [];
|
|
34
|
-
if (regions.length > 0) {
|
|
35
|
-
output += `### What Changed (${regions.length} regions)\n\n`;
|
|
36
|
-
regions.forEach((region, i) => {
|
|
37
|
-
const type = region.change_type || region.changeType || "unknown";
|
|
38
|
-
const title = region.change_title || region.changeTitle || "Untitled change";
|
|
39
|
-
const desc = region.change_description || region.changeDescription || "";
|
|
40
|
-
const reason = region.change_reason || region.changeReason || "";
|
|
41
|
-
const ignored = region.ignored;
|
|
42
|
-
output += `${i + 1}. ${ignored ? "~~" : "**"}${title}${ignored ? "~~" : "**"} (${type})`;
|
|
43
|
-
if (ignored)
|
|
44
|
-
output += " — *ignored by AI*";
|
|
45
|
-
output += "\n";
|
|
46
|
-
if (desc && depth !== "summary")
|
|
47
|
-
output += ` ${desc}\n`;
|
|
48
|
-
if (reason && depth !== "summary")
|
|
49
|
-
output += ` *Reason: ${reason}*\n`;
|
|
50
|
-
output += "\n";
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
else if (diffRatio > 0) {
|
|
54
|
-
output +=
|
|
55
|
-
"No AI region data available. Visual diff detected but not yet analyzed by AI.\n\n";
|
|
56
|
-
}
|
|
57
|
-
else {
|
|
58
|
-
output += "No visual differences detected.\n\n";
|
|
59
|
-
}
|
|
60
|
-
// Detailed depth: + coordinates
|
|
61
|
-
if (depth === "detailed" || depth === "full_rca") {
|
|
62
|
-
const coords = comparison.diffRects || comparison.aiDiffRects || [];
|
|
63
|
-
if (coords.length > 0) {
|
|
64
|
-
output += `### Diff Regions (coordinates)\n\n`;
|
|
65
|
-
coords.forEach((rect, i) => {
|
|
66
|
-
output += `${i + 1}. (${rect.x || rect.left || 0}, ${rect.y || rect.top || 0}) → (${rect.right || rect.x2 || 0}, ${rect.bottom || rect.y2 || 0})\n`;
|
|
67
|
-
});
|
|
68
|
-
output += "\n";
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
// Full RCA depth: + DOM/CSS changes
|
|
72
|
-
if (depth === "full_rca") {
|
|
73
|
-
output += `### Root Cause Analysis\n\n`;
|
|
74
|
-
try {
|
|
75
|
-
// Check if RCA exists, trigger if needed
|
|
76
|
-
let rcaData;
|
|
77
|
-
try {
|
|
78
|
-
rcaData = await client.get("/rca", {
|
|
79
|
-
comparison_id: args.comparison_id,
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
catch (e) {
|
|
83
|
-
if (e.statusCode === 404) {
|
|
84
|
-
// Trigger RCA
|
|
85
|
-
await client.post("/rca", {
|
|
86
|
-
data: {
|
|
87
|
-
type: "rca",
|
|
88
|
-
attributes: { "comparison-id": args.comparison_id },
|
|
89
|
-
},
|
|
90
|
-
});
|
|
91
|
-
// Poll for result (max 30s for inline use)
|
|
92
|
-
rcaData = await pollUntil(async () => {
|
|
93
|
-
const data = await client.get("/rca", {
|
|
94
|
-
comparison_id: args.comparison_id,
|
|
95
|
-
});
|
|
96
|
-
if (data?.status === "finished" || data?.status === "failed")
|
|
97
|
-
return { done: true, result: data };
|
|
98
|
-
return { done: false };
|
|
99
|
-
}, { maxTimeoutMs: 30000 });
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
throw e;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
if (rcaData?.status === "finished" && rcaData?.diffNodes) {
|
|
106
|
-
const nodes = rcaData.diffNodes;
|
|
107
|
-
const commonDiffs = nodes.common_diffs || [];
|
|
108
|
-
if (commonDiffs.length > 0) {
|
|
109
|
-
commonDiffs.slice(0, 10).forEach((diff, i) => {
|
|
110
|
-
const base = diff.base || {};
|
|
111
|
-
const head = diff.head || {};
|
|
112
|
-
const tag = head.tagName || base.tagName || "element";
|
|
113
|
-
const xpath = head.xpath || base.xpath || "";
|
|
114
|
-
output += `${i + 1}. **${tag}**`;
|
|
115
|
-
if (xpath)
|
|
116
|
-
output += ` — \`${xpath}\``;
|
|
117
|
-
output += "\n";
|
|
118
|
-
const baseAttrs = base.attributes || {};
|
|
119
|
-
const headAttrs = head.attributes || {};
|
|
120
|
-
for (const key of Object.keys(headAttrs)) {
|
|
121
|
-
if (JSON.stringify(baseAttrs[key]) !==
|
|
122
|
-
JSON.stringify(headAttrs[key])) {
|
|
123
|
-
output += ` ${key}: \`${baseAttrs[key] ?? "none"}\` → \`${headAttrs[key]}\`\n`;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
output += "\n";
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
output += "No DOM-level differences identified by RCA.\n";
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
else if (rcaData?.status === "failed") {
|
|
134
|
-
output +=
|
|
135
|
-
"RCA analysis failed — comparison may not have DOM metadata.\n";
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
output +=
|
|
139
|
-
"RCA analysis is still processing. Re-run with depth=full_rca later.\n";
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
catch (e) {
|
|
143
|
-
output += `RCA unavailable: ${e.message}. Falling back to AI-only analysis.\n`;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
return { content: [{ type: "text", text: output }] };
|
|
147
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { BrowserStackConfig } from "../../../lib/types.js";
|
|
2
|
-
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
-
export declare function percyPrVisualReport(args: {
|
|
4
|
-
project_id?: string;
|
|
5
|
-
branch?: string;
|
|
6
|
-
sha?: string;
|
|
7
|
-
build_id?: string;
|
|
8
|
-
}, config: BrowserStackConfig): Promise<CallToolResult>;
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
import { PercyClient } from "../../../lib/percy-api/client.js";
|
|
2
|
-
import { percyCache } from "../../../lib/percy-api/cache.js";
|
|
3
|
-
import { formatBuild } from "../../../lib/percy-api/formatter.js";
|
|
4
|
-
export async function percyPrVisualReport(args, config) {
|
|
5
|
-
const client = new PercyClient(config);
|
|
6
|
-
const errors = [];
|
|
7
|
-
// Step 1: Resolve build
|
|
8
|
-
let build;
|
|
9
|
-
try {
|
|
10
|
-
if (args.build_id) {
|
|
11
|
-
build = await client.get(`/builds/${args.build_id}`, { "include-metadata": "true" }, ["build-summary", "browsers"]);
|
|
12
|
-
}
|
|
13
|
-
else {
|
|
14
|
-
// Find build by branch or SHA
|
|
15
|
-
const params = {};
|
|
16
|
-
if (args.project_id) {
|
|
17
|
-
// Use project-scoped endpoint
|
|
18
|
-
}
|
|
19
|
-
if (args.branch)
|
|
20
|
-
params["filter[branch]"] = args.branch;
|
|
21
|
-
if (args.sha)
|
|
22
|
-
params["filter[sha]"] = args.sha;
|
|
23
|
-
params["page[limit]"] = "1";
|
|
24
|
-
const builds = await client.get("/builds", params);
|
|
25
|
-
const buildList = Array.isArray(builds)
|
|
26
|
-
? builds
|
|
27
|
-
: builds?.data
|
|
28
|
-
? Array.isArray(builds.data)
|
|
29
|
-
? builds.data
|
|
30
|
-
: [builds.data]
|
|
31
|
-
: [];
|
|
32
|
-
if (buildList.length === 0) {
|
|
33
|
-
const identifier = args.branch
|
|
34
|
-
? `branch '${args.branch}'`
|
|
35
|
-
: args.sha
|
|
36
|
-
? `SHA '${args.sha}'`
|
|
37
|
-
: "the given filters";
|
|
38
|
-
return {
|
|
39
|
-
content: [
|
|
40
|
-
{
|
|
41
|
-
type: "text",
|
|
42
|
-
text: `No Percy build found for ${identifier}. Ensure a Percy build has been created for this branch/commit.`,
|
|
43
|
-
},
|
|
44
|
-
],
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
const buildId = buildList[0]?.id || buildList[0];
|
|
48
|
-
build = await client.get(`/builds/${typeof buildId === "object" ? buildId.id : buildId}`, { "include-metadata": "true" }, ["build-summary", "browsers"]);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
catch (e) {
|
|
52
|
-
return {
|
|
53
|
-
content: [{ type: "text", text: `Failed to fetch build: ${e.message}` }],
|
|
54
|
-
isError: true,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
if (!build) {
|
|
58
|
-
return {
|
|
59
|
-
content: [{ type: "text", text: "Build not found." }],
|
|
60
|
-
isError: true,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
// Cache build data for other composite tools
|
|
64
|
-
percyCache.set(`build:${build.id}`, build);
|
|
65
|
-
// Step 2: Build header with state awareness
|
|
66
|
-
let output = "";
|
|
67
|
-
const state = build.state || "unknown";
|
|
68
|
-
output += `# Percy Visual Regression Report\n\n`;
|
|
69
|
-
output += formatBuild(build);
|
|
70
|
-
// Step 3: Get build summary if available
|
|
71
|
-
const buildSummary = build.buildSummary;
|
|
72
|
-
if (buildSummary?.summary) {
|
|
73
|
-
try {
|
|
74
|
-
const summaryData = typeof buildSummary.summary === "string"
|
|
75
|
-
? JSON.parse(buildSummary.summary)
|
|
76
|
-
: buildSummary.summary;
|
|
77
|
-
if (summaryData?.title || summaryData?.items) {
|
|
78
|
-
output += `\n### AI Build Summary\n\n`;
|
|
79
|
-
if (summaryData.title)
|
|
80
|
-
output += `> ${summaryData.title}\n\n`;
|
|
81
|
-
if (Array.isArray(summaryData.items)) {
|
|
82
|
-
summaryData.items.forEach((item) => {
|
|
83
|
-
output += `- ${item.title || item}\n`;
|
|
84
|
-
});
|
|
85
|
-
output += "\n";
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
90
|
-
// Summary parse failed, skip
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
// Step 4: Get changed build items
|
|
94
|
-
if (state === "finished" || state === "processing") {
|
|
95
|
-
let items = [];
|
|
96
|
-
try {
|
|
97
|
-
const itemsData = await client.get("/build-items", {
|
|
98
|
-
"filter[build-id]": build.id,
|
|
99
|
-
"filter[category]": "changed",
|
|
100
|
-
"page[limit]": "30",
|
|
101
|
-
});
|
|
102
|
-
items = Array.isArray(itemsData) ? itemsData : [];
|
|
103
|
-
}
|
|
104
|
-
catch (e) {
|
|
105
|
-
errors.push(`[Failed to load changed snapshots: ${e.message}]`);
|
|
106
|
-
}
|
|
107
|
-
if (items.length === 0 && errors.length === 0) {
|
|
108
|
-
output += `\n### No Visual Changes Detected\n\nAll snapshots match the baseline.\n`;
|
|
109
|
-
}
|
|
110
|
-
else if (items.length > 0) {
|
|
111
|
-
// Step 5: Rank by risk
|
|
112
|
-
// Critical: AI bug flags > Review: high diff > Expected: content changes > Noise: low diff
|
|
113
|
-
const critical = [];
|
|
114
|
-
const review = [];
|
|
115
|
-
const expected = [];
|
|
116
|
-
const noise = [];
|
|
117
|
-
for (const item of items) {
|
|
118
|
-
const name = item.name || item.snapshotName || "Unknown";
|
|
119
|
-
const diffRatio = item.diffRatio ?? item.maxDiffRatio ?? 0;
|
|
120
|
-
const potentialBugs = item.totalPotentialBugs || item.aiDetails?.totalPotentialBugs || 0;
|
|
121
|
-
const entry = { name, diffRatio, potentialBugs, item };
|
|
122
|
-
if (potentialBugs > 0) {
|
|
123
|
-
critical.push(entry);
|
|
124
|
-
}
|
|
125
|
-
else if (diffRatio > 0.15) {
|
|
126
|
-
review.push(entry);
|
|
127
|
-
}
|
|
128
|
-
else if (diffRatio > 0.005) {
|
|
129
|
-
expected.push(entry);
|
|
130
|
-
}
|
|
131
|
-
else {
|
|
132
|
-
noise.push(entry);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
output += `\n### Changed Snapshots (${items.length})\n\n`;
|
|
136
|
-
if (critical.length > 0) {
|
|
137
|
-
output += `**CRITICAL — Potential Bugs (${critical.length}):**\n`;
|
|
138
|
-
critical.forEach((e, i) => {
|
|
139
|
-
output += `${i + 1}. **${e.name}** — ${(e.diffRatio * 100).toFixed(1)}% diff, ${e.potentialBugs} bug(s) flagged\n`;
|
|
140
|
-
});
|
|
141
|
-
output += "\n";
|
|
142
|
-
}
|
|
143
|
-
if (review.length > 0) {
|
|
144
|
-
output += `**REVIEW REQUIRED (${review.length}):**\n`;
|
|
145
|
-
review.forEach((e, i) => {
|
|
146
|
-
output += `${i + 1}. **${e.name}** — ${(e.diffRatio * 100).toFixed(1)}% diff\n`;
|
|
147
|
-
});
|
|
148
|
-
output += "\n";
|
|
149
|
-
}
|
|
150
|
-
if (expected.length > 0) {
|
|
151
|
-
output += `**EXPECTED CHANGES (${expected.length}):**\n`;
|
|
152
|
-
expected.forEach((e, i) => {
|
|
153
|
-
output += `${i + 1}. ${e.name} — ${(e.diffRatio * 100).toFixed(1)}% diff\n`;
|
|
154
|
-
});
|
|
155
|
-
output += "\n";
|
|
156
|
-
}
|
|
157
|
-
if (noise.length > 0) {
|
|
158
|
-
output += `**NOISE (${noise.length}):** ${noise.map((e) => e.name).join(", ")}\n\n`;
|
|
159
|
-
}
|
|
160
|
-
// Recommendation
|
|
161
|
-
output += `### Recommendation\n\n`;
|
|
162
|
-
if (critical.length > 0) {
|
|
163
|
-
output += `Review ${critical.length} critical item(s) before approving. `;
|
|
164
|
-
}
|
|
165
|
-
if (review.length > 0) {
|
|
166
|
-
output += `${review.length} item(s) need manual review. `;
|
|
167
|
-
}
|
|
168
|
-
if (expected.length + noise.length > 0 &&
|
|
169
|
-
critical.length === 0 &&
|
|
170
|
-
review.length === 0) {
|
|
171
|
-
output += `All changes appear expected or are noise. Safe to approve.`;
|
|
172
|
-
}
|
|
173
|
-
output += "\n";
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
// Add any sub-call errors
|
|
177
|
-
if (errors.length > 0) {
|
|
178
|
-
output += `\n### Partial Results\n\n`;
|
|
179
|
-
errors.forEach((err) => {
|
|
180
|
-
output += `- ${err}\n`;
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
return { content: [{ type: "text", text: output }] };
|
|
184
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* percy_run_tests — Execute a test command with Percy visual testing.
|
|
3
|
-
*
|
|
4
|
-
* Wraps any test command with `percy exec` to capture snapshots during tests.
|
|
5
|
-
* Fire-and-forget: launches in background, returns immediately.
|
|
6
|
-
*
|
|
7
|
-
* Requires @percy/cli installed locally.
|
|
8
|
-
*/
|
|
9
|
-
import { BrowserStackConfig } from "../../../lib/types.js";
|
|
10
|
-
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
11
|
-
interface RunTestsArgs {
|
|
12
|
-
project_name: string;
|
|
13
|
-
test_command: string;
|
|
14
|
-
type?: string;
|
|
15
|
-
}
|
|
16
|
-
export declare function percyRunTests(args: RunTestsArgs, config: BrowserStackConfig): Promise<CallToolResult>;
|
|
17
|
-
export {};
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* percy_run_tests — Execute a test command with Percy visual testing.
|
|
3
|
-
*
|
|
4
|
-
* Wraps any test command with `percy exec` to capture snapshots during tests.
|
|
5
|
-
* Fire-and-forget: launches in background, returns immediately.
|
|
6
|
-
*
|
|
7
|
-
* Requires @percy/cli installed locally.
|
|
8
|
-
*/
|
|
9
|
-
import { execFile, spawn } from "child_process";
|
|
10
|
-
import { promisify } from "util";
|
|
11
|
-
const execFileAsync = promisify(execFile);
|
|
12
|
-
async function getProjectToken(projectName, config) {
|
|
13
|
-
const authString = `${config["browserstack-username"]}:${config["browserstack-access-key"]}`;
|
|
14
|
-
const auth = Buffer.from(authString).toString("base64");
|
|
15
|
-
const url = `https://api.browserstack.com/api/app_percy/get_project_token?name=${encodeURIComponent(projectName)}`;
|
|
16
|
-
const response = await fetch(url, {
|
|
17
|
-
headers: { Authorization: `Basic ${auth}` },
|
|
18
|
-
});
|
|
19
|
-
if (!response.ok)
|
|
20
|
-
throw new Error(`Failed to get token for "${projectName}"`);
|
|
21
|
-
const data = await response.json();
|
|
22
|
-
if (!data?.token || !data?.success)
|
|
23
|
-
throw new Error(`No token for "${projectName}"`);
|
|
24
|
-
return data.token;
|
|
25
|
-
}
|
|
26
|
-
export async function percyRunTests(args, config) {
|
|
27
|
-
const { project_name, test_command } = args;
|
|
28
|
-
let output = `## Percy Test Run — Local Execution\n\n`;
|
|
29
|
-
// Check Percy CLI
|
|
30
|
-
try {
|
|
31
|
-
await execFileAsync("npx", ["@percy/cli", "--version"]);
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
output += `**Percy CLI not found.** Install it:\n\n`;
|
|
35
|
-
output += `\`\`\`bash\nnpm install -g @percy/cli\n\`\`\`\n`;
|
|
36
|
-
return { content: [{ type: "text", text: output }] };
|
|
37
|
-
}
|
|
38
|
-
// Get token
|
|
39
|
-
let token;
|
|
40
|
-
try {
|
|
41
|
-
token = await getProjectToken(project_name, config);
|
|
42
|
-
}
|
|
43
|
-
catch (e) {
|
|
44
|
-
return {
|
|
45
|
-
content: [
|
|
46
|
-
{
|
|
47
|
-
type: "text",
|
|
48
|
-
text: `Failed to get project token: ${e.message}`,
|
|
49
|
-
},
|
|
50
|
-
],
|
|
51
|
-
isError: true,
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
output += `**Project:** ${project_name}\n`;
|
|
55
|
-
output += `**Command:** \`${test_command}\`\n\n`;
|
|
56
|
-
// Parse the test command into args
|
|
57
|
-
const cmdParts = test_command.split(" ").filter(Boolean);
|
|
58
|
-
// Spawn: npx @percy/cli exec -- <test_command>
|
|
59
|
-
const child = spawn("npx", ["@percy/cli", "exec", "--", ...cmdParts], {
|
|
60
|
-
env: { ...process.env, PERCY_TOKEN: token },
|
|
61
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
62
|
-
detached: true,
|
|
63
|
-
shell: false,
|
|
64
|
-
});
|
|
65
|
-
let stdoutData = "";
|
|
66
|
-
let buildUrl = "";
|
|
67
|
-
child.stdout?.on("data", (data) => {
|
|
68
|
-
const text = data.toString();
|
|
69
|
-
stdoutData += text;
|
|
70
|
-
const match = text.match(/https:\/\/percy\.io\/[^\s]+\/builds\/\d+/);
|
|
71
|
-
if (match)
|
|
72
|
-
buildUrl = match[0];
|
|
73
|
-
});
|
|
74
|
-
child.stderr?.on("data", (data) => {
|
|
75
|
-
stdoutData += data.toString();
|
|
76
|
-
});
|
|
77
|
-
// Wait briefly for build URL
|
|
78
|
-
await new Promise((resolve) => {
|
|
79
|
-
const timeout = setTimeout(() => resolve(), 10000);
|
|
80
|
-
child.on("close", () => {
|
|
81
|
-
clearTimeout(timeout);
|
|
82
|
-
resolve();
|
|
83
|
-
});
|
|
84
|
-
const check = setInterval(() => {
|
|
85
|
-
if (buildUrl) {
|
|
86
|
-
clearTimeout(timeout);
|
|
87
|
-
clearInterval(check);
|
|
88
|
-
resolve();
|
|
89
|
-
}
|
|
90
|
-
}, 500);
|
|
91
|
-
});
|
|
92
|
-
child.unref();
|
|
93
|
-
if (buildUrl) {
|
|
94
|
-
output += `**Build started!** Tests are running with Percy in the background.\n\n`;
|
|
95
|
-
output += `**Build URL:** ${buildUrl}\n\n`;
|
|
96
|
-
output += `Your tests are executing. Each \`percySnapshot()\` call in your tests captures a visual snapshot.\n`;
|
|
97
|
-
output += `Results will appear at the build URL when tests complete.\n`;
|
|
98
|
-
}
|
|
99
|
-
else {
|
|
100
|
-
const trimmed = stdoutData.trim().slice(0, 500);
|
|
101
|
-
if (trimmed) {
|
|
102
|
-
output += `**Percy output:**\n\`\`\`\n${trimmed}\n\`\`\`\n\n`;
|
|
103
|
-
}
|
|
104
|
-
output += `Tests are running in the background with Percy. Check your Percy dashboard for the build.\n`;
|
|
105
|
-
}
|
|
106
|
-
return { content: [{ type: "text", text: output }] };
|
|
107
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* percy_snapshot_urls — Actually runs Percy CLI to snapshot URLs locally.
|
|
3
|
-
*
|
|
4
|
-
* Fire-and-forget: launches percy CLI in background, returns immediately
|
|
5
|
-
* with build URL. User checks Percy dashboard for results.
|
|
6
|
-
*
|
|
7
|
-
* Requires @percy/cli installed locally (npx or global).
|
|
8
|
-
*/
|
|
9
|
-
import { BrowserStackConfig } from "../../../lib/types.js";
|
|
10
|
-
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
11
|
-
interface SnapshotUrlsArgs {
|
|
12
|
-
project_name: string;
|
|
13
|
-
urls: string;
|
|
14
|
-
widths?: string;
|
|
15
|
-
type?: string;
|
|
16
|
-
}
|
|
17
|
-
export declare function percySnapshotUrls(args: SnapshotUrlsArgs, config: BrowserStackConfig): Promise<CallToolResult>;
|
|
18
|
-
export {};
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* percy_snapshot_urls — Actually runs Percy CLI to snapshot URLs locally.
|
|
3
|
-
*
|
|
4
|
-
* Fire-and-forget: launches percy CLI in background, returns immediately
|
|
5
|
-
* with build URL. User checks Percy dashboard for results.
|
|
6
|
-
*
|
|
7
|
-
* Requires @percy/cli installed locally (npx or global).
|
|
8
|
-
*/
|
|
9
|
-
import { execFile, spawn } from "child_process";
|
|
10
|
-
import { promisify } from "util";
|
|
11
|
-
import { writeFile, unlink, mkdtemp } from "fs/promises";
|
|
12
|
-
import { join } from "path";
|
|
13
|
-
import { tmpdir } from "os";
|
|
14
|
-
const execFileAsync = promisify(execFile);
|
|
15
|
-
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
16
|
-
async function getProjectToken(projectName, config, type) {
|
|
17
|
-
const authString = `${config["browserstack-username"]}:${config["browserstack-access-key"]}`;
|
|
18
|
-
const auth = Buffer.from(authString).toString("base64");
|
|
19
|
-
const params = new URLSearchParams({ name: projectName });
|
|
20
|
-
if (type)
|
|
21
|
-
params.append("type", type);
|
|
22
|
-
const url = `https://api.browserstack.com/api/app_percy/get_project_token?${params.toString()}`;
|
|
23
|
-
const response = await fetch(url, {
|
|
24
|
-
headers: { Authorization: `Basic ${auth}` },
|
|
25
|
-
});
|
|
26
|
-
if (!response.ok)
|
|
27
|
-
throw new Error(`Failed to get token for "${projectName}"`);
|
|
28
|
-
const data = await response.json();
|
|
29
|
-
if (!data?.token || !data?.success)
|
|
30
|
-
throw new Error(`No token for "${projectName}"`);
|
|
31
|
-
return data.token;
|
|
32
|
-
}
|
|
33
|
-
async function checkPercyCli() {
|
|
34
|
-
// Check if @percy/cli is available
|
|
35
|
-
try {
|
|
36
|
-
const { stdout } = await execFileAsync("npx", ["@percy/cli", "--version"]);
|
|
37
|
-
return stdout.trim();
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
// Try global
|
|
41
|
-
try {
|
|
42
|
-
const { stdout } = await execFileAsync("percy", ["--version"]);
|
|
43
|
-
return stdout.trim();
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
export async function percySnapshotUrls(args, config) {
|
|
51
|
-
const urls = args.urls
|
|
52
|
-
.split(",")
|
|
53
|
-
.map((u) => u.trim())
|
|
54
|
-
.filter(Boolean);
|
|
55
|
-
const widths = args.widths
|
|
56
|
-
? args.widths.split(",").map((w) => w.trim())
|
|
57
|
-
: ["375", "1280"];
|
|
58
|
-
if (urls.length === 0) {
|
|
59
|
-
return {
|
|
60
|
-
content: [{ type: "text", text: "No URLs provided." }],
|
|
61
|
-
isError: true,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
let output = `## Percy Snapshot — Local Rendering\n\n`;
|
|
65
|
-
// Step 1: Check Percy CLI
|
|
66
|
-
const cliVersion = await checkPercyCli();
|
|
67
|
-
if (!cliVersion) {
|
|
68
|
-
output += `**Percy CLI not found.** Install it first:\n\n`;
|
|
69
|
-
output += `\`\`\`bash\nnpm install -g @percy/cli\n\`\`\`\n\n`;
|
|
70
|
-
output += `Or install locally: \`npm install --save-dev @percy/cli\`\n`;
|
|
71
|
-
return { content: [{ type: "text", text: output }] };
|
|
72
|
-
}
|
|
73
|
-
output += `**Percy CLI:** ${cliVersion}\n`;
|
|
74
|
-
// Step 2: Get project token
|
|
75
|
-
let token;
|
|
76
|
-
try {
|
|
77
|
-
token = await getProjectToken(args.project_name, config, args.type);
|
|
78
|
-
}
|
|
79
|
-
catch (e) {
|
|
80
|
-
return {
|
|
81
|
-
content: [
|
|
82
|
-
{
|
|
83
|
-
type: "text",
|
|
84
|
-
text: `Failed to get project token: ${e.message}`,
|
|
85
|
-
},
|
|
86
|
-
],
|
|
87
|
-
isError: true,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
output += `**Project:** ${args.project_name}\n`;
|
|
91
|
-
output += `**URLs:** ${urls.length}\n`;
|
|
92
|
-
output += `**Widths:** ${widths.join(", ")}px\n\n`;
|
|
93
|
-
// Step 3: Create snapshots.yml config
|
|
94
|
-
let yamlContent = "";
|
|
95
|
-
urls.forEach((url, i) => {
|
|
96
|
-
const name = urls.length === 1
|
|
97
|
-
? "Homepage"
|
|
98
|
-
: url
|
|
99
|
-
.replace(/^https?:\/\//, "")
|
|
100
|
-
.replace(/[/:]/g, "-")
|
|
101
|
-
.replace(/-+/g, "-")
|
|
102
|
-
.replace(/^-|-$/g, "") || `Page ${i + 1}`;
|
|
103
|
-
yamlContent += `- name: "${name}"\n`;
|
|
104
|
-
yamlContent += ` url: ${url}\n`;
|
|
105
|
-
yamlContent += ` waitForTimeout: 3000\n`;
|
|
106
|
-
yamlContent += ` additionalSnapshots:\n`;
|
|
107
|
-
widths.forEach((w) => {
|
|
108
|
-
yamlContent += ` - width: ${w}\n`;
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
// Write temp config file
|
|
112
|
-
const tmpDir = await mkdtemp(join(tmpdir(), "percy-mcp-"));
|
|
113
|
-
const configPath = join(tmpDir, "snapshots.yml");
|
|
114
|
-
await writeFile(configPath, yamlContent, "utf-8");
|
|
115
|
-
// Step 4: Launch Percy CLI in background
|
|
116
|
-
output += `### Launching Percy snapshot...\n\n`;
|
|
117
|
-
const env = {
|
|
118
|
-
...process.env,
|
|
119
|
-
PERCY_TOKEN: token,
|
|
120
|
-
};
|
|
121
|
-
// Spawn percy CLI in background (fire and forget)
|
|
122
|
-
const child = spawn("npx", ["@percy/cli", "snapshot", configPath], {
|
|
123
|
-
env,
|
|
124
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
125
|
-
detached: true,
|
|
126
|
-
});
|
|
127
|
-
// Collect initial output for a few seconds
|
|
128
|
-
let stdoutData = "";
|
|
129
|
-
let stderrData = "";
|
|
130
|
-
let buildUrl = "";
|
|
131
|
-
child.stdout?.on("data", (data) => {
|
|
132
|
-
const text = data.toString();
|
|
133
|
-
stdoutData += text;
|
|
134
|
-
// Try to extract build URL
|
|
135
|
-
const match = text.match(/https:\/\/percy\.io\/[^\s]+\/builds\/\d+/);
|
|
136
|
-
if (match)
|
|
137
|
-
buildUrl = match[0];
|
|
138
|
-
});
|
|
139
|
-
child.stderr?.on("data", (data) => {
|
|
140
|
-
stderrData += data.toString();
|
|
141
|
-
});
|
|
142
|
-
// Wait a few seconds for initial output (build creation)
|
|
143
|
-
await new Promise((resolve) => {
|
|
144
|
-
const timeout = setTimeout(() => resolve(), 8000);
|
|
145
|
-
child.on("close", () => {
|
|
146
|
-
clearTimeout(timeout);
|
|
147
|
-
resolve();
|
|
148
|
-
});
|
|
149
|
-
// Also resolve if we find the build URL early
|
|
150
|
-
const checkInterval = setInterval(() => {
|
|
151
|
-
if (buildUrl) {
|
|
152
|
-
clearTimeout(timeout);
|
|
153
|
-
clearInterval(checkInterval);
|
|
154
|
-
resolve();
|
|
155
|
-
}
|
|
156
|
-
}, 500);
|
|
157
|
-
});
|
|
158
|
-
// Unref so the process doesn't keep MCP server alive
|
|
159
|
-
child.unref();
|
|
160
|
-
// Clean up temp file after a delay
|
|
161
|
-
setTimeout(async () => {
|
|
162
|
-
try {
|
|
163
|
-
await unlink(configPath);
|
|
164
|
-
}
|
|
165
|
-
catch {
|
|
166
|
-
// ignore
|
|
167
|
-
}
|
|
168
|
-
}, 120000); // 2 minutes
|
|
169
|
-
// Step 5: Report results
|
|
170
|
-
if (buildUrl) {
|
|
171
|
-
output += `**Build started!** Percy is rendering your pages in the background.\n\n`;
|
|
172
|
-
output += `**Build URL:** ${buildUrl}\n\n`;
|
|
173
|
-
output += `Percy is capturing ${urls.length} URL(s) at ${widths.length} width(s) = ${urls.length * widths.length} snapshot(s).\n\n`;
|
|
174
|
-
output += `Check the build URL above for results (usually ready in 1-3 minutes).\n`;
|
|
175
|
-
}
|
|
176
|
-
else if (stdoutData || stderrData) {
|
|
177
|
-
// No build URL found yet — show what we have
|
|
178
|
-
const allOutput = (stdoutData + stderrData).trim();
|
|
179
|
-
// Check for common errors
|
|
180
|
-
if (allOutput.includes("not found") || allOutput.includes("ECONNREFUSED")) {
|
|
181
|
-
output += `**Error:** The URL may not be reachable.\n\n`;
|
|
182
|
-
output += `Make sure your app is running at the specified URL(s):\n`;
|
|
183
|
-
urls.forEach((u) => {
|
|
184
|
-
output += `- ${u}\n`;
|
|
185
|
-
});
|
|
186
|
-
output += `\n`;
|
|
187
|
-
}
|
|
188
|
-
output += `**Percy CLI output:**\n\`\`\`\n${allOutput.slice(0, 500)}\n\`\`\`\n\n`;
|
|
189
|
-
output += `Percy is running in the background. If a build was created, check your Percy dashboard.\n`;
|
|
190
|
-
}
|
|
191
|
-
else {
|
|
192
|
-
output += `**Percy CLI launched in background.** No output yet.\n\n`;
|
|
193
|
-
output += `The build should appear in your Percy dashboard shortly.\n`;
|
|
194
|
-
output += `Check: https://percy.io\n`;
|
|
195
|
-
}
|
|
196
|
-
return { content: [{ type: "text", text: output }] };
|
|
197
|
-
}
|