@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.
Files changed (182) hide show
  1. package/dist/server-factory.js +0 -4
  2. package/dist/tools/percy-sdk.js +20 -11
  3. package/dist/tools/testmanagement-utils/get-testplan.d.ts +16 -0
  4. package/dist/tools/testmanagement-utils/get-testplan.js +99 -0
  5. package/dist/tools/testmanagement-utils/list-folders.d.ts +16 -0
  6. package/dist/tools/testmanagement-utils/list-folders.js +77 -0
  7. package/dist/tools/testmanagement-utils/list-testcases.js +1 -1
  8. package/dist/tools/testmanagement-utils/list-testplans.d.ts +15 -0
  9. package/dist/tools/testmanagement-utils/list-testplans.js +75 -0
  10. package/dist/tools/testmanagement-utils/update-testcase.d.ts +16 -0
  11. package/dist/tools/testmanagement-utils/update-testcase.js +133 -10
  12. package/dist/tools/testmanagement.d.ts +15 -0
  13. package/dist/tools/testmanagement.js +73 -2
  14. package/package.json +2 -3
  15. package/dist/lib/percy-api/auth.d.ts +0 -41
  16. package/dist/lib/percy-api/auth.js +0 -96
  17. package/dist/lib/percy-api/cache.d.ts +0 -28
  18. package/dist/lib/percy-api/cache.js +0 -48
  19. package/dist/lib/percy-api/client.d.ts +0 -69
  20. package/dist/lib/percy-api/client.js +0 -275
  21. package/dist/lib/percy-api/errors.d.ts +0 -15
  22. package/dist/lib/percy-api/errors.js +0 -52
  23. package/dist/lib/percy-api/formatter.d.ts +0 -16
  24. package/dist/lib/percy-api/formatter.js +0 -344
  25. package/dist/lib/percy-api/percy-auth.d.ts +0 -43
  26. package/dist/lib/percy-api/percy-auth.js +0 -137
  27. package/dist/lib/percy-api/percy-error-handler.d.ts +0 -24
  28. package/dist/lib/percy-api/percy-error-handler.js +0 -302
  29. package/dist/lib/percy-api/percy-session.d.ts +0 -42
  30. package/dist/lib/percy-api/percy-session.js +0 -87
  31. package/dist/lib/percy-api/polling.d.ts +0 -26
  32. package/dist/lib/percy-api/polling.js +0 -42
  33. package/dist/lib/percy-api/types.d.ts +0 -56
  34. package/dist/lib/percy-api/types.js +0 -76
  35. package/dist/tools/percy-mcp/advanced/branchline-operations.d.ts +0 -16
  36. package/dist/tools/percy-mcp/advanced/branchline-operations.js +0 -81
  37. package/dist/tools/percy-mcp/advanced/manage-variants.d.ts +0 -16
  38. package/dist/tools/percy-mcp/advanced/manage-variants.js +0 -155
  39. package/dist/tools/percy-mcp/advanced/manage-visual-monitoring.d.ts +0 -16
  40. package/dist/tools/percy-mcp/advanced/manage-visual-monitoring.js +0 -171
  41. package/dist/tools/percy-mcp/auth/auth-status.d.ts +0 -3
  42. package/dist/tools/percy-mcp/auth/auth-status.js +0 -131
  43. package/dist/tools/percy-mcp/core/approve-build.d.ts +0 -14
  44. package/dist/tools/percy-mcp/core/approve-build.js +0 -97
  45. package/dist/tools/percy-mcp/core/get-build-items.d.ts +0 -13
  46. package/dist/tools/percy-mcp/core/get-build-items.js +0 -65
  47. package/dist/tools/percy-mcp/core/get-build.d.ts +0 -10
  48. package/dist/tools/percy-mcp/core/get-build.js +0 -16
  49. package/dist/tools/percy-mcp/core/get-comparison.d.ts +0 -11
  50. package/dist/tools/percy-mcp/core/get-comparison.js +0 -59
  51. package/dist/tools/percy-mcp/core/get-snapshot.d.ts +0 -10
  52. package/dist/tools/percy-mcp/core/get-snapshot.js +0 -40
  53. package/dist/tools/percy-mcp/core/list-builds.d.ts +0 -14
  54. package/dist/tools/percy-mcp/core/list-builds.js +0 -45
  55. package/dist/tools/percy-mcp/core/list-projects.d.ts +0 -12
  56. package/dist/tools/percy-mcp/core/list-projects.js +0 -51
  57. package/dist/tools/percy-mcp/creation/create-app-snapshot.d.ts +0 -12
  58. package/dist/tools/percy-mcp/creation/create-app-snapshot.js +0 -29
  59. package/dist/tools/percy-mcp/creation/create-build.d.ts +0 -19
  60. package/dist/tools/percy-mcp/creation/create-build.js +0 -68
  61. package/dist/tools/percy-mcp/creation/create-comparison.d.ts +0 -18
  62. package/dist/tools/percy-mcp/creation/create-comparison.js +0 -90
  63. package/dist/tools/percy-mcp/creation/create-snapshot.d.ts +0 -17
  64. package/dist/tools/percy-mcp/creation/create-snapshot.js +0 -99
  65. package/dist/tools/percy-mcp/creation/finalize-build.d.ts +0 -12
  66. package/dist/tools/percy-mcp/creation/finalize-build.js +0 -33
  67. package/dist/tools/percy-mcp/creation/finalize-comparison.d.ts +0 -10
  68. package/dist/tools/percy-mcp/creation/finalize-comparison.js +0 -16
  69. package/dist/tools/percy-mcp/creation/finalize-snapshot.d.ts +0 -12
  70. package/dist/tools/percy-mcp/creation/finalize-snapshot.js +0 -33
  71. package/dist/tools/percy-mcp/creation/upload-resource.d.ts +0 -15
  72. package/dist/tools/percy-mcp/creation/upload-resource.js +0 -43
  73. package/dist/tools/percy-mcp/creation/upload-tile.d.ts +0 -11
  74. package/dist/tools/percy-mcp/creation/upload-tile.js +0 -53
  75. package/dist/tools/percy-mcp/diagnostics/analyze-logs-realtime.d.ts +0 -13
  76. package/dist/tools/percy-mcp/diagnostics/analyze-logs-realtime.js +0 -65
  77. package/dist/tools/percy-mcp/diagnostics/get-build-logs.d.ts +0 -17
  78. package/dist/tools/percy-mcp/diagnostics/get-build-logs.js +0 -74
  79. package/dist/tools/percy-mcp/diagnostics/get-network-logs.d.ts +0 -5
  80. package/dist/tools/percy-mcp/diagnostics/get-network-logs.js +0 -21
  81. package/dist/tools/percy-mcp/diagnostics/get-suggestions.d.ts +0 -7
  82. package/dist/tools/percy-mcp/diagnostics/get-suggestions.js +0 -24
  83. package/dist/tools/percy-mcp/index.d.ts +0 -36
  84. package/dist/tools/percy-mcp/index.js +0 -1137
  85. package/dist/tools/percy-mcp/intelligence/get-ai-analysis.d.ts +0 -15
  86. package/dist/tools/percy-mcp/intelligence/get-ai-analysis.js +0 -166
  87. package/dist/tools/percy-mcp/intelligence/get-ai-quota.d.ts +0 -9
  88. package/dist/tools/percy-mcp/intelligence/get-ai-quota.js +0 -73
  89. package/dist/tools/percy-mcp/intelligence/get-build-summary.d.ts +0 -11
  90. package/dist/tools/percy-mcp/intelligence/get-build-summary.js +0 -78
  91. package/dist/tools/percy-mcp/intelligence/get-rca.d.ts +0 -6
  92. package/dist/tools/percy-mcp/intelligence/get-rca.js +0 -153
  93. package/dist/tools/percy-mcp/intelligence/suggest-prompt.d.ts +0 -15
  94. package/dist/tools/percy-mcp/intelligence/suggest-prompt.js +0 -86
  95. package/dist/tools/percy-mcp/intelligence/trigger-ai-recompute.d.ts +0 -16
  96. package/dist/tools/percy-mcp/intelligence/trigger-ai-recompute.js +0 -64
  97. package/dist/tools/percy-mcp/management/create-project.d.ts +0 -14
  98. package/dist/tools/percy-mcp/management/create-project.js +0 -52
  99. package/dist/tools/percy-mcp/management/get-usage-stats.d.ts +0 -12
  100. package/dist/tools/percy-mcp/management/get-usage-stats.js +0 -61
  101. package/dist/tools/percy-mcp/management/manage-browser-targets.d.ts +0 -12
  102. package/dist/tools/percy-mcp/management/manage-browser-targets.js +0 -136
  103. package/dist/tools/percy-mcp/management/manage-comments.d.ts +0 -14
  104. package/dist/tools/percy-mcp/management/manage-comments.js +0 -147
  105. package/dist/tools/percy-mcp/management/manage-ignored-regions.d.ts +0 -18
  106. package/dist/tools/percy-mcp/management/manage-ignored-regions.js +0 -182
  107. package/dist/tools/percy-mcp/management/manage-project-settings.d.ts +0 -16
  108. package/dist/tools/percy-mcp/management/manage-project-settings.js +0 -97
  109. package/dist/tools/percy-mcp/management/manage-tokens.d.ts +0 -14
  110. package/dist/tools/percy-mcp/management/manage-tokens.js +0 -90
  111. package/dist/tools/percy-mcp/management/manage-webhooks.d.ts +0 -15
  112. package/dist/tools/percy-mcp/management/manage-webhooks.js +0 -180
  113. package/dist/tools/percy-mcp/v2/auth-status.d.ts +0 -3
  114. package/dist/tools/percy-mcp/v2/auth-status.js +0 -80
  115. package/dist/tools/percy-mcp/v2/clone-build.d.ts +0 -24
  116. package/dist/tools/percy-mcp/v2/clone-build.js +0 -539
  117. package/dist/tools/percy-mcp/v2/create-app-build.d.ts +0 -28
  118. package/dist/tools/percy-mcp/v2/create-app-build.js +0 -442
  119. package/dist/tools/percy-mcp/v2/create-build.d.ts +0 -16
  120. package/dist/tools/percy-mcp/v2/create-build.js +0 -601
  121. package/dist/tools/percy-mcp/v2/create-project.d.ts +0 -8
  122. package/dist/tools/percy-mcp/v2/create-project.js +0 -33
  123. package/dist/tools/percy-mcp/v2/discover-urls.d.ts +0 -7
  124. package/dist/tools/percy-mcp/v2/discover-urls.js +0 -38
  125. package/dist/tools/percy-mcp/v2/figma-baseline.d.ts +0 -7
  126. package/dist/tools/percy-mcp/v2/figma-baseline.js +0 -18
  127. package/dist/tools/percy-mcp/v2/figma-build.d.ts +0 -7
  128. package/dist/tools/percy-mcp/v2/figma-build.js +0 -39
  129. package/dist/tools/percy-mcp/v2/figma-link.d.ts +0 -6
  130. package/dist/tools/percy-mcp/v2/figma-link.js +0 -27
  131. package/dist/tools/percy-mcp/v2/get-ai-summary.d.ts +0 -5
  132. package/dist/tools/percy-mcp/v2/get-ai-summary.js +0 -109
  133. package/dist/tools/percy-mcp/v2/get-build-detail.d.ts +0 -22
  134. package/dist/tools/percy-mcp/v2/get-build-detail.js +0 -567
  135. package/dist/tools/percy-mcp/v2/get-builds.d.ts +0 -8
  136. package/dist/tools/percy-mcp/v2/get-builds.js +0 -63
  137. package/dist/tools/percy-mcp/v2/get-comparison.d.ts +0 -5
  138. package/dist/tools/percy-mcp/v2/get-comparison.js +0 -94
  139. package/dist/tools/percy-mcp/v2/get-devices.d.ts +0 -5
  140. package/dist/tools/percy-mcp/v2/get-devices.js +0 -33
  141. package/dist/tools/percy-mcp/v2/get-insights.d.ts +0 -7
  142. package/dist/tools/percy-mcp/v2/get-insights.js +0 -52
  143. package/dist/tools/percy-mcp/v2/get-projects.d.ts +0 -6
  144. package/dist/tools/percy-mcp/v2/get-projects.js +0 -41
  145. package/dist/tools/percy-mcp/v2/get-snapshot.d.ts +0 -5
  146. package/dist/tools/percy-mcp/v2/get-snapshot.js +0 -96
  147. package/dist/tools/percy-mcp/v2/get-test-case-history.d.ts +0 -5
  148. package/dist/tools/percy-mcp/v2/get-test-case-history.js +0 -20
  149. package/dist/tools/percy-mcp/v2/get-test-cases.d.ts +0 -6
  150. package/dist/tools/percy-mcp/v2/get-test-cases.js +0 -36
  151. package/dist/tools/percy-mcp/v2/index.d.ts +0 -35
  152. package/dist/tools/percy-mcp/v2/index.js +0 -544
  153. package/dist/tools/percy-mcp/v2/list-integrations.d.ts +0 -5
  154. package/dist/tools/percy-mcp/v2/list-integrations.js +0 -41
  155. package/dist/tools/percy-mcp/v2/manage-domains.d.ts +0 -8
  156. package/dist/tools/percy-mcp/v2/manage-domains.js +0 -33
  157. package/dist/tools/percy-mcp/v2/manage-insights-email.d.ts +0 -8
  158. package/dist/tools/percy-mcp/v2/manage-insights-email.js +0 -49
  159. package/dist/tools/percy-mcp/v2/manage-usage-alerts.d.ts +0 -10
  160. package/dist/tools/percy-mcp/v2/manage-usage-alerts.js +0 -43
  161. package/dist/tools/percy-mcp/v2/migrate-integrations.d.ts +0 -6
  162. package/dist/tools/percy-mcp/v2/migrate-integrations.js +0 -20
  163. package/dist/tools/percy-mcp/v2/preview-comparison.d.ts +0 -5
  164. package/dist/tools/percy-mcp/v2/preview-comparison.js +0 -17
  165. package/dist/tools/percy-mcp/v2/search-build-items.d.ts +0 -12
  166. package/dist/tools/percy-mcp/v2/search-build-items.js +0 -45
  167. package/dist/tools/percy-mcp/workflows/auto-triage.d.ts +0 -7
  168. package/dist/tools/percy-mcp/workflows/auto-triage.js +0 -82
  169. package/dist/tools/percy-mcp/workflows/clone-build.d.ts +0 -22
  170. package/dist/tools/percy-mcp/workflows/clone-build.js +0 -414
  171. package/dist/tools/percy-mcp/workflows/create-percy-build.d.ts +0 -32
  172. package/dist/tools/percy-mcp/workflows/create-percy-build.js +0 -434
  173. package/dist/tools/percy-mcp/workflows/debug-failed-build.d.ts +0 -5
  174. package/dist/tools/percy-mcp/workflows/debug-failed-build.js +0 -122
  175. package/dist/tools/percy-mcp/workflows/diff-explain.d.ts +0 -6
  176. package/dist/tools/percy-mcp/workflows/diff-explain.js +0 -147
  177. package/dist/tools/percy-mcp/workflows/pr-visual-report.d.ts +0 -8
  178. package/dist/tools/percy-mcp/workflows/pr-visual-report.js +0 -184
  179. package/dist/tools/percy-mcp/workflows/run-tests.d.ts +0 -17
  180. package/dist/tools/percy-mcp/workflows/run-tests.js +0 -107
  181. package/dist/tools/percy-mcp/workflows/snapshot-urls.d.ts +0 -18
  182. package/dist/tools/percy-mcp/workflows/snapshot-urls.js +0 -197
@@ -1,601 +0,0 @@
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
- }
@@ -1,8 +0,0 @@
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>;
@@ -1,33 +0,0 @@
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
- }
@@ -1,7 +0,0 @@
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>;
@@ -1,38 +0,0 @@
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
- }