@browserstack/mcp-server 1.2.15-beta.1 → 1.2.15-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +3 -2
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
import { percyTokenPost, getOrCreateProjectToken, } from "../../../lib/percy-api/percy-auth.js";
|
|
2
|
+
import { setActiveProject, setActiveBuild, } from "../../../lib/percy-api/percy-session.js";
|
|
3
|
+
import { execFile, spawn } from "child_process";
|
|
4
|
+
import { promisify } from "util";
|
|
5
|
+
import { writeFile, readdir, readFile, stat, unlink, mkdtemp, } from "fs/promises";
|
|
6
|
+
import { join, basename, extname } from "path";
|
|
7
|
+
import { tmpdir } from "os";
|
|
8
|
+
import { createHash } from "crypto";
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
async function getGitBranch() {
|
|
11
|
+
try {
|
|
12
|
+
return ((await execFileAsync("git", ["branch", "--show-current"])).stdout.trim() || "main");
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return "main";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function getGitSha() {
|
|
19
|
+
try {
|
|
20
|
+
return (await execFileAsync("git", ["rev-parse", "HEAD"])).stdout.trim();
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return createHash("sha1").update(Date.now().toString()).digest("hex");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function isPercyCliInstalled() {
|
|
27
|
+
try {
|
|
28
|
+
await execFileAsync("npx", ["@percy/cli", "--version"]);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// ── Main handler ────────────────────────────────────────────────────────────
|
|
36
|
+
export async function percyCreateBuildV2(args, config) {
|
|
37
|
+
const branch = args.branch || (await getGitBranch());
|
|
38
|
+
const commitSha = await getGitSha();
|
|
39
|
+
const widths = args.widths
|
|
40
|
+
? args.widths.split(",").map((w) => w.trim())
|
|
41
|
+
: ["375", "1280"];
|
|
42
|
+
// Get project token and activate in session
|
|
43
|
+
let token;
|
|
44
|
+
try {
|
|
45
|
+
token = await getOrCreateProjectToken(args.project_name, config, args.type);
|
|
46
|
+
setActiveProject({ name: args.project_name, token, type: args.type });
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
return {
|
|
50
|
+
content: [
|
|
51
|
+
{
|
|
52
|
+
type: "text",
|
|
53
|
+
text: `Failed to access project "${args.project_name}": ${e.message}`,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
isError: true,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
// Parse custom snapshot names and test cases
|
|
60
|
+
const customNames = args.snapshot_names
|
|
61
|
+
? args.snapshot_names.split(",").map((n) => n.trim())
|
|
62
|
+
: [];
|
|
63
|
+
// test_case can be single (applies to all) or comma-separated (maps 1:1)
|
|
64
|
+
const testCases = args.test_case
|
|
65
|
+
? args.test_case.split(",").map((t) => t.trim())
|
|
66
|
+
: [];
|
|
67
|
+
// Detect mode
|
|
68
|
+
if (args.urls) {
|
|
69
|
+
return handleUrlSnapshot(args.project_name, token, args.urls, widths, branch, customNames, testCases);
|
|
70
|
+
}
|
|
71
|
+
else if (args.test_command) {
|
|
72
|
+
return handleTestCommand(args.project_name, token, args.test_command, branch);
|
|
73
|
+
}
|
|
74
|
+
else if (args.screenshots_dir || args.screenshot_files) {
|
|
75
|
+
return handleScreenshotUpload(token, args, branch, commitSha, customNames, testCases);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
let output = `## Percy Build — ${args.project_name}\n\n`;
|
|
79
|
+
output += `**Token:** ready (${token.slice(0, 8)}...)\n`;
|
|
80
|
+
output += `**Branch:** ${branch}\n\n`;
|
|
81
|
+
output += `Provide one of:\n`;
|
|
82
|
+
output += `- \`urls\` — URLs to snapshot\n`;
|
|
83
|
+
output += `- \`test_command\` — test command to wrap\n`;
|
|
84
|
+
output += `- \`screenshots_dir\` — folder with PNG/JPG files\n`;
|
|
85
|
+
output += `- \`screenshot_files\` — comma-separated file paths\n`;
|
|
86
|
+
return { content: [{ type: "text", text: output }] };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// ── URL Snapshot ────────────────────────────────────────────────────────────
|
|
90
|
+
async function handleUrlSnapshot(projectName, token, urls, widths, branch, customNames, testCases) {
|
|
91
|
+
const urlList = urls
|
|
92
|
+
.split(",")
|
|
93
|
+
.map((u) => u.trim())
|
|
94
|
+
.filter(Boolean);
|
|
95
|
+
const cliInstalled = await isPercyCliInstalled();
|
|
96
|
+
if (!cliInstalled) {
|
|
97
|
+
let output = `## Percy CLI Not Installed\n\n`;
|
|
98
|
+
output += `Install it first:\n\`\`\`bash\nnpm install -g @percy/cli\n\`\`\`\n\n`;
|
|
99
|
+
output += `Then re-run this command.\n`;
|
|
100
|
+
return { content: [{ type: "text", text: output }] };
|
|
101
|
+
}
|
|
102
|
+
// Build snapshots.yml with names, test cases, and widths
|
|
103
|
+
// Percy CLI YAML supports: name, url, testCase, widths, waitForTimeout
|
|
104
|
+
let yamlContent = "";
|
|
105
|
+
urlList.forEach((url, i) => {
|
|
106
|
+
const name = customNames[i] ||
|
|
107
|
+
(urlList.length === 1
|
|
108
|
+
? "Homepage"
|
|
109
|
+
: url
|
|
110
|
+
.replace(/^https?:\/\/[^/]+/, "")
|
|
111
|
+
.replace(/^\//, "")
|
|
112
|
+
.replace(/[/:?&=]/g, "-")
|
|
113
|
+
.replace(/-+/g, "-")
|
|
114
|
+
.replace(/^-|-$/g, "") || `Page ${i + 1}`);
|
|
115
|
+
const tc = testCases.length === 1 ? testCases[0] : testCases[i];
|
|
116
|
+
yamlContent += `- name: "${name}"\n`;
|
|
117
|
+
yamlContent += ` url: ${url}\n`;
|
|
118
|
+
yamlContent += ` waitForTimeout: 3000\n`;
|
|
119
|
+
if (tc) {
|
|
120
|
+
yamlContent += ` testCase: "${tc}"\n`;
|
|
121
|
+
}
|
|
122
|
+
if (widths.length > 0) {
|
|
123
|
+
yamlContent += ` widths:\n`;
|
|
124
|
+
widths.forEach((w) => {
|
|
125
|
+
yamlContent += ` - ${w}\n`;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
// Write config to temp file
|
|
130
|
+
const tmpDir = await mkdtemp(join(tmpdir(), "percy-mcp-"));
|
|
131
|
+
const configPath = join(tmpDir, "snapshots.yml");
|
|
132
|
+
await writeFile(configPath, yamlContent);
|
|
133
|
+
// Launch Percy CLI — EXECUTE AUTOMATICALLY
|
|
134
|
+
const child = spawn("npx", ["@percy/cli", "snapshot", configPath], {
|
|
135
|
+
env: { ...process.env, PERCY_TOKEN: token },
|
|
136
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
137
|
+
detached: true,
|
|
138
|
+
});
|
|
139
|
+
let buildUrl = "";
|
|
140
|
+
let stdoutData = "";
|
|
141
|
+
let stderrData = "";
|
|
142
|
+
child.stdout?.on("data", (d) => {
|
|
143
|
+
const text = d.toString();
|
|
144
|
+
stdoutData += text;
|
|
145
|
+
const match = text.match(/https:\/\/percy\.io\/[^\s]+\/builds\/\d+/);
|
|
146
|
+
if (match)
|
|
147
|
+
buildUrl = match[0];
|
|
148
|
+
});
|
|
149
|
+
child.stderr?.on("data", (d) => {
|
|
150
|
+
stderrData += d.toString();
|
|
151
|
+
});
|
|
152
|
+
// Wait for build URL or timeout
|
|
153
|
+
await new Promise((resolve) => {
|
|
154
|
+
const timeout = setTimeout(resolve, 15000);
|
|
155
|
+
child.on("close", () => {
|
|
156
|
+
clearTimeout(timeout);
|
|
157
|
+
resolve();
|
|
158
|
+
});
|
|
159
|
+
const check = setInterval(() => {
|
|
160
|
+
if (buildUrl) {
|
|
161
|
+
clearTimeout(timeout);
|
|
162
|
+
clearInterval(check);
|
|
163
|
+
resolve();
|
|
164
|
+
}
|
|
165
|
+
}, 500);
|
|
166
|
+
});
|
|
167
|
+
child.unref();
|
|
168
|
+
// Cleanup temp file later
|
|
169
|
+
setTimeout(async () => {
|
|
170
|
+
try {
|
|
171
|
+
await unlink(configPath);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
/* ignore */
|
|
175
|
+
}
|
|
176
|
+
}, 120000);
|
|
177
|
+
// Extract build ID from URL (format: .../builds/12345)
|
|
178
|
+
const buildIdMatch = buildUrl.match(/\/builds\/(\d+)/);
|
|
179
|
+
const buildId = buildIdMatch ? buildIdMatch[1] : "";
|
|
180
|
+
// Store in session
|
|
181
|
+
if (buildId || buildUrl) {
|
|
182
|
+
setActiveBuild({ id: buildId, url: buildUrl, branch });
|
|
183
|
+
}
|
|
184
|
+
// Build response
|
|
185
|
+
let output = `## Percy Build — ${projectName}\n\n`;
|
|
186
|
+
// Always show build info table
|
|
187
|
+
output += `| Field | Value |\n|---|---|\n`;
|
|
188
|
+
output += `| **Project** | ${projectName} |\n`;
|
|
189
|
+
if (buildId)
|
|
190
|
+
output += `| **Build ID** | ${buildId} |\n`;
|
|
191
|
+
output += `| **Branch** | ${branch} |\n`;
|
|
192
|
+
output += `| **URLs** | ${urlList.length} |\n`;
|
|
193
|
+
output += `| **Widths** | ${widths.join(", ")}px |\n`;
|
|
194
|
+
output += `| **Expected Snapshots** | ${urlList.length * widths.length} |\n`;
|
|
195
|
+
if (buildUrl)
|
|
196
|
+
output += `| **Build URL** | ${buildUrl} |\n`;
|
|
197
|
+
output += `| **Token** | \`${token.slice(0, 8)}...${token.slice(-4)}\` |\n`;
|
|
198
|
+
output += "\n";
|
|
199
|
+
if (testCases.length > 0) {
|
|
200
|
+
output += `**Test cases:** ${testCases.join(", ")}\n\n`;
|
|
201
|
+
}
|
|
202
|
+
// Show snapshot details
|
|
203
|
+
output += `**Snapshots:**\n`;
|
|
204
|
+
urlList.forEach((url, i) => {
|
|
205
|
+
const name = customNames[i] || (urlList.length === 1 ? "Homepage" : `Page ${i + 1}`);
|
|
206
|
+
const tc = testCases.length === 1 ? testCases[0] : testCases[i];
|
|
207
|
+
output += `- **${name}**`;
|
|
208
|
+
if (tc)
|
|
209
|
+
output += ` (test: ${tc})`;
|
|
210
|
+
output += ` → ${url}\n`;
|
|
211
|
+
});
|
|
212
|
+
output += "\n";
|
|
213
|
+
if (buildUrl) {
|
|
214
|
+
output += `**Build started!** Percy is rendering in the background.\n`;
|
|
215
|
+
output += `Results ready in 1-3 minutes.\n`;
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
const allOutput = (stdoutData + stderrData).trim();
|
|
219
|
+
if (allOutput.includes("ECONNREFUSED") || allOutput.includes("not found")) {
|
|
220
|
+
output += `**Error:** URL not reachable. Make sure your app is running.\n\n`;
|
|
221
|
+
urlList.forEach((u) => {
|
|
222
|
+
output += `- ${u}\n`;
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
else if (allOutput) {
|
|
226
|
+
output += `**Percy output:**\n\`\`\`\n${allOutput.slice(0, 500)}\n\`\`\`\n`;
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
output += `Percy launched in background. Check your Percy dashboard for results.\n`;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// Next steps
|
|
233
|
+
output += `\n### Next Steps\n\n`;
|
|
234
|
+
if (buildId) {
|
|
235
|
+
output += `- \`percy_get_build\` with build_id "${buildId}" — View build details\n`;
|
|
236
|
+
output += `- \`percy_get_build\` with build_id "${buildId}" and detail "snapshots" — List snapshots\n`;
|
|
237
|
+
output += `- \`percy_get_build\` with build_id "${buildId}" and detail "ai_summary" — AI analysis\n`;
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
output += `- \`percy_get_builds\` — Find the build ID once processing completes\n`;
|
|
241
|
+
}
|
|
242
|
+
return { content: [{ type: "text", text: output }] };
|
|
243
|
+
}
|
|
244
|
+
// ── REMOVED: handleUrlWithTestCases — test cases now handled in YAML directly
|
|
245
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
246
|
+
async function _handleUrlWithTestCases_UNUSED(projectName, token, urlList, widths, branch, customNames, testCases) {
|
|
247
|
+
// Use @percy/core directly via a generated Node.js script
|
|
248
|
+
// This is the only way to set testCase on URL-based snapshots
|
|
249
|
+
const snapshots = urlList.map((url, i) => {
|
|
250
|
+
const name = customNames[i] ||
|
|
251
|
+
url
|
|
252
|
+
.replace(/^https?:\/\/[^/]+/, "")
|
|
253
|
+
.replace(/^\//, "")
|
|
254
|
+
.replace(/[/:?&=]/g, "-")
|
|
255
|
+
.replace(/-+/g, "-")
|
|
256
|
+
.replace(/^-|-$/g, "") ||
|
|
257
|
+
`Page ${i + 1}`;
|
|
258
|
+
const tc = testCases.length === 1 ? testCases[0] : testCases[i];
|
|
259
|
+
return { url, name, testCase: tc || undefined };
|
|
260
|
+
});
|
|
261
|
+
const scriptContent = `
|
|
262
|
+
import Percy from '@percy/core';
|
|
263
|
+
|
|
264
|
+
const percy = new Percy({
|
|
265
|
+
token: process.env.PERCY_TOKEN,
|
|
266
|
+
snapshot: { widths: [${widths.join(",")}] }
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
await percy.start();
|
|
270
|
+
console.log('[percy-mcp] Percy started');
|
|
271
|
+
|
|
272
|
+
const snapshots = ${JSON.stringify(snapshots)};
|
|
273
|
+
|
|
274
|
+
for (const snap of snapshots) {
|
|
275
|
+
try {
|
|
276
|
+
await percy.snapshot({
|
|
277
|
+
url: snap.url,
|
|
278
|
+
name: snap.name,
|
|
279
|
+
testCase: snap.testCase,
|
|
280
|
+
widths: [${widths.join(",")}],
|
|
281
|
+
waitForTimeout: 3000,
|
|
282
|
+
});
|
|
283
|
+
console.log('[percy-mcp] ok ' + snap.name + (snap.testCase ? ' (test: ' + snap.testCase + ')' : ''));
|
|
284
|
+
} catch (e) {
|
|
285
|
+
console.error('[percy-mcp] fail ' + snap.name + ': ' + e.message);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
await percy.stop();
|
|
290
|
+
console.log('[percy-mcp] Done');
|
|
291
|
+
`;
|
|
292
|
+
const tmpDir = await mkdtemp(join(tmpdir(), "percy-mcp-"));
|
|
293
|
+
const scriptPath = join(tmpDir, "snapshot.mjs");
|
|
294
|
+
await writeFile(scriptPath, scriptContent);
|
|
295
|
+
// Run the script in background
|
|
296
|
+
const child = spawn("node", [scriptPath], {
|
|
297
|
+
env: { ...process.env, PERCY_TOKEN: token },
|
|
298
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
299
|
+
detached: true,
|
|
300
|
+
});
|
|
301
|
+
let buildUrl = "";
|
|
302
|
+
const stdoutLines = [];
|
|
303
|
+
child.stdout?.on("data", (d) => {
|
|
304
|
+
const text = d.toString();
|
|
305
|
+
stdoutLines.push(text.trim());
|
|
306
|
+
const match = text.match(/https:\/\/percy\.io\/[^\s]+\/builds\/\d+/);
|
|
307
|
+
if (match)
|
|
308
|
+
buildUrl = match[0];
|
|
309
|
+
});
|
|
310
|
+
child.stderr?.on("data", (d) => {
|
|
311
|
+
stdoutLines.push(d.toString().trim());
|
|
312
|
+
});
|
|
313
|
+
// Wait for completion (up to 60s — Percy needs to start browser, render, upload)
|
|
314
|
+
await new Promise((resolve) => {
|
|
315
|
+
const timeout = setTimeout(resolve, 60000);
|
|
316
|
+
child.on("close", () => {
|
|
317
|
+
clearTimeout(timeout);
|
|
318
|
+
resolve();
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
child.unref();
|
|
322
|
+
setTimeout(async () => {
|
|
323
|
+
try {
|
|
324
|
+
await unlink(scriptPath);
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
/* ignore */
|
|
328
|
+
}
|
|
329
|
+
}, 120000);
|
|
330
|
+
// Build output
|
|
331
|
+
let output = `## Percy Build — ${projectName}\n\n`;
|
|
332
|
+
output += `**Branch:** ${branch}\n`;
|
|
333
|
+
output += `**URLs:** ${urlList.length}\n`;
|
|
334
|
+
output += `**Widths:** ${widths.join(", ")}px\n\n`;
|
|
335
|
+
output += `**Snapshots:**\n`;
|
|
336
|
+
for (const snap of snapshots) {
|
|
337
|
+
const logLine = stdoutLines.find((l) => l.includes(snap.name));
|
|
338
|
+
const ok = logLine?.includes("[percy-mcp] ok");
|
|
339
|
+
output += `- ${ok ? "✓" : "?"} **${snap.name}**`;
|
|
340
|
+
if (snap.testCase)
|
|
341
|
+
output += ` (test: ${snap.testCase})`;
|
|
342
|
+
output += ` → ${snap.url}\n`;
|
|
343
|
+
}
|
|
344
|
+
output += "\n";
|
|
345
|
+
if (buildUrl) {
|
|
346
|
+
output += `**Build URL:** ${buildUrl}\n\n`;
|
|
347
|
+
output += `${snapshots.length} snapshot(s) with test cases. Results ready in 1-3 minutes.\n`;
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
const percyOutput = stdoutLines
|
|
351
|
+
.filter((l) => l.includes("[percy"))
|
|
352
|
+
.join("\n");
|
|
353
|
+
if (percyOutput) {
|
|
354
|
+
output += `**Percy output:**\n\`\`\`\n${percyOutput.slice(0, 500)}\n\`\`\`\n`;
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
output += `Percy is processing. Check dashboard for results.\n`;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return { content: [{ type: "text", text: output }] };
|
|
361
|
+
}
|
|
362
|
+
// ── Test Command ────────────────────────────────────────────────────────────
|
|
363
|
+
async function handleTestCommand(projectName, token, testCommand, branch) {
|
|
364
|
+
const cliInstalled = await isPercyCliInstalled();
|
|
365
|
+
if (!cliInstalled) {
|
|
366
|
+
let output = `## Percy CLI Not Installed\n\n`;
|
|
367
|
+
output += `Install it first:\n\`\`\`bash\nnpm install -g @percy/cli\n\`\`\`\n\n`;
|
|
368
|
+
output += `Then re-run this command.\n`;
|
|
369
|
+
return { content: [{ type: "text", text: output }] };
|
|
370
|
+
}
|
|
371
|
+
const cmdParts = testCommand.split(" ").filter(Boolean);
|
|
372
|
+
// EXECUTE AUTOMATICALLY
|
|
373
|
+
const child = spawn("npx", ["@percy/cli", "exec", "--", ...cmdParts], {
|
|
374
|
+
env: { ...process.env, PERCY_TOKEN: token },
|
|
375
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
376
|
+
detached: true,
|
|
377
|
+
});
|
|
378
|
+
let buildUrl = "";
|
|
379
|
+
let stdoutData = "";
|
|
380
|
+
child.stdout?.on("data", (d) => {
|
|
381
|
+
const text = d.toString();
|
|
382
|
+
stdoutData += text;
|
|
383
|
+
const match = text.match(/https:\/\/percy\.io\/[^\s]+\/builds\/\d+/);
|
|
384
|
+
if (match)
|
|
385
|
+
buildUrl = match[0];
|
|
386
|
+
});
|
|
387
|
+
child.stderr?.on("data", (d) => {
|
|
388
|
+
stdoutData += d.toString();
|
|
389
|
+
});
|
|
390
|
+
await new Promise((resolve) => {
|
|
391
|
+
const timeout = setTimeout(resolve, 15000);
|
|
392
|
+
child.on("close", () => {
|
|
393
|
+
clearTimeout(timeout);
|
|
394
|
+
resolve();
|
|
395
|
+
});
|
|
396
|
+
const check = setInterval(() => {
|
|
397
|
+
if (buildUrl) {
|
|
398
|
+
clearTimeout(timeout);
|
|
399
|
+
clearInterval(check);
|
|
400
|
+
resolve();
|
|
401
|
+
}
|
|
402
|
+
}, 500);
|
|
403
|
+
});
|
|
404
|
+
child.unref();
|
|
405
|
+
// Extract build ID from URL
|
|
406
|
+
const buildIdMatch = buildUrl.match(/\/builds\/(\d+)/);
|
|
407
|
+
const buildId = buildIdMatch ? buildIdMatch[1] : "";
|
|
408
|
+
if (buildId || buildUrl) {
|
|
409
|
+
setActiveBuild({ id: buildId, url: buildUrl, branch });
|
|
410
|
+
}
|
|
411
|
+
let output = `## Percy Build — Tests\n\n`;
|
|
412
|
+
output += `| Field | Value |\n|---|---|\n`;
|
|
413
|
+
output += `| **Project** | ${projectName} |\n`;
|
|
414
|
+
if (buildId)
|
|
415
|
+
output += `| **Build ID** | ${buildId} |\n`;
|
|
416
|
+
output += `| **Command** | \`${testCommand}\` |\n`;
|
|
417
|
+
output += `| **Branch** | ${branch} |\n`;
|
|
418
|
+
output += `| **Token** | \`${token.slice(0, 8)}...${token.slice(-4)}\` |\n`;
|
|
419
|
+
if (buildUrl)
|
|
420
|
+
output += `| **Build URL** | ${buildUrl} |\n`;
|
|
421
|
+
output += "\n";
|
|
422
|
+
if (buildUrl) {
|
|
423
|
+
output += `Tests running in background.\n`;
|
|
424
|
+
}
|
|
425
|
+
else if (stdoutData.trim()) {
|
|
426
|
+
output += `**Output:**\n\`\`\`\n${stdoutData.trim().slice(0, 500)}\n\`\`\`\n`;
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
output += `Tests launched in background. Check Percy dashboard.\n`;
|
|
430
|
+
}
|
|
431
|
+
// Next steps
|
|
432
|
+
output += `\n### Next Steps\n\n`;
|
|
433
|
+
if (buildId) {
|
|
434
|
+
output += `- \`percy_get_build\` with build_id "${buildId}" — View build details\n`;
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
output += `- \`percy_get_builds\` — Find build once processing completes\n`;
|
|
438
|
+
}
|
|
439
|
+
return { content: [{ type: "text", text: output }] };
|
|
440
|
+
}
|
|
441
|
+
// ── Screenshot Upload ───────────────────────────────────────────────────────
|
|
442
|
+
async function handleScreenshotUpload(token, args, branch, commitSha, customNames, testCases) {
|
|
443
|
+
let files = [];
|
|
444
|
+
if (args.screenshot_files) {
|
|
445
|
+
files = args.screenshot_files
|
|
446
|
+
.split(",")
|
|
447
|
+
.map((f) => f.trim())
|
|
448
|
+
.filter(Boolean);
|
|
449
|
+
}
|
|
450
|
+
if (args.screenshots_dir) {
|
|
451
|
+
try {
|
|
452
|
+
const dirStat = await stat(args.screenshots_dir);
|
|
453
|
+
if (dirStat.isDirectory()) {
|
|
454
|
+
const entries = await readdir(args.screenshots_dir);
|
|
455
|
+
files.push(...entries
|
|
456
|
+
.filter((f) => /\.(png|jpg|jpeg|webp)$/i.test(f))
|
|
457
|
+
.map((f) => join(args.screenshots_dir, f)));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
catch (e) {
|
|
461
|
+
return {
|
|
462
|
+
content: [
|
|
463
|
+
{
|
|
464
|
+
type: "text",
|
|
465
|
+
text: `Directory not accessible: ${e.message}`,
|
|
466
|
+
},
|
|
467
|
+
],
|
|
468
|
+
isError: true,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (files.length === 0) {
|
|
473
|
+
return {
|
|
474
|
+
content: [{ type: "text", text: "No image files found." }],
|
|
475
|
+
isError: true,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
// Create build
|
|
479
|
+
const buildResponse = await percyTokenPost("/builds", token, {
|
|
480
|
+
data: {
|
|
481
|
+
type: "builds",
|
|
482
|
+
attributes: { branch, "commit-sha": commitSha },
|
|
483
|
+
relationships: { resources: { data: [] } },
|
|
484
|
+
},
|
|
485
|
+
});
|
|
486
|
+
const buildId = buildResponse?.data?.id;
|
|
487
|
+
const buildUrl = buildResponse?.data?.attributes?.["web-url"] || "";
|
|
488
|
+
if (!buildId) {
|
|
489
|
+
return {
|
|
490
|
+
content: [{ type: "text", text: "Failed to create build." }],
|
|
491
|
+
isError: true,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
// Store in session
|
|
495
|
+
setActiveBuild({ id: buildId, url: buildUrl, branch });
|
|
496
|
+
let output = `## Percy Build — Screenshot Upload\n\n`;
|
|
497
|
+
output += `| Field | Value |\n|---|---|\n`;
|
|
498
|
+
output += `| **Build ID** | ${buildId} |\n`;
|
|
499
|
+
output += `| **Project** | ${args.project_name} |\n`;
|
|
500
|
+
output += `| **Branch** | ${branch} |\n`;
|
|
501
|
+
output += `| **Files** | ${files.length} |\n`;
|
|
502
|
+
output += `| **Token** | \`${token.slice(0, 8)}...${token.slice(-4)}\` |\n`;
|
|
503
|
+
if (buildUrl)
|
|
504
|
+
output += `| **Build URL** | ${buildUrl} |\n`;
|
|
505
|
+
output += "\n";
|
|
506
|
+
let uploaded = 0;
|
|
507
|
+
for (let i = 0; i < files.length; i++) {
|
|
508
|
+
const filePath = files[i];
|
|
509
|
+
// Use custom name, or clean filename
|
|
510
|
+
const name = customNames[i] ||
|
|
511
|
+
basename(filePath, extname(filePath)).replace(/[-_]/g, " ");
|
|
512
|
+
try {
|
|
513
|
+
const content = await readFile(filePath);
|
|
514
|
+
const sha = createHash("sha256").update(content).digest("hex");
|
|
515
|
+
const base64 = content.toString("base64");
|
|
516
|
+
let width = 1280;
|
|
517
|
+
let height = 800;
|
|
518
|
+
if (content[0] === 0x89 && content[1] === 0x50) {
|
|
519
|
+
width = content.readUInt32BE(16);
|
|
520
|
+
height = content.readUInt32BE(20);
|
|
521
|
+
}
|
|
522
|
+
// Create snapshot with optional test case
|
|
523
|
+
// If 1 test case provided → applies to all snapshots
|
|
524
|
+
// If multiple → maps 1:1 with files
|
|
525
|
+
const snapAttrs = { name };
|
|
526
|
+
const tc = testCases.length === 1 ? testCases[0] : testCases[i];
|
|
527
|
+
if (tc)
|
|
528
|
+
snapAttrs["test-case"] = tc;
|
|
529
|
+
const snapRes = await percyTokenPost(`/builds/${buildId}/snapshots`, token, { data: { type: "snapshots", attributes: snapAttrs } });
|
|
530
|
+
const snapId = snapRes?.data?.id;
|
|
531
|
+
if (!snapId) {
|
|
532
|
+
output += `- ✗ ${name}: snapshot failed\n`;
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
// Create comparison
|
|
536
|
+
const compRes = await percyTokenPost(`/snapshots/${snapId}/comparisons`, token, {
|
|
537
|
+
data: {
|
|
538
|
+
attributes: {
|
|
539
|
+
"external-debug-url": null,
|
|
540
|
+
"dom-info-sha": null,
|
|
541
|
+
},
|
|
542
|
+
relationships: {
|
|
543
|
+
tag: {
|
|
544
|
+
data: {
|
|
545
|
+
attributes: {
|
|
546
|
+
name: "Screenshot",
|
|
547
|
+
width,
|
|
548
|
+
height,
|
|
549
|
+
"os-name": "Upload",
|
|
550
|
+
"browser-name": "Screenshot",
|
|
551
|
+
},
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
tiles: {
|
|
555
|
+
data: [
|
|
556
|
+
{
|
|
557
|
+
attributes: {
|
|
558
|
+
sha,
|
|
559
|
+
"status-bar-height": 0,
|
|
560
|
+
"nav-bar-height": 0,
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
],
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
});
|
|
568
|
+
const compId = compRes?.data?.id;
|
|
569
|
+
if (!compId) {
|
|
570
|
+
output += `- ✗ ${name}: comparison failed\n`;
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
// Upload tile
|
|
574
|
+
await percyTokenPost(`/comparisons/${compId}/tiles`, token, {
|
|
575
|
+
data: { attributes: { "base64-content": base64 } },
|
|
576
|
+
});
|
|
577
|
+
// Finalize comparison
|
|
578
|
+
await percyTokenPost(`/comparisons/${compId}/finalize`, token, {});
|
|
579
|
+
uploaded++;
|
|
580
|
+
output += `- ✓ **${name}** (${width}×${height})\n`;
|
|
581
|
+
}
|
|
582
|
+
catch (e) {
|
|
583
|
+
output += `- ✗ ${name}: ${e.message}\n`;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
// Finalize build
|
|
587
|
+
try {
|
|
588
|
+
await percyTokenPost(`/builds/${buildId}/finalize`, token, {});
|
|
589
|
+
output += `\n**Build finalized.** ${uploaded}/${files.length} uploaded.\n`;
|
|
590
|
+
}
|
|
591
|
+
catch (e) {
|
|
592
|
+
output += `\n**Finalize failed:** ${e.message}\n`;
|
|
593
|
+
}
|
|
594
|
+
if (buildUrl)
|
|
595
|
+
output += `\n**View:** ${buildUrl}\n`;
|
|
596
|
+
output += `\n### Next Steps\n\n`;
|
|
597
|
+
output += `- \`percy_get_build\` with build_id "${buildId}" — View build details\n`;
|
|
598
|
+
output += `- \`percy_get_build\` with build_id "${buildId}" and detail "snapshots" — List snapshots\n`;
|
|
599
|
+
output += `- \`percy_get_build\` with build_id "${buildId}" and detail "ai_summary" — AI analysis\n`;
|
|
600
|
+
return { content: [{ type: "text", text: output }] };
|
|
601
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { BrowserStackConfig } from "../../../lib/types.js";
|
|
2
|
+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
export declare function percyCreateProjectV2(args: {
|
|
4
|
+
name: string;
|
|
5
|
+
type?: string;
|
|
6
|
+
default_branch?: string;
|
|
7
|
+
workflow?: string;
|
|
8
|
+
}, config: BrowserStackConfig): Promise<CallToolResult>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getOrCreateProjectToken } from "../../../lib/percy-api/percy-auth.js";
|
|
2
|
+
import { setActiveProject } from "../../../lib/percy-api/percy-session.js";
|
|
3
|
+
export async function percyCreateProjectV2(args, config) {
|
|
4
|
+
const token = await getOrCreateProjectToken(args.name, config, args.type);
|
|
5
|
+
const tokenPrefix = token.includes("_") ? token.split("_")[0] : "ci";
|
|
6
|
+
const masked = token.length > 8 ? `${token.slice(0, 8)}...${token.slice(-4)}` : "****";
|
|
7
|
+
const projectType = args.type || (tokenPrefix === "app" ? "app" : "web");
|
|
8
|
+
// Store in session — all subsequent calls will use this token
|
|
9
|
+
setActiveProject({
|
|
10
|
+
name: args.name,
|
|
11
|
+
token,
|
|
12
|
+
type: projectType,
|
|
13
|
+
});
|
|
14
|
+
let output = `## Percy Project — ${args.name}\n\n`;
|
|
15
|
+
output += `| Field | Value |\n|---|---|\n`;
|
|
16
|
+
output += `| **Name** | ${args.name} |\n`;
|
|
17
|
+
output += `| **Type** | ${projectType} |\n`;
|
|
18
|
+
output += `| **Token** | \`${masked}\` (${tokenPrefix}) |\n`;
|
|
19
|
+
output += `| **Status** | Active — token set for this session |\n`;
|
|
20
|
+
output += `\n**Full token:**\n\`\`\`\n${token}\n\`\`\`\n`;
|
|
21
|
+
output += `\n> Token is now **active** for all subsequent Percy commands in this session. No need to set PERCY_TOKEN manually.\n`;
|
|
22
|
+
output += `\n### Next Steps\n\n`;
|
|
23
|
+
if (projectType === "app") {
|
|
24
|
+
output += `- \`percy_create_app_build\` with project_name "${args.name}" — Create app BYOS build\n`;
|
|
25
|
+
output += `- \`percy_create_app_build\` with project_name "${args.name}" (no resources_dir) — Quick test with sample data\n`;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
output += `- \`percy_create_build\` with project_name "${args.name}" and urls "http://localhost:3000" — Snapshot URLs\n`;
|
|
29
|
+
output += `- \`percy_create_build\` with project_name "${args.name}" and screenshots_dir "./screenshots" — Upload screenshots\n`;
|
|
30
|
+
}
|
|
31
|
+
output += `- \`percy_get_builds\` — List builds for this project\n`;
|
|
32
|
+
return { content: [{ type: "text", text: output }] };
|
|
33
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { BrowserStackConfig } from "../../../lib/types.js";
|
|
2
|
+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
export declare function percyDiscoverUrls(args: {
|
|
4
|
+
project_id: string;
|
|
5
|
+
sitemap_url?: string;
|
|
6
|
+
action?: string;
|
|
7
|
+
}, config: BrowserStackConfig): Promise<CallToolResult>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { percyPost, percyGet } from "../../../lib/percy-api/percy-auth.js";
|
|
2
|
+
export async function percyDiscoverUrls(args, config) {
|
|
3
|
+
const action = args.action || (args.sitemap_url ? "create" : "list");
|
|
4
|
+
if (action === "create" && args.sitemap_url) {
|
|
5
|
+
const result = await percyPost("/sitemaps", config, {
|
|
6
|
+
data: {
|
|
7
|
+
type: "sitemaps",
|
|
8
|
+
attributes: { url: args.sitemap_url, "project-id": args.project_id },
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
const urls = result?.included?.filter((i) => i.type === "sitemap-urls") || [];
|
|
12
|
+
let output = `## URLs Discovered from Sitemap\n\n`;
|
|
13
|
+
output += `**Sitemap:** ${args.sitemap_url}\n`;
|
|
14
|
+
output += `**URLs found:** ${urls.length}\n\n`;
|
|
15
|
+
urls.forEach((u, i) => {
|
|
16
|
+
output += `${i + 1}. ${u.attributes?.url || u.url || "?"}\n`;
|
|
17
|
+
});
|
|
18
|
+
if (urls.length === 0)
|
|
19
|
+
output += `No URLs found in sitemap. Check the URL.\n`;
|
|
20
|
+
output += `\nUse these URLs with \`percy_create_build\` to snapshot them.\n`;
|
|
21
|
+
return { content: [{ type: "text", text: output }] };
|
|
22
|
+
}
|
|
23
|
+
// List existing sitemaps
|
|
24
|
+
const response = await percyGet("/sitemaps", config, {
|
|
25
|
+
project_id: args.project_id,
|
|
26
|
+
});
|
|
27
|
+
const sitemaps = response?.data || [];
|
|
28
|
+
let output = `## Sitemaps for Project\n\n`;
|
|
29
|
+
if (!sitemaps.length) {
|
|
30
|
+
output += `No sitemaps found. Create one with \`sitemap_url\` parameter.\n`;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
sitemaps.forEach((s, i) => {
|
|
34
|
+
output += `${i + 1}. ${s.attributes?.url || "?"} (${s.attributes?.state || "?"})\n`;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return { content: [{ type: "text", text: output }] };
|
|
38
|
+
}
|